시스템아 미안해
Chapter 1: Safety - Item 8: Close resources with use 본문
자동으로 닫을 수 없는 리소스들이 있기 때문에,
더 이상 필요하지 않을 때는 반드시 close 메서드를 직접 호출해야 합니다.
Kotlin/JVM에서 사용하는 Java 표준 라이브러리에는 이러한 리소스들이 많이 포함되어 있으며,
대표적인 예는 다음과 같습니다.
- InputStream, OutputStream
- java.sql.Connection
- java.io.Reader (FileReader, BufferedReader, CSSParser 등)
- java.net.Socket, java.util.Scanner
이러한 모든 리소스는 Closeable 인터페이스를 구현하고 있으며,
Closeable은 다시 AutoCloseable을 상속합니다.
문제는, 이들 리소스가 비교적 비용이 큰 자원이며
스스로 쉽게 해제되지 않는다는 점입니다.
가비지 컬렉터(Garbage Collector)가 참조가 사라진 리소스를
언젠가는 정리해 주긴 하지만, 그 시점은 보장되지 않고 시간이 걸릴 수 있습니다.
따라서 리소스를 더 이상 사용하지 않을 때
반드시 close 메서드가 호출되도록 보장해야 하며,
전통적으로는 이를 위해 try-finally 블록으로 리소스를 감싸고,
finally 블록에서 close를 호출하는 방식을 사용해 왔습니다.
fun countCharactersInFile(path: String): Int {
val reader = BufferedReader(FileReader(path))
try {
return reader.lineSequence().sumBy { it.length }
} finally {
reader.close()
}
}
이러한 구조는 복잡할 뿐만 아니라 올바르지 않기도 합니다.
그 이유는 다음과 같습니다.
- close 메서드 자체가 예외를 던질 수 있는데, 이 예외가 제대로 처리되지 않을 수 있습니다.
- try 블록의 본문과 finally 블록에서 모두 예외가 발생하는 경우,
실제로는 하나의 예외만 전파되고 나머지 하나는 사라질 수 있습니다.
우리가 기대하는 바람직한 동작은,
새로 발생한 예외 정보가 기존 예외에 함께 추가되는 것입니다.
하지만 이를 올바르게 구현하려면 코드가 매우 길고 복잡해집니다.
이 패턴은 너무 흔하게 사용되기 때문에,
Kotlin 표준 라이브러리에서는 이를 use 함수로 추출해 두었습니다.
use 함수는 리소스를 올바르게 닫고,
예외를 안전하게 처리하기 위해 사용해야 합니다.
이 함수는 Closeable을 구현한 모든 객체에서 사용할 수 있습니다.
fun countCharactersInFile(path: String): Int {
val reader = BufferedReader(FileReader(path))
reader.use {
return reader.lineSequence().sumBy { it.length }
}
}
이 경우 리시버(receiver), 즉 여기서는 reader가
람다의 인자로도 함께 전달되기 때문에,
다음과 같이 문법을 더 간결하게 줄일 수 있습니다.
fun countCharactersInFile(path: String): Int {
BufferedReader(FileReader(path)).use { reader ->
return reader.lineSequence().sumBy { it.length }
}
}
이러한 지원은 파일을 다룰 때 특히 자주 필요하고,
파일을 한 줄씩 읽는 경우도 매우 흔하기 때문에,
Kotlin 표준 라이브러리에는 useLines 함수도 제공됩니다.
useLines 함수는 각 줄을 String으로 이루어진 시퀀스(sequence) 형태로 제공하며,
처리가 완료되면 내부에서 사용된 Reader를 자동으로 닫아 줍니다.
fun countCharactersInFile(path: String): Int {
File(path).useLines { lines ->
return lines.sumBy { it.length }
}
}
이 방식은 아주 큰 파일까지도 처리하기에 올바른 방법입니다.
해당 시퀀스는 필요할 때마다 한 줄씩 읽는 지연(lazy) 방식으로 동작하며,
메모리에는 한 번에 한 줄만 유지하기 때문입니다.
다만 그 대가로, 이 시퀀스는 한 번만 사용할 수 있습니다.
파일의 각 줄을 여러 번 순회해야 한다면,
파일을 그만큼 여러 번 다시 열어야 합니다.
또한 useLines 함수는 표현식(expression) 으로도 사용할 수 있습니다.
fun countCharactersInFile(path: String): Int =
File(path).useLines { lines ->
lines.sumBy { it.length }
}
위의 모든 구현은 파일을 처리할 때 시퀀스(sequence)를 사용하고 있으며,
이는 가장 올바른 접근 방식입니다.
이 덕분에 파일의 전체 내용을 한꺼번에 메모리에 로드하지 않고,
항상 한 줄씩만 읽어 처리할 수 있습니다.
이에 대한 더 자세한 내용은
아이템 54: 여러 처리 단계가 있는 큰 컬렉션에는 시퀀스를 선호하라에서 다룹니다.