본문으로 바로가기

[Kotlin] 코틀린 - 코루틴#5 - exception

category 개발이야기/Kotlin 2018. 12. 12. 17:56
반응형

이 글은 아래 링크의 내용을 기반으로 하여 설명합니다.

https://github.com/Kotlin/kotlinx.coroutines/blob/master/coroutines-guide.md

또한 예제에서 로그 print시 println과 안드로이드의 Log.e()를 혼용합니다.


Exception propagation

Coroutine builder들을 Exception handling 측면에서 두가지 타입으로 나뉩니다.

  • Exception을 외부로 전파(propagation) 시킴: launch, actor
  • Exception을 노출(exposing)시킴: async, produce
언뜻 보기엔 말장난 같습니다.
fun main() = runBlocking {
    val job = GlobalScope.launch {
        println("Throwing exception from launch")
        throw IndexOutOfBoundsException() // Will be printed to the console by Thread.defaultUncaughtExceptionHandler
    }
    job.join()
    println("Joined failed job")
    val deferred = GlobalScope.async {
        println("Throwing exception from async")
        throw ArithmeticException() // Nothing is printed, relying on user to call await
    }
    try {
        deferred.await()
        println("Unreached")
    } catch (e: ArithmeticException) {
        println("Caught ArithmeticException")
    }
}



Throwing exception from launch

Exception in thread "DefaultDispatcher-worker-2 @coroutine#2" java.lang.IndexOutOfBoundsException

Joined failed job

Throwing exception from async

Caught ArithmeticException


lauch의경우 exception이 발생하면 바로 예외가 발생합니다.

하지만 async의 경우 코드가 수행되어 exception이 있더라도 실제로 exception이 발생되는 부분은 await()를 만날때 입니다.

전파와 노출의 차이가 구분 되시나요?


CoroutineExceptionHandler

사실 둘중 뭘해도 콘솔에 exception은 발생합니다.


이를 방지하기 위해 CoroutineExceptionHandler를 이용하여 coroutine 내부의 기본 catch block으로 사용할 수 있습니다.

Java에서 Thread에 사용하는 Thread.defaultUncaughtExceptionHandler와 비슷다하고 생각하면 됩니다.

추가적으로 Android에서는 기본으로 uncaughtExceptionPreHandler가 coroutine의 exception 처리를 할수있도록 설정 되어 있습니다.

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    val job = GlobalScope.launch(handler) {
        throw AssertionError()
    }
    val deferred = GlobalScope.async(handler) {
        throw ArithmeticException() // Nothing will be printed, relying on user to call deferred.await()
    }
    joinAll(job, deferred)
}

Caught java.lang.AssertionError


결과를 보면 launch에서 발생한 exception만 처리되었음을 알수있습니다.

async처럼 await()를 만나야 exception이 발생하는 경우에는 동작하지 않습니다.

Cancellation and exceptions

coroutine은 취소되면 CancellationException을 발생시키는것처럼 exception과 매우 밀접한 관계를 같습니다.

특이하게도 CancellationException은 어떤 handler도 처리하지 않습니다.
따라서 catch를 하지 않아도 exception을 발생시키지는 않습니다.
대신 catch를 이용하여 debug log를 찍거나 finally를 이용해서 resource를 해체하는등의 작업을 할수 있습니다.

또한 Job.cancel()을 통해 coroutine을 취소시 부모는 취소되지 않습니다.
(따라서 부모가 자식을 취소할때 Job.cancel()을 사용할 수 있습니다.)
fun main() = runBlocking {
    val job = launch {
        val child = launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                println("Child is cancelled")
            }
        }
        yield()
        println("Cancelling child")
        child.cancel()
        child.join()
        yield()
        println("Parent is not cancelled")
    }
    job.join()
}



Cancelling child

Child is cancelled

Parent is not cancelled


Coroutine은 취소를 제외한 다른 exception이 발생하면 부모의 corouitne까지 모두 취소 시킵니다.

이런 동작은 structured concurrency를 유지하기 위함이기 때문에, CoroutineExceptionHandler를 설정하더라도 막을 수 없습니다.

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception -> 
        println("Caught $exception") 
    }
    val job = GlobalScope.launch(handler) {
        launch { // the first child
            try {
                delay(Long.MAX_VALUE)
            } finally {
                withContext(NonCancellable) {
                    println("Children are cancelled, but exception is not handled until all children terminate")
                    delay(100)
                    println("The first child finished its non cancellable block")
                }
            }
        }
        launch { // the second child
            delay(10)
            println("Second child throws an exception")
            throw ArithmeticException()
        }
    }
    job.join()
    println("End runBlocking")
}


자식 코루틴에서 exception이 발생되면 다른 자식 코루틴이 모두 취소된 이후에 부모에 의해서 exception이 handling 됩니다.

Second child throws an exception

Children are cancelled, but exception is not handled until all children terminate

The first child finished its non cancellable block

Caught java.lang.ArithmeticException

End runBlocking


위 예제에서는 GlobalScope에 handler를 설정합니다.
이유는 자식 coroutine에서 exception이 발생하면, 다른 자식까지 취소된 후에 runBlocking까지도 취소되기 때문에 이 안에서 exception handling을 해봐야 수행되지 않습니다.

이해를 돕기위해 GlobalScope.launch를 launch로 변경한다면 아래와 같이 출력됩니다.

Second child throws an exception

Children are cancelled, but exception is not handled until all children terminate

The first child finished its non cancellable block

Exception in thread "main" java.lang.ArithmeticException

...

...



Exception aggregation

자식 coroutine에서 여러개의 exception이 발생할 경우 가장 먼저 발생한 exception이 handler로 전달됩니다.
(나머지 exception은 무시됩니다.)

fun main() = runBlocking {
    val handler = CoroutineExceptionHandler { _, exception ->
        println("Caught $exception with suppressed ${exception.suppressed.contentToString()}")
    }
    val job = GlobalScope.launch(handler) {
        launch {
            try {
                delay(Long.MAX_VALUE)
            } finally {
                throw ArithmeticException()
            }
        }
        launch {
            delay(100)
            throw IOException()
        }
        delay(Long.MAX_VALUE)
    }
    job.join()  
}

Caught java.io.IOException with suppressed [java.lang.ArithmeticException]

다만 CancellationException의 경우에는 throw 하더라도 handler에서 무시됩니다.

fun main() = runBlocking {//sampleStart
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }
        val job = GlobalScope.launch(handler) {
            val inner = launch {
                launch {
                    launch {
                        throw IOException()
                    }
                }
            }
            try {
                inner.join()
            } catch (e: CancellationException) {
                println("Rethrowing CancellationException with original cause")
                throw e
            }
        }
        job.join()
    }

Rethrowing CancellationException with original cause

Caught original java.io.IOException


handler의 위치, GlobalScope이 아닐때의 exception 처리에 대해서는 아래 링크에서 추가적인 예제로 다룹니다.

https://tourspace.tistory.com/175


반응형