본문으로 바로가기

[Kotlin] 코루틴 Exception 추가 예제

category 개발이야기/Kotlin 2019. 1. 23. 00:07
반응형


Exception에 대하여 이전에 내용을 다뤘습니다.

하지만 예제의 내용만으로는 부족한게 많습니다. 따라서 여기서는 조금씩 상황을 바꿔가면 테스트를 진행하고 그 결과에 대해서 확인합니다.


혹시라도 아직 coroutine의 exception에 대해서 읽지 않으신 분은 https://tourspace.tistory.com/154?category=797357 를 먼저 확인하시기 바랍니다

위 글에서 언급했던 기본 예제 코드는 아래와 같습니다.

GlbalScope 내부에서 launch를 하고 그 안에서 exception을 발생 시킵니다.

fun main() = runBlocking {
        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를 Global.launch에 등록시킨 결과 위와 같은 결과가 나옵니다.
하지만 GlobalScope없이 그냥 launch를 시킬 경우 아래와 같이 나옵니다.
fun main() = runBlocking {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }
        val job = 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

Exception in thread "main" java.io.IOException

at MainTest$main$1$job$1$inner$1$1$1.invokeSuspend(MainTest.kt:17)

at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)

at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)

....


그냥 Exception을 맞고 죽어버립니다.


handler를 달아놓은 의미가 없습니다.

Handler의 구현 위치 변경

그럼 이번에는 handler를 아예 main() 함수 밖으로 지정하면 어떻게 될까요?

혹시 runBlocking 안에 정의되서 handler가 동작하지 않을수 있다는 가정으로 예제를 테스트해 봅니다.


fun main() {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }

        runBlocking {
            val job = 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

Exception in thread "main" java.io.IOException

at MainTest$main$1$job$1$inner$1$1$1.invokeSuspend(MainTest.kt:17)

at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)

at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)

....

try-catch 사용

아래와 같이 try-catch 구문을 사용해 보겠습니다.
결과가 어떨지 예상이 가시나요?
fun main() {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }

        runBlocking {
            try {
                val job = launch(handler) {
                    val inner = launch {
                        launch {
                            launch {
                                throw IOException()
                            }
                        }
                    }
                }
                job.join()
            } catch (e: Exception) {
                println("internal try-catch is working!! $e")
            }
        }
        println("End main.")
    }
위 코드에서 "End main."이 출력 되었을 까요?
아쉽지만 아닙니다.

internal try-catch is working!! kotlinx.coroutines.JobCancellationException: BlockingCoroutine is cancelling; job=BlockingCoroutine{Cancelling}@4dcbadb4
Exception in thread "main" java.io.IOException


위와같은 코드가 찍히면서 죽습니다.

특이한건 try catch로 묶었음에도 불구하고 exception이 runBlocking으로 전달됩니다.
이는 당연히 runBlocking 밖으로도 전달 되면서 process가 죽습니다.


특이한 점은 내부 try catch에서 걸려든 exception은 cancellationException입니다.
이는 throw 시킨 exception은 처리 하지 못하고, 외부로 그대로 전달되며, job이 취소되면서 발생한 원하지 않는 exception만 처리 됩니다.

fun main() {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }

        runBlocking {
            try {
                val job = launch(handler) {
                    val inner = launch {
                        launch {
                            launch {
                                throw IOException()
                            }
                        }
                    }
                }
                job.join()
            } catch (ce: CancellationException) {
                println("internal exception1 is working!! $ce")
            } catch (e: Exception) {
                println("internal try-catch is working!! $e")
            }
        }
        println("End main.")
    }


혹시나 하는 노파심에 catch를 분리하지만 결과는 아래와 같은 로그를 찍으면 죽습니다.

internal exception1 is working!! kotlinx.coroutines.JobCancellationException: BlockingCoroutine is cancelling; job=BlockingCoroutine{Cancelling}@4dcbadb4

Exception in thread "main" java.io.IOException



마지막으로 runBlocking 외부를 try catch로 감쌉니다.

당연히 이 코드는 정상적으로 try-catch가 수행 됩니다.

fun main() {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }

        try {
            runBlocking {

                val job = launch(handler) {
                    val inner = launch {
                        launch {
                            launch {
                                throw IOException()
                            }
                        }
                    }
                }
                job.join()

            }
        } catch (e: Exception) {
            println("external try-catch is working!! $e")
        }
        println("End main.")
    }

external try-catch is working!! java.io.IOException

End main.


하지만 여전히 handler는 동작하지 않습니다.

Thread도 영향이 있을까?

fun main() {
        val handler = CoroutineExceptionHandler { _, exception ->
            println("Caught original $exception")
        }

        runBlocking {
            val job = launch(Dispatchers.Default + handler) {
                val inner = launch {
                    launch {
                        launch {
                            throw IOException()
                        }
                    }
                }
            }
            job.join()
        }
        
        println("End main.")
    }


