커스텀 갤러리 기능을 개발할 때 이미지 썸네일을 만드는 과정에서 흔히 하는 실수가 있습니다. BitmapFactory.decodeStream()은 InputStream을 Bitmap으로 디코딩한 후에도 스트림을 자동으로 닫지 않습니다. 개발자가 명시적으로 FileInputStream을 닫아야 하는데, 이를 놓치는 경우가 많습니다. 사용자가 빠르게 스크롤하면 수백 개의 스트림이 열린 채로 남아 파일 디스크립터가 고갈됩니다. 안드로이드도 역시 일반적으로 프로세스당 1024개
의 파일 디스크립터 제한이 있어서 이를 초과하면 앱이 크래시됩니다. 이런 문제를 방지하기 위해 Kotlin의 use
함수를 사용해야 합니다.
use
는 겉보기엔 단순합니다. 리소스를 자동으로 닫아주는 함수입니다. 하지만 실제 구현을 뜯어보면 30줄 남짓한 코드 안에 예외 처리, 성능 최적화, 그리고 JVM의 한계를 극복하기 위한 고민들이 녹아있습니다.
리소스 관리가 왜 이렇게 어려운가
JVM의 가비지 컬렉터는 메모리는 알아서 정리해주지만, 파일 핸들이나 소켓 같은 OS 리소스는 관리하지 않습니다. 운영체제는 프로세스당 동시에 열 수 있는 파일 수를 제한하는데, macOS는 기본 256개, Linux는 보통 1024개입니다. 웹 서버가 요청마다 파일을 열고 닫지 않는다면? 금세 한계에 도달합니다.
ContentResolver로 미디어 파일을 읽을 때도 주의해야 합니다. contentResolver.openInputStream()으로 얻은 스트림을 제대로 닫지 않으면 에뮬레이터에서는 파일이 적어 문제가 안 보이지만, 실제 디바이스에서 수천 개의 사진을 스캔할 때 Too many open files
에러가 발생할 가능성이 매우매우 커집니다!
그렇다고 Java의 finalize()
에 기대는 것도 답이 아닙니다. finalize()
는 실행 시점을 예측할 수 없고, GC 성능을 크게 저하시키며, 예외 처리가 복잡하고, 심지어 객체 부활(resurrection)
같은 위험한 동작도 가능합니다. 이러한 문제들 때문에 Java 9부터 deprecated되었고, OpenJDK의 JEP 421에 따르면 곧 완전히 제거될 예정입니다.
Java의 시도
이런 문제들을 해결하기 위해 Java 7에서는 try-with-resources
구문을 도입했습니다. 하지만 여전히 문법이 장황하고, 더 큰 문제는 예외 처리 방식이었습니다. 비즈니스 로직과 리소스 정리 과정 모두에서 예외가 발생하면 어떤 예외를 보여줘야 할까요?
만약 단순하게 finally 블록에서 close()
를 호출한다면, 원본 예외가 사라지는 심각한 문제가 발생합니다. 디버깅할 때 진짜 원인을 찾을 수 없게 될 것입니다.
Suppressed Exception?
Java 7은 이 문제를 suppressed exception 메커니즘으로 해결했고, Kotlin의 use
는 이를 완벽하게 활용합니다. 원본 예외를 보존하면서 close()
중 발생한 예외는 suppressed로 붙이는 방식입니다.
실제 구현을 보면 이렇습니다.
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
var exception: Throwable? = null
try {
return block(this)
} catch (e: Throwable) {
exception = e // 원본 예외 저장
throw e
} finally {
this.closeFinally(exception)
}
}
internal fun Closeable?.closeFinally(cause: Throwable?) = when {
this == null -> {}
cause == null -> close() // 정상 경로
else -> try {
close()
} catch (closeException: Throwable) {
cause.addSuppressed(closeException) // 핵심!
}
}
이제 스택 트레이스를 보면 두 예외를 모두 확인할 수 있습니다. 원인도 명확하고, 부차적 문제도 놓치지 않게 됩니다. 실제로 프로덕션 이슈를 디버깅할 때 이 정보가 정말 소중해질 수 있습니다.
마치며
리소스 누수는 개발 환경에서는 잘 안 나타나다가 프로덕션에서 터지는 경우가 많습니다. 찾기도 어렵고 고치기는 더 어려운 것 같습니다. Closeable 또는 AutoCloseable로 구현된 객체를 다룰 때, 특히 안드로이드 개발에서는 Stream
, Cursor
, 그리고 Bitmap
같은 리소스를 다룰 때 use
를 사용하는 것이 좋습니다. use
를 습관화하면 리소스 누수로 인한 크래시를 효과적으로 방지할 수 있습니다.