C++의 RAII는 자원 관리 문제를 매우 강력하게 해결한다.
그렇다면 자연스럽게 이런 질문이 생긴다.
“왜 Kotlin이나 Android에서는 RAII를 쓰지 않는가?”
정확히 말하면,
Android / Kotlin에서는 RAII를 언어 차원에서 구현할 수 없다.
RAII의 전제 조건
RAII가 성립하려면 다음 조건이 필요하다.
- 객체의 소멸 시점이 명확해야 한다.
- 소멸 시점이 스코프 종료와 일치해야 한다.
- 소멸자가 반드시 즉시 호출되어야 한다.
C++은 이 세 가지를 모두 만족한다.
- 스택 객체는 스코프 종료 시 파괴된다.
- 소멸자는 예외 여부와 관계없이 호출된다.
- 객체의 생명주기는 언어 규칙으로 결정된다.
Kotlin / Java의 객체 생명주기
Kotlin과 Java는 GC 기반 언어다.
- 객체는 힙에 할당된다.
- 객체가 언제 소멸될지는 GC가 결정한다.
- 스코프 종료와 객체 소멸은 무관하다.
다음 코드를 보자.
fun f() {
val obj = Resource()
}
`
이 함수가 끝났다고 해서 Resource가 즉시 소멸된다는 보장은 없다.
객체는 “더 이상 참조되지 않을 뿐”이며,
실제 메모리 해제 시점은 알 수 없다.
이 한 가지 사실만으로 RAII의 전제 조건이 깨진다.
finalizer가 대안이 될 수 없는 이유
Java에는 finalize()가 있었고,
Kotlin에서도 동일한 개념을 사용할 수 있었다.
그러나 finalizer는 RAII의 대안이 되지 못한다.
- 호출 시점이 비결정적이다.
- 호출되지 않을 수도 있다.
- 성능 비용이 크다.
- Android에서는 사실상 사용 금지다.
그래서 modern JVM 생태계에서는
자원 관리를 finalizer에 의존하지 않는다.
try-finally와 use의 한계
Kotlin에는 다음과 같은 패턴이 있다.
lock.lock()
try {
// 작업
} finally {
lock.unlock()
}
또는
FileInputStream("a.txt").use {
// 사용
}
이 방식은 안전하다.
하지만 이것은 RAII가 아니다.
이유는 명확하다.
- 자원 해제는 객체의 소멸이 아니라
- 언어 문법에 의해 수행된다
즉,
- “이 객체가 살아 있는 동안 자원을 가진다”가 아니라
- “이 블록이 끝나면 자원을 해제한다”이다.
RAII의 책임 주체는 타입이지만,
Kotlin 방식의 책임 주체는 코드 구조다.
Android 환경에서의 추가 제약
Android에서는 다음 제약이 더해진다.
- GC 타이밍이 앱 성능에 직접 영향을 준다.
- 메모리 압박이 잦다.
- finalizer는 ANR과 직결될 수 있다.
- 리소스 누수는 즉시 크래시로 이어질 수 있다.
그래서 Android에서는 다음이 강제된다.
- 파일, 커서, 스트림은 명시적으로 닫는다.
- lock은 반드시 try-finally로 해제한다.
- lifecycle에 맞춘 명시적 정리가 필요하다.
RAII처럼 “언어가 알아서 정리해준다”는 접근은 허용되지 않는다.
그럼 Kotlin에서는 RAII가 완전히 불가능한가
완전히 불가능하지는 않다.
하지만 흉내만 낼 수 있을 뿐 동일하지는 않다.
use {}는 RAII의 일부 개념만 차용한다.- inline 함수와 람다를 이용해 스코프를 제한한다.
- 그러나 객체 생명주기와는 여전히 분리되어 있다.
즉, Kotlin의 자원 관리는
- RAII가 아니라
- Scoped Resource Management에 가깝다.
정리
RAII가 가능한 이유는 C++이 특별해서가 아니다.
C++은 다음을 언어 차원에서 보장하기 때문이다.
- 결정적 소멸
- 스코프 기반 객체 생명주기
- 예외와 무관한 소멸자 호출
Kotlin / Android는 GC 기반 언어이며,
객체 소멸 시점이 언어 규칙에 포함되어 있지 않다.
따라서 Android에서의 자원 관리는
RAII가 아니라 명시적 구조와 규칙에 의존한다.