Java/Kotlin

[Coroutine] 코루틴 학습 - 8 (Exception handling)

Icarus8050 2022. 5. 5. 06:56
반응형

Exception handling

  • 코루틴은 익셉션이 발생하면 해당 코루틴을 cancel 시키고, 해당 익셉션을 부모 코루틴으로 전파한다.
  • 익셉션을 전파받은 부모 코루틴은 자신과 자신의 자식 코루틴들을 cancel 시키고, 또 다시 자신의 부모 코루틴으로 익셉션을 전파한다.
  • 위 과정을 반복해서 최상위 코루틴까지 예외가 전파된다.
fun main(): Unit = runBlocking {
    launch {
        launch {
            delay(1000)
            throw Error("Some error")
        }

        launch {
            delay(2000)
            println("출력되지 않는다.")
        }

        launch {
            delay(300)
            println("출력된다.")
        }
    }

    launch {
        delay(2000)
        println("출력되지 않는다.")
    }
}

  • 위 코드에서 예외가 발생하는 코루틴을 try-catch로 래핑하여 감싸는 방법은 Job을 통해서 서로 커뮤니케이션이 일어나는 코루틴의 특성 때문에 cancel 이 전파되는 것을 막을 수 없다.
fun main(): Unit = runBlocking {
    launch {
        try {
            launch {
                delay(1000)
                throw Error("Some error")
            }
        } catch (e: Throwable) {
            println("출력되지 않는다.")
        }

        launch {
            delay(2000)
            println("출력되지 않는다.")
        }
    }
}

 

SupervisorJob

 SupervisorJob은 자식 코루틴에게서 발생하는 cancel 을 무시하도록 하는 Job이다.

fun main(): Unit = runBlocking {
    launch {
        val supervisorJob = SupervisorJob()
        launch(supervisorJob) {
            delay(1000)
            throw Error("Some error")
        }

        launch(supervisorJob) {
            delay(2000)
            println("Inner Coroutine")
        }

        supervisorJob.join()
    }
    launch {
        delay(3000)
        println("Outer Coroutine")
    }
}
// Exception in thread "main" java.lang.Error: Some error
// Innter Coroutine
// Outer Coroutine

 

 일반적으로 부모 코루틴을 argument로 넘기는 경우에 발생하는 실수는 아래의 코드와 같다.

fun main(): Unit = runBlocking {
    // Bad practice
    launch(SupervisorJob()) {
        launch {
            delay(1000)
            throw Error("Some error")
        }

        launch {
            delay(2000)
            println("Inner Coroutine")
        }
    }

    delay(3000)
}

 위 코드는 SupervisorJob이 오직 하나의 자식 코루틴만 있기 때문에 예외 처리에 도움이 되지 않는다. 아래와 같이 여러 coroutine builder의 컨텍스트로 작업을 수행하는 것이 더 좋다. 이는 자식 코루틴들이 각자 취소될 수 있지만, 서로 영향을 주어 취소시키진 않는다.

fun main(): Unit = runBlocking {
    val job = SupervisorJob()
    launch(job) {
        delay(1000)
        throw Error("Some error")
    }

    launch(job) {
        delay(2000)
        println("출력된다")
    }
    job.join()
}
// (1 sec)
// Exception..
// (1 sec)
// 출력된다

 

supervisorScope

 예외가 전파되는 것을 멈추는 또다른 방법은 coroutine builder를 supervisorScope로 래핑하는 것이다. 이는 부모 코루틴과 연결을 유지하기 때문에 편리한 방법이다. 그리고 코루틴으로부터의 예외는 묵음처리 된다.

fun main(): Unit = runBlocking {
    supervisorScope {
        launch {
            delay(1000)
            throw Error("Some error")
        }

        launch {
            delay(2000)
            println("Inner 코루틴")
        }
    }
    delay(1000)
    println("Done")
}
// Exception
// Inner 코루틴
// (1 sec)
// Done

 supervisorScope는 일시중단 함수이며, 일시중단 함수 본문을 래핑하는데 사용할 수 있다. 이는 일반적으로 서로 독립적인 여러 작업들을 시작할 때 많이 사용한다.

 

Await

 예외가 발생하는 케이스에서 코루틴 빌더 async는 launch와 와 다른 코루틴 빌더들과 마찬가지로 그와 관련된 부모 코루틴을 중단시킨다.  하지만 supervisorScope를 이용하면 마찬가지로 cancel이 전파되는 것을 막을 수 있다.

 아래의 코드에서는 supervisorScope 내에서 async 가 바깥으로 예외를 던지는 상황인데, 다른 async는 중단되지 않고 실행이 완료된다.

suspend fun main() = supervisorScope {
    val str1 = async<String> {
        delay(1000)
        throw MyException()
    }

    val str2 = async {
        delay(2000)
        "Text2"
    }

    try {
        println(str1.await())
    } catch (e: MyException) {
        println(e)
    }

    println(str2.await())
}
// MyException
// Text2

 

CancellationException

 발생한 예외가 CancellationException의 서브클래스인 경우에는 부모 클래스로 예외를 전파하지 않고, 현재 코루틴 내에서만 예외가 발생한다.

suspend fun main(): Unit = coroutineScope {
    launch {
        launch {
            delay(2000)
            println("출력되지 않는다.")
        }
        throw CustomNonPropagatingException
    }
    launch {
        delay(3000)
        println("출력된다.")
    }
}

 

Coroutine exception handler

  • CoroutineExceptionHandler를 이용하면 익셉션을 핸들링할 때 기본적으로 수행해야 하는 로직들을 처리하기 편리하다.
  • 핸들러는 예외가 전파되는 것을 막지는 않지만, 예외가 발생했을 때 수행해야 할 로직들을 설정할 수 있다. (로깅 등..)
  • 아래 코드는 CoroutineExceptionHandler와 예외의 전파를 막을 수 있는 SupervisorJob()을 이용한 예시 코드이다.
  • SupervisorJob으로 인해 예외가 전파되지 않기 때문에 두 번째 자식 코루틴이 중지되지 않고 계속 실행된다.
fun main(): Unit = runBlocking {
    val handler = CoroutineExceptionHandler { ctx, exception ->
        println("exception - $exception")
    }
    val scope = CoroutineScope(SupervisorJob() + handler)
    scope.launch {
        delay(1000)
        throw Error("Some error")
    }

    scope.launch {
        delay(2000)
        println("출력된다.")
    }
    scope.coroutineContext.job.children.forEach { it.join() }
}

 


참고자료

https://www.amazon.com/Kotlin-Coroutines-Deep-Marcin-Moskala/dp/8396395837

반응형