본문 바로가기
Kotlin/Coroutine

(Coroutine) 6. CoroutineExceptionHandler, SupervisorJob

by jaesungLeee 2022. 11. 2.

1. CEH (Coroutine Exception Handler)

GlobalScope와 CoroutineScope를 통해 runBlocking을 사용하지 않고도 별도의 Scope를 만들 수 있게 되었다.

CoroutineScope와 함께 이번에 살펴볼 CEH를 이용하면 더욱 체계적으로 코루틴에서의 예외 처리를 할 수 있다.

 

아래 예시를 살펴보자.

 

suspend fun printRandom1() {
    delay(1000L)
    println(Random.nextInt(0, 500))
}

suspend fun printRandom2() {
    delay(500L)
    throw ArithmeticException()
}

val ceh = CoroutineExceptionHandler { _, throwable ->
    println("Something happened: $throwable")
}

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.IO)
    val job = scope.launch(ceh) {
        launch { printRandom1() }
        launch { printRandom2() }
    }
    job.join()
}

>> Something happened: java.lang.ArithmeticException

 

CEH를 만들기 위해서는 예시처럼 CoroutineExceptionHandler라는 람다를 만들면 된다.

해당 람다는 CoroutineContext와 Exception 두 개의 인자를 받는다.

사용하지 않을 인자의 경우 _ 로 대체할 수 있다.

 

CEH를 만들고 나면 해당 CoroutineContext를 사용할 때 같이 사용할 수 있다.

지금까지 우리는 launch 블록에 CoroutineContext를 넣었는데, 이와 함께 CEH도 넣을 수 있다는 의미이다.

 

해당 예시에서 printRandom2는 ArithmeticException을 발생시키고 있다.

여기서 발생한 Exception은 코루틴의 구조적 원칙에 따라 형제와 부모 코루틴에 전파되기 때문에 모든 Job을 취소하고 CEH가 수행될 것이다.

 

일반적으로 CEH는 공통된 예외 처리 방식을 수행해야 하는 상황에서 많이 쓰인다.

예를 들어, 앱 내에서 예외가 발생할 경우 팝업 다이얼로그를 띄우는 상황이 많은데, 공통으로 처리할 수 있는 모든 예외들에 대해 CEH를 만들게 되면 이러한 상황들을 공통으로 처리할 수 있다는 장점이 있다.

 

2. runBlocking과 CEH

이전 예시에서는 CoroutineScope를 통해 스코프를 만들고 CEH를 사용하였다.

동일하게 runBlocking의 스코프에서도 CEH를 사용할 수 있을 까?

 

suspend fun getRandom1CEH(): Int {
    delay(1000L)
    return Random.nextInt(0, 500)
}

suspend fun getRandom2CEH(): Int {
    delay(500L)
    throw ArithmeticException()
}

fun main() = runBlocking<Unit> {  // 1 최상위 코루틴
    val job = launch (ceh) {  // 2 
        val a = async { getRandom1CEH() }  // 3
        val b = async { getRandom2CEH() }  // 4
        println(a.await())
        println(b.await())
    }
    job.join()
}

>> Exception in thread "main" java.lang.ArithmeticException
>> ...

 

runBlocking을 통해 최상위 코루틴을 만들고 해당 스코프 내에서 launch 빌더를 사용하고 있다.

위 예시에서 정상적으로 CEH를 등록하였지만 CEH는 동작하지 않고 ArithmeticException이 그대로 발생했다.

 

기본적으로, runBlocking에서는 CEH를 사용할 수 없다.

runBlocking은 자식 코루틴이 발생된 예외에 의해 종료되면 그대로 종료되기 때문이다.

 

3. SupervisorJob

일반적인 Job은 예외가 발생하면 해당 Job에 대한 cancel을 양방향으로 전파해 부모 코루틴과 자식 코루틴 모두 cancel하게 된다.

자식이 예외가 생기면 부모의 Job도 cancel되며 자식의 자식까지도 cancel된다는 의미이다.

동일하게 자식의 예외로 인해 부모가 cancel되기 때문에 부모는 또 다른 자식까지도 cancel되어 계층적으로 관련된 모든 Job들은 cancel될 것이다.

 

