Kotlin extensions use를 알아보고, 사용법을 알아보자.



개인 광고 영역

Kotlin의 extensions 중 use가 있다. 이 use의 알맞은 사용 방법을 알아보려면 Java의 Closeable을 간단하게 알고 넘어가면 좋을 것 같다.

이 글에서는 kotlin에서의 Closeable 처리를 알아보고, use를 간단하게 소개한다.


이 글에서 알아볼 내용

  • AutoCloseable과 Closeable을 알아본다.
  • kotlin의 exception 처리를 알아본다.
  • kotlin의 use를 이용하자.


close가 꼭 필요한 것들

개발을 하면서 당연하게 close 처리해야 할 것들이 있다.

  • InputStream, OutputStream
  • java.io.Reader를 상속받는 부분(InputStreamReader 등등)
  • java.net.Socket
  • 등등

이런 class들은 Java 1.7에 추가되어 있는 interface AutoCloseable을 상속 받고 있는데, java 1.5에 추가된 interface Closeable에서 AutoCloseable을 다시 상속받는 형태로 구성되어 있다.

/**
 * 1.7에서 추가
 */
public interface AutoCloseable {
    void close() throws Exception;
}

/**
 * 1.5에서 추가
 */
public interface Closeable extends AutoCloseable {
    public void close() throws IOException;
}


AutoCloseable

AutoCloseable은 아래와 같이 try에 값을 바로 사용하고, 내부적으로 AutoCloseable의 close()를 호출해 준다고 한다.

static String readFirstLineFromFile(String path) throws IOException {
    try (BufferedReader br =
                   new BufferedReader(new FileReader(path))) {
        return br.readLine();
    }
}

Java 1.7 이상에서는 위와 같은 코드가 동작한다. 그리고 1.7 이전 버전에서는 이미 익숙할 try/catch/finally를 직접 정의해 사용했었다.

아래 코드가 누가 보아도 익숙한 코드일듯하다.

static String readFirstLineFromFileWithFinallyBlock(String path)
                                                     throws IOException {
    BufferedReader br = new BufferedReader(new FileReader(path));
    try {
        return br.readLine();
    } finally {
        if (br != null) br.close();
    }
}


kotlin exceptions 처리 방법

kotlin의 exceptions 처리는 버전과 상관없이 아직은 아래와 같이 작성해야 한다.

try {
    // some code
}
catch (e: SomeException) {
    // handler
}
finally {
    // optional finally block
}


kotlin에서 close 처리 방법

코틀린의 close 처리 방법은 다음과 같다.

@Test
fun test() = runBlocking {
    val socket = Socket("thdev.tech", 80)
    val inputStream = socket.getInputStream()
    val reader = InputStreamReader(inputStream)
    println(reader.readText())
    reader.close()
    inputStream.close()
    socket.close()
}

여기에 try/catch/finally를 추가한다면 아래와 같이 추가할 수 있다. 여기서 주의해야 할 부분을 알아보면

  • Socket을 생성하는 부분은 Exception 발생할 수 있다.
  • getInputStream 부분 역시 Exception 발생할 수 있다.
  • finally에 포함한 close 역시 Exception이 발생할 수 있다.
@Test
fun test() = runBlocking {
    var socket: Socket? = null
    var inputStream: InputStream? = null
    var reader: InputStreamReader? = null
    try {
        socket = Socket("thdev.tech", 80)
        inputStream = socket.getInputStream()
        reader = InputStreamReader(inputStream)
        println(reader.readText())
    } catch (e: Exception) {
        // ...
    } finally {
        socket?.close()
        inputStream?.close()
        reader?.close()
    }
}

이 코드를 좀 더 수정하면 아래와 같이 finally 역시 exceptions을 처리해야 한다.

이렇게 처리해야 안전하게 처리하는 방법에 해당한다.

} finally {
    try { socket?.close() } catch (e: Exception) {}
    try { inputStream?.close() } catch (e: Exception) {}
    try { reader?.close() } catch (e: Exception) {}
}

매우 코드가 복잡해졌고, Nullable까지 허용해야 한다.


Closeable을 상속받는 모든 class에 접근할 수 있는 use 확장 함수가 있다.

다행히 코틀린 1.2부터 제공하는 use {}이 제공된다. 이 use extensions은 위에서 보았던 모든 try/catch/finally까지 포함하고 있다.

거기에 close 처리 시 안전하게 처리해 주기 위한 try/catch를 포함하고있다.

하지만 이 코드에서 주의해야 할 부분이 하나 있는데, catch의 오류는 throw e를 하고 있기 때문에 안전하게 사용하려면 최소한의 try/catch 문을 한 번 더 묶어줘야 한다.

@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를 적용해보자.

use를 이용해 매우 간단하게 처리할 수 있다. 다만 use를 사용하더라도, try/catch를 한번 묶어줘야 함을 기억해야 한다.

내부 코드에서 보았듯 exception은 그대로 흘려보내 밖으로 노출시켜준다. 결국 읽기 하다가 오류가 발생하면 밖에서 알아야 하기 때문에 이를 처리해 준다.

그리고 잊지 않고 사용할 수 있는 close는 내부에서 처리해 준다.

@Test
fun test() = runBlocking {
    try {
        Socket("thdev.tech", 80).use {
            it.getInputStream().use {
                InputStreamReader(it).use {
                    // throw java.lang.Exception("Read error!!!!")
                    println(it.readLines())
                }
            }
        }
    } catch (e: Exception) {
        // ...
    }
}

결국 필요한 오류 정보는 외부로 보내주고, 그 외에는 안전하게 사용할 수 있도록 만들어준 extensions을 활용할 수 있다.


Use를 사용할 때 읽기에 주의하라.

위에서 작성한 코드는 중첩으로 use를 활용하고 있다. 모두 it으로 접근하고 있다.

읽기 좋은 Kotlin 코드! Property와 Scope Functions!에서 다루었지만 저렇게 중첩으로 쓸 때는 어디가 어디인지 알기 어렵다.

그래서 명칭을 명확하게 작성해 줄 필요가 있다. it이 어디가 어디까지인지를 명확한 스쿱으로 지정해주는 게 필요하다. 그렇기에 it 대신 이름을 명확하게 지정해 주고 사용하길 추천한다.

@Test
fun test() = runBlocking {
    try {
        Socket("thdev.tech", 80).use { socket ->
            socket.getInputStream().use { inputStream ->
                InputStreamReader(inputStream).use { reader ->
                    println(reader.readLines())
                }
            }
        }
    } catch (e: Exception) {
        // ...
    }
}


마무리

간단하게 close 처리를 해야 하는 클래스들의 사용에 대해 알아보았고, kotlin의 use를 이용한 간단한 처리를 알아보았다.


이 글만 보기 아쉽죠. Effective Kotlin 더 보기

지금까지 작성한 EffectiveKotlin 관련 글은 태그로 모아 링크를 추가한다.

더 보기 - Effective Kotlin 관련 글



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments