티스토리 뷰
코루틴은 잡히지 않은 예외가 발생했을 때 종료된다
스레드도 동일한 경우에 종료되지만, 차이점이 있다.
코루틴 빌더는 부모도 종료시키고, 종료된 부모는 자식들까지 모두 취소시킨다는 것이다.
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에서 예외를 발생하면 자기 자신을 취소하고 예외를 부모(바깥쪽 launch)로 전파한다.
- 바깥쪽 launch는 자기 자신과 모든 자식들을 취소하고, 예외를 부모(runBlocking)에게 전파한다.
- runBlocking은 루트 코루틴이기 때문에 프로그램을 종료시킨다. (runBlocking은 예외를 다시 던진다)

예외는 자식에서 부모로 전파되고, 부모에서 모든 자식에게 전파된다.
따라서 예외 전파가 멈추지 않으면 구조상 모든 코루틴이 취소된다.
코루틴 종료 멈추기
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...
부모-자식 코루틴 간의 상호작용은 잡을 통해서 일어난다.
따라서 코루틴 빌더 내부에서 자식 코루틴 빌더를 try-catch 문으로 감싸는 건 아무런 의미가 없다.
SupervisorJob
코루틴의 종료를 멈추는 가장 중요한 방법은 SupervisorJob을 사용하는 것이다.
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...
Will be printed
일반적으로 SupervisorJob은 여러 코루틴을 시작하는 스코프로 사용된다.
fun main(): Unit = runBlocking {
launch(SupervisorJob()) {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Will not be printed")
}
}
delay(3000)
}
Exception...
흔한 실수 중 하나는 SupervisorJob을 부모 코루틴의 인자에 전달하는 것이다.
이 경우 SupervisorJob은 단 하나의 자식(바깥쪽 launch)만 가지기 때문에 예외 전파 방지에 아무런 도움이 되지 않는다.
자식 코루틴이 하나고 부모 코루틴이 없는 잡은 일반 잡과 동일하게 작동한다.
fun main(): Unit = runBlocking {
val job = SupervisorJob()
launch(job) {
delay(1000)
throw Error("Some error")
}
launch(job) {
delay(2000)
println("Will be printed")
}
job.join()
}
(1초 후)
Exception...
(1초 후)
Will be printed
하나의 SupervisorJob을 다수의 코루틴의 컨텍스트로 사용하는 것이 좋다.
이렇게 하면 하나의 코루틴이 취소되어도 다른 코루틴이 취소되지 않는다.
supervisorScope
fun main(): Unit = runBlocking {
supervisorScope {
launch {
delay(1000)
throw Error("Some error")
}
launch {
delay(2000)
println("Will be printed")
}
}
delay(1000)
println("Done")
}
Exception...
Will be printed
(1초 후)
Done
예외 전파를 막는 또 다른 방법은 코루틴 빌더를 supervisorsScope로 감싸는 것이다.
supervisorsScope는 다른 코루틴에서 발생한 예외를 무시하고 부모와의 연결을 유지한다.
suspend fun notifyAnalytics(actions: List<UserAction>) =
supervisorScope {
actions.forEach { action ->
launch {
notifyAnalytics(action)
}
}
}
supervisorScope는 중단 함수 본체를 감싸는 데 사용된다.
일반적으로 서로 무관한 다수의 작업을 supervisorScope 스코프 내에서 실행한다.
또 다른 방법은 coroutineScope를 사용하는 것이다.
coroutineScope는 부모에 예외가 전파되는 대신 try-catch를 이용해 잡을 수 있는 예외를 던진다.
supervisorscope와 coroutineScope의 자세한 내용은 다음 포스팅에서 살펴본다.
suspend fun sendNotifications(
notifications: List<Notification>
) = withContext(SupervisorJob()) {
for (notification in notifications) {
launch {
client.send(notification)
}
}
}

