Kotlin

[Effective Kotlin] 9 - use를 사용하여 리소스를 닫아라

매운돌 2023. 3. 26. 16:44

더 이상 필요하지 않을 때, close 메서드를 사용해서 명시적으로 닫아야 하는 리소스가 있습니다.

Kotlin / JVM에서 사용하는 JAVA 표준 라이브러리에는 이런 리소스들이 굉장히 많습니다.

  • InputStream과 OutputStream
  • java.sql.Connection
  • java.io.Reader(FileReader, BufferedReader, CSSParser)
  • java.new.Socket과 java.util.Scanner

이러한 리소스들은 AutoCloseable을 상속 받는 Closeable 인터페이스를 구현하고 있습니다.

 

이러한 모든 리소스는 최종적으로 리소스에 대한 레퍼런스가 없어질 때, 가비지 컬렉터가 처리합니다. 하지만 굉장히 느리며(쉽게 처리되지 않습니다.)그동안 리소스를 유지하는 비용이 많이 들어갑니다. 따라서 더 이상 필요하지 않는다면, 명시적으로 close 메서드를 호출해 주는 것이 좋습니다.

 

전통적으로 이러한 리소스는 다음과 같이 try-finally블록을 사용해서 처리했습니다.

fun countCharactersInFile(path: String): Int {
	val reader= BufferedReader(FileReader(path))
    try {
    	return reader.lineSequence().sumBy { it.length }
    } finally {
    	reader.close()
    }
}

하지만, 이러한 코드는 복잡하고 좋지 않습니다. 왜냐하면 리소스를 닫을 때 예외가 발생할 수도 있는데, 이러한 예외를 따로 처리하지 않기 때문입니다. 또한 try블록과 finally 블록 내부에서 오류가 발생하면, 둘 중 하나만 전파 됩니다.

하지만 이를 직접 구현하려면 코드가 굉장히 길고 복잡해 집니다. 그래도 굉장히 많이 사용되는 일반적인 구현이므로, 표준 라이브러리에 use라는 함수로 포함되어 있습니다.

/**
 * Executes the given [block] function on this resource and then closes it down correctly whether an exception
 * is thrown or not.
 *
 * @param block a function to process this [Closeable] resource.
 * @return the result of [block] function invoked on this resource.
 */
@InlineOnly
@RequireKotlin("1.2", versionKind = RequireKotlinVersionKind.COMPILER_VERSION, message = "Requires newer compiler version to be inlined correctly.")
public inline fun <T : Closeable?, R> T.use(block: (T) -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    var exception: Throwable? = null
    try {
        return block(this)
    } catch (e: Throwable) {
        exception = e
        throw e
    } finally {
        when {
            apiVersionIsAtLeast(1, 1, 0) -> this.closeFinally(exception)
            this == null -> {}
            exception == null -> close()
            else ->
                try {
                    close()
                } catch (closeException: Throwable) {
                    // cause.addSuppressed(closeException) // ignored here
                }
        }
    }
}

 

use함수를 사용해서 앞의 코드를 적절하게 변경하면 다음과 같습니다. 

fun countCharactersInFile(path: String): Int {
	BufferedReader(FileReader(path)).use { reader ->
    	return reader.lineSequense().sumBy { it.length }
    }
}

파일을 리소스로 사용하는 경우가 많고 파일을 한 줄씩 읽어 들이는 겨우도 많으므로, Kotlin 표준 라이브러리는 파일을 한 줄씩 처리할 때 활용할 수 있는 useLines함수도 제공합니다.

fun countCharactersInFile(path: String): Int {
	BufferedReader(FileReader(path)).useLines { lines ->
    	return lines.sumBy { it.length }
    }
}