본문 바로가기
코틀린

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

by RWriter 2022. 1. 14.
반응형

앞의 글에서 이야기했던 두개의 코루틴(A, B)이 실행된다고 할 때

 

각각 걸리는 시간

A: 1초

B: 2초

 

  • A가 먼저 끝나고 B가 그 뒤에 예외가 발생함
  • B가 실행 중에 A에서 예외가 발생함

예외가 발생하면 어떻게 될까?

 

코루틴은 어떤 스코프에 있었는지에 따라 취소되거나 그렇지 않는다.

기본은 취소(Cancellation)가 된다.

 

 

아래 코루틴을 실행시키면 b가 실행되고 있는 동안 a가 먼저 예외를 발생시키고, 결과적으로 프린트 되는 것은 없다. 취소가 된 것이다.

그리고 main 스레드는 예외가 발생되어 프로그램이 종료된다.

suspend fun parallelLaunchException_1() = coroutineScope {
    launch { // coroutine a
        delay(100)
        throw RuntimeException("boom")
    }
    launch { // coroutine b
        delay(200)
        println("never printed.")
    }
    delay(300)
}

fun main() = runBlocking {
    parallelLaunchException_1()
}

 

결과적으로 예외가 발생하기 때문에 parallelLaunchException_1 을 try catch 로 감싸면 핸들링을 할 수 있지만, 

coroutine b 가 취소되었을 때의 무언가 처리를 하기는 어렵다.

 

코루틴은 취소가 되면 CancellationException 이 발생되는데 이는 launch의 join, async의 await 를 호출할 때 발생한다.

예외는 job1에서 발생되었지만, job1, job2 모두 취소가 되었다.

그리고 실행해보면 기본적으로 취소가 발생한 상황은 부모에게까지 전파되어 RuntimeException("Boom") 이 main 스레드까지 전파된다. 이를 잡고 싶으면 상위에서 한번 더 try catch 로 감싸준다.

suspend fun parallelLaunchException_catch_cancel() = coroutineScope {
    val job1 = launch {
        delay(100)
        throw RuntimeException("boom")
    }
    val job2 = launch {
        delay(200)
        println("never printed.")
    }
    try {
        job1.join()
    } catch (e: CancellationException) {
        println("catch cancelled")
    }
    try {
        job2.join()
    } catch (e: CancellationException) {
        println("catch cancelled2")
    }
}

/**
catch cancelled
catch cancelled2
Exception in thread "main" java.lang.RuntimeException: boom
**/

 

 

Async 코루틴의 예외,취소

launch와 async는 예외전파 방식이 조금 다른데 아래 코드에서 보듯이 a1 이 RuntimeException 을 catch 하고 있다. 

(자세한 내용까지는 잘 모르겠다 ㅠ) 

그리고 이 마저도 상위 부모에게 다시 예외가 전파되어 한번 더 감싸주어야 한다. 

suspend fun parallelAsyncException_1() = coroutineScope {
    val a1 = async {
        delay(100)
        throw RuntimeException("boom")
    }
    val a2 = async {
        delay(200)
        "job2"
    }
    try {
        a1.await()
    } catch (e: RuntimeException) {
        println("raised exception")
    }
    try {
        println(a2.await())
    } catch (e: CancellationException) {
        println("cancelled2")
    }
}

 

 

supervisorScope 의 사용

coroutineScope 대신 supervisorScope 를 사용하게 되면 코루틴에서 발생한 예외를 children, parent에게 전파시키지 않게 된다.

suspend fun parallelAsyncException_1() = supervisorScope {
    val a1 = async {
        delay(100)
        throw RuntimeException("boom")
    }
    val a2 = async {
        delay(200)
        "job2"
    }
    try {
        a1.await()
    } catch (e: RuntimeException) {
        println("raised exception")
    }
    try {
        println(a2.await())
    } catch (e: CancellationException) {
        println("cancelled2")
    }
}

/**
raised exception
job2
**/

 

 

안전하게 처리할 수 있는 방법?

코루틴에 대한 이해가 깊지 않고서는 이처럼 예상치 못한 방식으로 작동하기 때문에 try, catch 를 이용해서 꽁꽁 싸매주는 것이 안전한 방식이라고 생각한다. 기존의 Global Exception Handler 식으로 exception 을 던지기만 하고 상위에서 처리하는 식으로 짜여진 코드와 코루틴을 결합하려고 할 때 문제가 발생할 수 있다.

 

다음 글에는 코틀린의 Functional Programming Library 인 Arrow 를 사용하여 예외를 안전하게 처리하는 방식을 작성하겠다.

반응형

댓글