Notice
Recent Posts
Recent Comments
Link
«   2024/07   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30 31
Archives
Today
Total
관리 메뉴

디지안의 개발일지

[TIL] Exception handling in Kotlin Coroutines 본문

Kotlin

[TIL] Exception handling in Kotlin Coroutines

안덕기 2022. 2. 23. 20:53

이 문서는 Kt.Academy를 보고 배운 내용을 작성하였습니다.

코루틴을 다루는데 있어서 예외처리는 매우 중요한 부분이다. 코루틴에서도 예외가 발생하면 코루틴 자체가 중단된다. 코루틴 빌더는 예외에 의해 코루틴이 취소되면 상위 코루틴도 취소하고 각 상위 코루틴의 하위 코루틴을 삭제한다.

다음 예제를 보자. 아래와 같이 에러가 발생하기 전에 실행된 코루틴은 실행되지만 하위 코루틴에서 예외가 발생하면 모든 코루틴이 취소된다.

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

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

        launch {
            delay(500) // faster than the exception
            println("Will be printed")
        }
    }

    launch {
        delay(2000)
        println("Will not be printed")
    }
}
// Will be printed
// Exception in thread "main" java.lang.Error: Some error...

결과적으로 이미 실행된 코루틴이 아니라면 계층 구조 내에 있는 모든 코루틴이 취소될 수 있다.

https://marcinmoskala.com/coroutines_book/manuscript/resources/calcellation.png

코루틴이 취소 되지 않게 하려면

아래와 같이 코루틴이 실행된 밖에 try-catch 문을 감싼다고 해서 코루틴의 취소되는 것을 방지할 수 없다.

fun main(): Unit = runBlocking {
    // Don't wrap in a try-catch here. It will be ignored.
    try {
        launch {
            delay(1000)
            throw Error("Some error")
        }
    } catch (e: Throwable) { // nope, does not help here
        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은 하위 코루틴의 모든 예외를 무시하는 특별한 종류의 작업이다.

https://marcinmoskala.com/coroutines_book/manuscript/resources/cancellation_supervisor_1.pnghttps://marcinmoskala.com/coroutines_book/manuscript/resources/cancellation_supervisor_2.png

일반적으로 여러 개의 코루틴 scope를 사용할 때 주로 사용한다.

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을 사용하는데 일반적인 실수는 SupervisorJob을 상위 코루틴에 대한 인수로 사용하는 것이다. 이는 예외를 처리하는데 도움이 되지 않는다. 이러한 경우 SupervisorJob에 1개의 직계 하위 코루틴만 생성된 것으로 보기 때문이다. 예외가 발생하면 전체적으로 멈추게 된다.

fun main(): Unit = runBlocking {
    // Don't do that, SupervisorJob with one children 
    // and no parent works similar to just Job
    launch(SupervisorJob()) { // 1
        launch {
            delay(1000)
            throw Error("Some error")
        }

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

    delay(3000)
}
// Exception... 

그래서 아래와 같이 동일한 작업을 여러 코루틴 빌더의 컨텍스트로 사용하는 것이 더 합리적이다. 그러면 각각이 취소되지만 서로 취소되지는 않는다.

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 sec)
// Exception...
// (1 sec)
// Will be printed

supervisorScope

다른 방법은 supervisorScope로 묶는 것이다. 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 sec)
// Done

supervisorScope는 suspending 함수고 suspending 함수 본문을 래핑하는데 사용할 수 있다. 예를 들어, 아래와 같이 여러 독립작업을 시작하도록 사용할 수 있다.

suspend fun notifyAnalytics(actions: List<UserAction>) =
    supervisorScope {
        actions.forEach { action ->
            launch {
                notifyAnalytics(action)
            }
        }
    }

Await

예외 전파를 중지하는 방법을 알고 있지만 때로는 충분하지 않을 수도 있다. 코루틴 빌더 async는 다른 코루틴 빌더와 마찬가지로 상위 코루틴을 모두 중단시킨다. 만약 아래와 같이 async로 만든 코루틴을 예외 방지하면 어떻게 될까?

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이 된다. 다른 async는 supervisorScope를 사용한다는 사실 덕분에 중단 없이 완료된다.

CancellationException은 상위 코루틴에 전파되지 않는다.

CancellationException의 하위 클래스인 예외는 상위 코루틴으로 전파되지 않는다. 단지 현재 코루틴을 취소할 뿐이다. CancellationException은 open class이기 때문에 자신의 클래스나 객체로 확장할 수 있다.

import kotlinx.coroutines.*

object MyNonPropagatingException : CancellationException()

suspend fun main(): Unit = coroutineScope {
    launch { // 1
        launch { // 2
            delay(2000)
            println("Will not be printed")
        }
        throw MyNonPropagatingException // 3
    }
    launch { // 4
        delay(2000)
        println("Will be printed")
    }
}
// (2 sec)
// Will be printed 

위의 1코루틴이 취소 됬기 때문에 2는 상위 코루틴이 취소 됬기 때문에 취소가 되고 4는 상관 없이 실행된다.

Coroutine exception handler

코루틴 예외를 다룰 때 모든 예외에 대한 기본 동작을 정의하는 것이 유용하다. 이 때 CoroutineExceptionHandler를 사용하면 유용하다. 예외 전파를 막지는 않지만 예외의 경웨 발생해야 하는 상황을 설정하는데 사용할 수 있다.

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

이 컨텍스트는 많은 플랫폼에서 예외를 처리하는 기본 방법을 추가하는데 유용하다.