본문 바로가기
도서/코틀린 코루틴

2장 코루틴 라이브러리 - 예외 처리

by 안스 인민군 2024. 2. 3.

코루틴은 부모-자식 관계이므로 자식 코루틴이 예외가 발생하면되면 부모 코루틴도 예외가 발생한다.

이러한 해결책을 try-catch 가 아닌 다른 방법을 사용해야 한다. 코루틴의 상호작용으로 인한 예외이므로

try-catch는 아무 기능도 할 수 없다.

SupervisorJob

코루틴 종료를 멈추는 방법은 SupervisorJob를 사용하는 것이다.

SupervisorJob를 사용하면 자식에서 발생한 모든 예외를 무시할 수 있다.

fun main() {
    runBlocking {
        val scope = CoroutineScope(SupervisorJob())

        scope.launch {
            delay(1000)
            throw Exception("error")
        }
        scope.launch() {
            delay(2000)
            println("will be printed")
        }
        delay(3000)
    }
}
// result
Exception in thread "main" java.lang.Exception: error
will be printed

흔히하는 실수 중 하나는, SupervisorJob을 부모의 인자로 사용하는 것 이다.

SupervisorJob은 오직 하나의 자식만 가지기 때문에 예외를 처리하는데 도움이 되질 않는다.

fun main() {
    runBlocking {
        launch(SupervisorJob()) {
            launch {
                delay(1000)
                throw Exception("error")
            }
            launch {
                delay(2000)
                println("will be printed")
            }
        }
        delay(3000)
    }
}
//result
Exception in thread "main" java.lang.Exception: error

하나의 코루틴이 다른 코루틴이 취소되지 않는 다는 점에서 다수의 코루틴에서 Context로 사용하는 방법이 더 나은 방법 이다.

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

    launch(job) {
        delay(2000)
        println("print")
    }
    job.join()
}
// result
Exception in thread "main" java.lang.Error:  error
print

SupervisorScope

예외 전파를 막는 또 다른 방법은 CoroutineBuiilder를 SupervisorScope로 래핑하는 것 이다.

다른 코루틴에서 발생한 예외를 무시하고 부모와의 연결을 유지할 수 있다.

이는 CoroutineScope와 동일한 역할을 하지만 SupervisorJob을 상속한다고 이해하면 된다.

supervisorScope

public suspend fun <R> supervisorScope(block: suspend CoroutineScope.() -> R): R {
    contract {
        callsInPlace(block, InvocationKind.EXACTLY_ONCE)
    }
    return suspendCoroutineUninterceptedOrReturn { uCont ->
        val coroutine = SupervisorCoroutine(uCont.context, uCont)
        coroutine.startUndispatchedOrReturn(coroutine, block)
    }
}
fun main(): Unit = runBlocking {
    supervisorScope {
        launch {
            delay(1000)
            throw Error(" error")
        }

        launch {
            delay(2000)
            println("print")
        }
    }
    delay(1000)
    println("Done")
}
// result
Exception in thread "main" java.lang.Error:  error
print
Done

Await

fun main(): Unit = runBlocking {
    supervisorScope {
        val str1 = async<String> {
            delay(1000)
            throw Throwable("error")
        }
        val str2 = async {
            delay(2000)
            "Text2"
        }
        try {
            println(str1.await())
        } catch (e: Throwable) {
            println(e)
        }
        println(str2.await())
    }
}
// result
java.lang.Throwable: error
Text2

awaitLaunch와 동일한 작동을 하지만 에러를 부모 및 자식에게 전달하는 타이밍이 다르다.

await는 supervisorScope 가 사용되었기 때문에 async는 중단되지 않고 끝까지 실행 된다.

CancellationException은 부모까지 전파되지 않는다.

예외가 CancellationException의 서브클래스라면 부모로 전파되지 않고, 현재 코루틴을 취소시킬 뿐이다.

public actual typealias CancellationException = java.util.concurrent.CancellationException

fun main(): Unit = runBlocking {
    coroutineScope {
        launch { //1
            launch { //2
                delay(2000)
                println("print")
            }
            throw CancellationException() //3
        }
        launch { //4
            delay(2000)
            println("print")
        }
    }
}
//2초뒤
print

위 코드에서 두개의 코루틴 빌더에서 1과 4가 시작되고, 3에서 CancellationException 로 예외를 던진다.

이 예외는 1에서 시작되는 lauch 에서 잡히게 되며 1과 2 의 코루틴을 취소한다.

4에서 시작된 코루틴 은 영향받지 않고 정상적으로 출력할 수 있다.

코루틴 예외 핸들러

예외를 다룰 때 예외가 나타날 시 기본 로직에 대해 정의하고 싶을 때가 있다.

이럴 경우 CoroutineExceptionHandler Context를 사용할 수있다.

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { coroutineContext, throwable ->
        println("error : $throwable")
    }

    val scope = CoroutineScope(SupervisorJob() + handler)

    scope.launch {
        delay(1000L)
        throw Error("Error")
    }
    scope.launch {
        delay(2000)
        println("print")
    }

    delay(3000)
}
// result
error : java.lang.Error: Error
print