SupervisorJob은 다르다.

SupervisorJob은 예외가 발생하면 자식만 cancel한다.

발생한 예외에 의한 취소를 아래 방향으로만 내려가게 한다고 이해할 수 있다.

 

suspend fun printRandom1Supervisor() {
    delay(1000L)
    println(Random.nextInt(0, 500))
}

suspend fun printRandom2Supervisor() {
    delay(500L)
    throw ArithmeticException()
}


val sCEH = CoroutineExceptionHandler { _, exception ->
    println("Something happend: $exception")
}

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.IO + SupervisorJob() + sCEH)
    val job1 = scope.launch { printRandom1Supervisor() }
    val job2 = scope.launch { printRandom2Supervisor() }
    joinAll(job1, job2)
}

>> Something happened: java.lang.ArithmeticException
>> 182

 

 

출력 결과에서도 알 수 있듯이 job2는 종료되지 않고 CEH를 수행하였다.

지금까지 살펴본 예시에서는 CEH가 수행되더라도 발생한 예외에 의해 다른 Job들이 모두 cancel되었다.

하지만 job1이 그대로 수행되어 Random Number를 출력하였다.

 

일반적으로 job2가 cancel되면 job2 -> scope -> job1 순서로 cancel될 것이다.

SupervisorJob의 경우 job2에서 발생한 예외는 job2의 자식 코루틴에만 영향을 주고 scope나 job1에는 영향을 주지 않는다.

 

추가로, joinAll은 복수개의 Job에 대해 각각 join을 수행하여 완전히 완료될 때 까지 기다린다.

 

 

4. SupervisorScope

SupervisorScope는 CoroutineScope와 SupervisorJob을 합친듯 한 Scope이다.

 

SupervisorScope는 우리가 coroutineScope 빌더를 사용한 것과 비슷하게 사용할 수 있다.

기본적으로 SupervisorJob과 관련있기 때문에 앞서 살펴본 것 처럼 예외가 발생했을 때 자식 코루틴에게만 영향을 준다.

 

SupervisorScope의 큰 특징 중 하나는 예외가 발생하는 launch 빌더에는 무조건 CEH를 추가해야 한다.

 

아래 예시를 살펴보자.

 

suspend fun printRandom1() {
    delay(1000L)
    println(Random.nextInt(0, 500))
}

suspend fun printRandom2() {
    delay(500L)
    throw ArithmeticException()
}

suspend fun supervisoredFunc() = supervisorScope {
    launch { printRandom1() }
    launch(ceh) { printRandom2() }
}

val ceh = CoroutineExceptionHandler { _, exception ->
    println("Something happend: $exception")
}

fun main() = runBlocking<Unit> {
    val scope = CoroutineScope(Dispatchers.IO)
    val job = scope.launch {
        supervisoredFunc()
    }
    job.join()
}

>> Something happened: java.lang.ArithmeticException
>> 362

 

 

이전에 살펴본 SupervisorJob을 사용했을 때와 동일한 결과을 얻을 수 있다.

 

중요한 점은, SupervisorScope를 사용할 때는 예외가 발생하는 곳에는 반드시 CEH를 붙여야 한다.

만약, CEH가 없는 경우에는 원하는 동작 결과를 얻을 수 없다.

SupervisorScope는 자식 수준에서 예외를 처리하지 않으면 외부로 전파되기 때문이다.

따라서, 예외가 발생하는 곳에는 CEH를 통한 예외 처리를 진행하거나 try..catch를 통한 예외 처리를 해야한다.

 

Reference

https://kotlinlang.org/docs/coroutines-overview.html

'Kotlin > Coroutine' 카테고리의 다른 글

(Coroutine) 7. Flow Basics  (0) 2022.11.02
(Coroutine) 5. Scope  (0) 2022.11.01
(Coroutine) 4. CoroutineContext, Dispatcher  (0) 2022.09.14
(Coroutine) 3. async  (0) 2022.09.14
(Coroutine) 2. Light weight Thread, Job  (0) 2022.09.12