본문 바로가기
코틀린

코루틴을 사용한 안전한 예외처리 - 1

by RWriter 2022. 1. 14.
반응형

코루틴을 사용한 예외처리는 꽤나 까다롭다. 

 

두개의 코루틴(A, B)이 실행된다고 할 때 아래 상황을 고려할 수 있다.

 

각각 걸리는 시간

A: 1초

B: 2초

 

  • A가 먼저 끝나고 B가 그 뒤에 예외가 발생함
  • B가 실행 중에 A에서 예외가 발생함
  • A, B 둘 다 예외가 발생함
  • A, B 를 실행시키고 있는 부모 코루틴에서 예외가 발생함
  • A, B 둘 중 하나에서 예외가 발생하였고 main스레드에 의해 runBlocking 안에서 실행되고 있을 때

 

 

 

결론부터 이야기하면 예외처리를 잘 하기 위해서는 국소적으로 try catch를 사용해주는 것이 권장된다.

아래는 CoroutineExceptionHandler에 나오는 docs 내용이다.

 

 

If you need to handle exception in a specific part of the code, it is recommended to use try/catch around the corresponding code inside your coroutine. This way you can prevent completion of the coroutine with the exception (exception is now caught), retry the operation, and/or take other arbitrary actions:

 

 

코루틴에는 CoroutineExceptionHandler 가 있어서 코루틴 안에서 발생한 Exception 을 처리하는 핸들러를 제공하고 있다.

아래 코드에서 기대하는 바는 RuntimeException 이 발생한 이후 handler에서 처리하였기 때문에 예외 없이 안전하게 끝나는 것으로 생각되지만, 실제로는 그렇지 못하다. 

val handler = CoroutineExceptionHandler { _, exception ->
    println("CoroutineExceptionHandler. ${Thread.currentThread().name} got $exception")
}

suspend fun parallelLaunchException_handler_not_working() = coroutineScope {
    launch {
        delay(100)
        println("printed first")
    }
    launch {
        delay(200)
        throw RuntimeException("Boom")
    }
    delay(300)
}

fun main() = runBlocking(handler) {
    parallelLaunchException_handler_working()
}


/**
메인 스레드는 RuntimeException을 처리하지 못하고 뱉어내게 된다.

printed first
Exception in thread "main" java.lang.RuntimeException: Boom
**/

 

CoroutineExceptionHandler 는 Top-Level 코루틴에서 UnCaughted Exception 을 처리하는데 사용된다고 한다.

사실 main 의 runBlocking 위로 어떤 코루틴이 더 있는지는 모르겠는데, 이 마저도 동작을 하지 않는다. (난감)

 

 

핸들러를 동작시키고자 한다면 아래처럼 GlobalScope 에서 코루틴을 실행시켜야 한다. 

suspend fun parallelLaunchException_handler_working() = coroutineScope {
    launch {
        delay(100)
        println("printed first")
    }
    GlobalScope.launch(handler) {
        delay(200)
        throw RuntimeException("Boom")
    }
    delay(300)
}

fun main() = runBlocking {
    parallelLaunchException_handler_working()
}

/**
printed first
CoroutineExceptionHandler. DefaultDispatcher-worker-1 got java.lang.RuntimeException: Boom
**/

 

참고로 GlobalScope 는 싱글톤으로 만들어진 스코프인데, 이를 사용하여 실행시키면 root coroutine 으로 인식이 되고 최상위 코루틴에서까지 잡지 못한 예외를 처리하게 되는 것이다.

GlobalScope 는 이런저런 이유로 사용하지 말고, coroutineScope를 사용하여 코루틴의 범위를 직접 지정하는 것으로 권고하고 있다.

 

여튼 예외가 발생하는 곳으로부터 try, catch 를 사용해주면 된다.

 

1. 국소적으로 try, catch를 해주기

suspend fun parallelLaunchException_catch() = coroutineScope {
    launch {
        delay(100)
        println("printed first")
    }
    launch {
        try {
            delay(200)
            throw RuntimeException("Boom")
        } catch (e: RuntimeException) {
            println(e.message)
        }
    }
    delay(300)
}

/**
printed first
Boom
**/

 

2. 전체를 한번 더 감싸기

suspend fun parallelLaunchException_catch() = coroutineScope {
    launch {
        delay(100)
        println("printed first")
    }
    launch {
        delay(200)
        throw RuntimeException("Boom")
    }
    delay(300)
}

fun main() = runBlocking {
    try {
        parallelLaunchException_catch()
    } catch (e: RuntimeException) {
        println(e.message)
    }
}
반응형

댓글