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()
}
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 사용
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."이 출력 되었을 까요?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.")
}
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.")
그럼 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.")
}
어느 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.")
}
하지만 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 시킬 위치를 한정한다.
'개발이야기 > Kotlin' 카테고리의 다른 글
[Kotlin] 코틀린 let을 null check으로 쓰지 마세요~ (좀더 스마트한 let 사용법) (8) | 2019.07.19 |
---|---|
[Kotlin] Coroutine의 배신 (5) | 2019.05.08 |
[Kotlin] 코틀린 - 코루틴#9 Select (Experimental) (0) | 2019.01.06 |
[Kotlin] 코틀린 - 코루틴#8 - 동기화 제어 (0) | 2018.12.26 |
[Kotlin] 코틀린 - 코루틴#7 - Channels (0) | 2018.12.21 |