Exception in thread "main" java.io.IOException
at MainTest$main$1$job$1$inner$1$1$1.invokeSuspend(MainTest.kt:22)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)

결과는 역시 Exception을 발생하고 죽습니다.

사실 이렇게 exception을 막기 위해서는 supervisorJob 이나 supervisorScope을 써야 합니다.


superviorJob 과 supervisorScope의 사용

혹시라도 해당 기반 지식이 아직 없다면 아래 링크를 먼저 보고 오시기 바랍니다.
fun main() {
        runBlocking {
            val handler = CoroutineExceptionHandler { _, exception ->
                println("Caught original $exception")
            }
            supervisorScope {
                val job = launch(handler) {
                    val inner = launch {
                        launch {
                            launch {
                                throw IOException()
                            }
                        }
                    }
                }
            }
        }
        println("End main.")
    }


다행스럽게도 위 코드는 handler의 코드까지 정상적으로 찍으면 종료 됩니다.

Caught original java.io.IOException

End main.


그럼 가장 안쪽 launch에 handler를 넣으면 어떻게 될까요?

fun main() {
        runBlocking {
            val handler = CoroutineExceptionHandler { _, exception ->
                println("Caught original $exception")
            }
            supervisorScope {
                val job = launch {
                    val inner = launch {
                        launch {
                            launch(handler) {
                                throw IOException()
                            }
                        }
                    }
                }
            }
        }
        println("End main.")
    }


End main.

Exception in thread "main" java.io.IOException

at MainTest$main$1$1$job$1$inner$1$1$1.invokeSuspend(MainTest.kt:20)

at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)

at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)

...


Exception이 발생하면서 handler는 동작하지 못했지만 supervisorScope 덕분에 "End main"은 출력되었습니다.


사실 4개의 launch 중에 맨 처음 launch를 제외하고 자식 launch 세개에는 handler를 넣더라도 전부 위와같은 error가 떨어집니다.

..

일관성을 못찾겠네요..@.@


아래 코드로 실제 최 상단 handler가 동작하는걸 확인할 수 있습니다.

runBlocking {
        val handler1 = CoroutineExceptionHandler { _, exception ->
            println("Caught original#1 $exception")
        }
        val handler2 = CoroutineExceptionHandler { _, exception ->
            println("Caught original#2 $exception")
        }
        val handler3 = CoroutineExceptionHandler { _, exception ->
            println("Caught original#3 $exception")
        }
        val handler4 = CoroutineExceptionHandler { _, exception ->
            println("Caught original#4 $exception")
        }
        
        supervisorScope {
            val job = launch(handler1) {
                val inner = launch(handler2) {
                    launch(handler3) {
                        launch(handler4) {
                            throw IOException()
                        }
                    }
                }
            }
        }
    }
    println("End main.")


결과는..
Caught original#1 java.io.IOException
End main.


그럼 SupervisorJob은 어떻게 동작하는지 테스트 해 봅니다.


fun main() {
        runBlocking {
            val handler = CoroutineExceptionHandler { _, exception ->
                println("Caught original $exception")
            }

            val supervisorJob = SupervisorJob()

            val job = launch(supervisorJob) {
                val inner = launch {
                    launch {
                        launch {
                            throw IOException()
                        }
                    }
                }
            }

        }
        println("End main.")
    }


End main.

위와 같이 sueprvisorJob을 사용할 경우 원하는대로 exception이 위로 전달되지 않습니다.

어느 launch에 넣더라도 "End main."이 정상적으로 찍힙니다. 단!! 아래와 같은 경우에만 Exception을 찍고 정상적으로 종료 됩니다.


fun main() {
        runBlocking {
            val handler = CoroutineExceptionHandler { _, exception ->
                println("Caught original $exception")
            }

            val supervisorJob = SupervisorJob()

            val job = launch {
                val inner = launch {
                    launch {
                        launch(supervisorJob) {
                            throw IOException()
                        }
                    }
                }
            }

        }
        println("End main.")
    }


Exception in thread "main" java.io.IOException
at MainTest$main$1$job$1$inner$1$1$1.invokeSuspend(MainTest.kt:22)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:32)
at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:233)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.kt:116)
...
End main.


하지만 supervisorJob + handler로 수정한다면 아래와 같이 exception이 handling 됩니다.

Caught original#1 java.io.IOException

End main.


사실 규칙을 딱 찾기에는 예제만 가지고는 어려워 보입니다.

가정하건데 안정하게 exception을 핸들링 하기 위해서는 아래와 같은 방법을 사용하면 될것 같습니다.

  • CoroutineScope 밖에서 try-catch로 묶는다. (코루틴 전체를 외부에서 try-catch로 감싼다.)
    • supervisorScope은 try-catch로 처리되지 않음!!
  • supervisorScope을 이용해서 exception을 propagation 시킬 위치를 한정한다.
  • SupervisorJob을 이용하여 exception을 propagation 시킬 위치를 한정한다.

반응형