잡히지 않은 예외가 발생하면 프로그램이 종료되는 것처럼 코루틴도 잡히지 않은 예외가 발생했을 때 종료된다. 스레드 또한 같은 경우에 종료된다. 차이가 있다면 코루틴 빌더는 부모도 종료시키며, 취소된 부모는 자식들 모두를 취소시킨다는 점이다.

아래 예시는 코루틴이 예외를 받았을 때 자기 자신을 취소하고 예외를 부모로 전파한다(launch). 부모는 자기 자신과 자식들 모두를 취소하고 예외를 부모에게 전파한다(runBlocking). runBlocking은 부모가 없는 코루틴이기 때문에 프로그램을 종료시킨다(runBlocking은 예외를 다시 던진다).

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

        launch {
            delay(2000)
            println("Will not be printed")
        }

        launch {
            delay(500)
            println("Will be printed")
        }
    }
    launch {
        delay(2000)
        println("Will not be printed")
    }
}

Will be printed
Exception in thread "main" java.lang.Error: Some error

launch 코루틴을 더하는 건 아무것도 바꾸지 못한다. 예외는 자식에서 부모로 전파되며, 부모가 취소되면 자식도 취소되기 때문에 쌍방으로 전파된다. 예외 전파가 정지되지 않으면 계통 구조상 모든 코루틴이 취소된다.

20240416_102556.jpg

코루틴 종료 멈추기

코루틴이 종료되기 전에 예외를 잡는 건 도움이 되지만, 코루틴 간의 상호작용은 잡을 통해서 일어나기 때문에, 코루틴 빌더 내부에서 새로운 코루틴 빌더를 try-catch 문을 통해 래핑하는 건 전혀 도움이 되지 않는다.

fun main(): Unit = runBlocking {
    try {
        launch {
            delay(1000)
            throw Error("Some error")
        }
    } catch (e: Throwable) {
        println("Will not be printed")
    }
    launch {
        delay(2000)
        println("Will not be printed")
    }
}

Exception in thread "main" java.lang.Error: Some error

SupervisorJob

코루틴 종료를 멈추는 가장 중요한 방법은 SupervisorJob을 사용하는 것이다. SupervisorJob을 사용하면 자식에게 발생한 모든 예외를 무시할 수 있다.

20240416_103226.jpg

20240416_103235.jpg

일반적으로 SupervisorJob은 다수의 코루틴을 시작하는 스코프로 사용된다.

fun main(): Unit = runBlocking {
    val scope = CoroutineScope(SupervisorJob())
    scope.launch {
        delay(1000)
        throw Error("Some error")
    }
    scope.launch {
        delay(2000)
        println("Will be printed")
    }
    delay(3000)
}

Exception in thread "DefaultDispatcher-worker-1" java.lang.Error: Some error...
Will be printed

20240416_103604.jpg

흔한 실수 중 하나는 SupervisorJob을 다음 코드처럼 부모 코루틴의 인자로 사용하는 것이다. 1에서 정의된 launch가 SupervisorJob을 인자로 받는데, 이럴 경우 SupervisorJob은 단 하나의 자식만 가지기 때문에 예외를 처리하는 데 아무런 도움이 되지 않는다. 따라서 SupervisorJob을 Job 대신 사용하더라도 아무 도움이 되지 않는다.(두 경우 모두 runBlocking의 잡을 사용하지 않기 때문에 예외는 runBlocking으로 전파되지 않는다).

fun main(): Unit = runBlocking {
    // 자식 코루틴 하나가 있고, 부모 코루틴이 없는 잡은 일반 잡과 동일하게 작동한다.
    launch(SupervisorJob()) { // 1
        launch {
            delay(1000)
            throw Error("Some error")
        }
        launch {
            delay(2000)
            println("Will not be printed")
        }
    }
    delay(3000)
}

Exception in thread "main" java.lang.Error: Some error