withContext(SupervisorJob())과 supervisorScope는 같지 않다는 걸 주의해야 한다.
잡은 부모로부터 상속받지 않는 컨텍스트이기 때문에 문제가 발생한다.
코루틴은 각각 자신만의 잡을 가지고 있고,잡을 다른 코루틴에 전달하여 부모-자식 관계를 맺는다.
위 코드에서 withContext의 부모는 SupervisorJob이 된다.
따라서 자식 코루틴에서 예외가 발생하면 withContext 코루틴으로 전달이 된다.
그 후 잡이 취소되고, 모든 자식 코루틴이 취소되며, 예외가 던져진다.
Supervisorjob이 부모가 되어도 예외 전파 방지에 도움이 되지 않는다.
await
SupervisorJob이나 supervisorScope를 사용하면 예외가 부모로 전파되지 않는다는 걸 배웠다.
그럼 await를 호출하면 어떻게 될까? 아래 코드를 보자.
class MyException : Throwable()
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
첫 번째 코루틴은 예외로 종료되었기 때문에 반환할 값이 없다.
하지만 await()가 MyException을 던지기 때문에 MyException이 출력된다.
즉, 예외로 종료된 async 코루틴 빌더의 await()를 호출하면, 예외가 다시 던져진다.
supervisorScope가 사용되었기 때문에 두 번째 코루틴은 중단되지 않고 끝까지 실행된다.
CancellationException은 부모까지 전파되지 않는다
예외가 CancelAationException의 하위 클래스라면 부모로 전파되지 않는다. 현재 코루틴을 취소시킬 뿐이다.
CancellationException은 open 클래스이기 때문에 다른 클래스에서 상속받을 수 있다.
object MyNonPropagatingException : CancellationException()
suspend fun main(): Unit = coroutinescope {
launch { // 1
launch { // 2
delay(2000)
println("Will not be printed")
}
throw MyNonPropagatingException
}
launch { // 3
delay(2000)
println("Will be printed")
}
}
(2초 후)
Will be printed
위 코드의 흐름을 정리하면 아래와 같다.
- 1번과 3번 코루틴이 launch 빌더로 시작된다.
- 1번 코루틴 내부에서 CancellationException의 하위 클래스인 MyNonPropagatingException 예외를 던진다.
- 1번 코루틴에서 예외가 잡힌다.
- 1번 코루틴은 자기 자신을 취소하고, 자식인 2번 코루틴도 취소시킨다.
- 3번 코루틴은 예외에 영향을 받지 않고 2초 후 "Will be printed"를 출력한다.
CancellationException의 하위 클래스인 MyNonPropagatingException가 던져졌기 때문에 1번 코루틴은 부모로 예외를 전파시키지 않는다.
따라서 3번 코루틴이 끝까지 실행된다.
코루틴 예외 핸들러
fun main(): Unit = runBlocking {
val handler =
CoroutineExceptionHandler { ctx, exception ->
println("Caught $exception")
}
val scope = CoroutineScope(SupervisorJob() + handler)
scope.launch {
delay(1000)
throw Error("Some error")
}
scope.launch {
delay(2000)
println("Will be printed")
}
delay(3000)
}
Caught java.lang.Error: Some error
Will be printed
예외를 처리하는 기본 행동을 정의해야 할 때가 있다. 이런 경우 CoroutineExceptionHandler 컨텍스트를 사용하면 된다.
예외 전파를 중단시키지는 않지만, 예외가 발생했을 때 해야 할 작업을 정의하는 데 사용한다.
예를 들어 안드로이드에서는 CoroutineExceptionHandler를 사용해 공통적인 Dialog나 에러 메시지를 보여주면서 어떤 문제가 발생했는지 사용자에게 알려줄 수 있다.
출처
'kotlin > coroutines' 카테고리의 다른 글
2.7 코틀린 코루틴 라이브러리 - 디스패처 (2) | 2025.01.16 |
---|---|
2.6 코틀린 코루틴 라이브러리 - 코루틴 스코프 함수 (0) | 2025.01.14 |
2.4 코틀린 코루틴 라이브러리 - 취소 (0) | 2025.01.12 |
2.3 코틀린 코루틴 라이브러리 - 잡과 자식 코루틴 기다리기 (0) | 2025.01.12 |
2.2 코틀린 코루틴 라이브러리 - 코루틴 컨텍스트 (0) | 2025.01.11 |