You will be fine

<Coroutine> 5. 코루틴의 예외처리

by BFine
반응형

참고 & 출처 : https://www.yes24.com/Product/Goods/125014350

 

코틀린 코루틴의 정석 - 예스24

많은 개발자들이 어렵게 느끼는 비동기 프로그래밍을 다양한 시각적 자료와 설명을 통해 누구나 쉽게 이해할 수 있도록 쓰인 책이다. 안드로이드, 스프링 등 코틀린을 사용하는 개발자들 중 코

www.yes24.com

가.  코루틴의 예외 

 a. 예외 전파

  -  코루틴 실행 중 예외가 발생하면 예외가 발생한 코루틴을 취소되고 부모 코루틴으로 예외가 전파된다. 이어지면 최상위 루트 코루틴까지 전파될 수도 있다.

      => 즉 예외가 발생하면 예외가 발생한 코루틴만 취소 되는 것이 아니라 전파 받은 모든 코루틴이 취소 처리 될 수 있다.

  -  예외가 처리되지 않은 경우 상위 부모 코루틴으로만 전파 된다.

      => 예외는 부모 코루틴 방향으로 전파되나 이 예외로 인해 부모 코루틴이 취소가 된다면 하위 모든 자식 코루틴에 취소가 전파되는 점은 헷갈리지 말자

 

 b.  예외 전파 예시

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")

        launch {
            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}

=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  다른 코루틴들이 완료되기 전에 아래에 있던 1-2-1번 자식 코루틴 작업에서 먼저 예외가 발생한 결과로 모든 작업이 완료되지 못하는 것을 볼 수 있다.

     => 예외의 전파는 1-2-1번 작업 -> 1-2번 작업 -> 1번 작업, 취소 전파는 1번 작업 -> 1-1번 작업 -> 1-1-1번 작업으로 처리 되었다.

  -  이처럼 작은 작업에서 발생한 예외로 인해 큰 작업이 취소되면 특성에 따라 서비스의 안정성에 문제가 생길 수 도 있다. 

 

나. 예외 전파 제한 

 a.  구조화 깨기

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")

        launch {
            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch(context = Job()) {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}
=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main @coroutine#4" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)
1-1번 작업 종료
1-1-1번 작업 종료
1번 작업 종료
루트 작업 종료

  -  예외가 발생하는 코루틴의 1-2번 상단에 새로운 Job을 추가해보면 위처럼 1-1번 작업에는 아까 와는 전파되지 않아 다르게 정상 처리되는 것을 볼 수 있다.

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")

        launch(context = Job()) {
            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}

=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  반면에 1-1번 작업 상단에 Job 객체를 새로 만들어도 동일하게 모든 작업이 완료되지 못한 것을 볼 수가 있다.

  -  Job 객체로 코루틴의 구조화를 깨는 방법은 예외 전파를 제한하는 것 뿐만아니라 취소 전파도 제한하는데 큰 작업이 취소 되었는데 작은 작업은 

       그대로 진행이 된다면 이거는 비동기 작업을 불안정 하게 만든다.

 

 b. SupervisorJob 객체 사용 

  -  자식 코루틴으로부터 예외를 전파받지 않는 특수한 Job 객체로 하나의 자식 코루틴에서 발생한 예외가 다른 자식 코루틴에 영향을 미치지 못하도록 한다.

     => 일반적인 Job 객체는 자식 코루틴에 예외가 발생하면 부모코루틴이 전파받아 취소하지만 SupervisorJob 객체는 예외를 전파받지않아 취소되지 않는다. 

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")

        launch {
            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch(context = SupervisorJob()) {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}

=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main @coroutine#4" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)
1-1번 작업 종료
1-1-1번 작업 종료
1번 작업 종료
루트 작업 종료

  -  SupervisorJob 객체를 사용하니 1-1번 코루틴의 작업들이 취소되지 않고 처리된 것을 볼 수 있는데 이 방법도 runBlocking의 Job 객체와의

      구조화를 여전히 깬다는 단점이 있다.

       => 추가로 예시에서는 어디에서 예외가 발생할지 알기 때문에 발생하는 쪽에만 처리하였지만 실제로는 다양한 곳에서 발생할 수 있다.

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")
    val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
    launch {
        println("1번 작업 시작")
        launch(context = supervisorJob) {
            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch(context = supervisorJob) {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
        supervisorJob.complete()
    }

    delay(10000L)
    println("루트 작업 종료")
}
=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main @coroutine#4" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:30)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)
1-1번 작업 종료
1-1-1번 작업 종료
1번 작업 종료
루트 작업 종료

  -  구조화를 깨지않고 하는 방법으로는 위의 예시처럼  SupervisorJob에 parent 인자로 주면 코루틴의 구조화를 깨지 않고 예외 전파를 제한 할 수 있다.

       => 여기서 주의할 점은 parent의 Job을 받아서 새로운 SupervisorJob 객체를 생성하는 경우 명시적으로 종료처리가 필요하다.

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")
    val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
    val coroutineScope = CoroutineScope(supervisorJob)

    coroutineScope.launch {
        println("1번 작업 시작")
        launch{

            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(5000L)
    println("루트 작업 종료")
    supervisorJob.complete()
}
=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-1-1번 작업 시작
1-2번 작업 시작
1-2-1번 작업 시작
Exception in thread "DefaultDispatcher-worker-1 @coroutine#5" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:33)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:571)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.executeTask(CoroutineScheduler.kt:750)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.runWorker(CoroutineScheduler.kt:678)
at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:665)
루트 작업 종료

  -  CoroutineScope 와 SupervisorJob 를 사용하는 방법도 존재한다.

      => 위의 예시에서 자식코루틴에서 예외가 발생했으나 루트 코루틴까지 전파되지 않는것을 볼수 있다.

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    launch(context = SupervisorJob()) {
        println("1번 작업 시작")
        launch{

            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }
    delay(5000L)
}
=== 결과 ===
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main @coroutine#2" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:29)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  SupervisorJob 객체를 직접 인자로 줄 수 있는데 주의할점은 위 코드에서 예외로 인한 1-1번 작업이 취소가 전파되지 않을거라고 생각하는 부분이다.

      => 이는 launch는 Job이 입력 되는 경우 해당 Job 객체를 부모로 하는 새로운 Job객체를 만들기 때문에 SuperviosrJob에 전파가 되지 않을 뿐이 된다. 

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    
    supervisorScope {
        println("1번 작업 시작")
        launch{

            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }
    delay(5000L)
}
=== 결과 ===
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-1-1번 작업 시작
1-2-1번 작업 시작
Exception in thread "main @coroutine#3" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:29)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)
1-1번 작업 종료
1-1-1번 작업 종료
1번 작업 종료

  -  위처럼 supervisorScope 함수를 사용하면 아까와는 다르게 1-1번 코루틴의 자식코루틴까지 정상적으로 수행되는 것을 볼 수 있다.

      => 이 함수는 호출한 코루틴의 Job객체를 부모로 가지기 때문에 예외 전파가 제한이 되었고 코루틴 구조화도 깨지지 않았다.

  -  추가로 supervisorScope 함수 내부에서 실행되는 코루틴은 부모 자식으로 관계가 설정되며 자식코루틴이 완료되면 자동으로 완료처리 된다.

 

 c.  번외 : 구조화를 깼는데 왜 실행이 안될까?

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")
        CoroutineScope(Dispatchers.Default).launch {

            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}

=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-2-1번 작업 시작
1-1-1번 작업 시작
Exception in thread "main" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:31)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  CoroutineScope를 통해서 별도의 스코프를 가지도록 해서 구조화를 깼으니 취소전파받지 않아서 1-1번 작업이 출력될 것 이라 생각했다. 

  -  하지만 보면 하위 코루틴에서 예외가 발생하면 부모로 전파되어 루트 코루틴(runBlocking)까지가서 main 스레드가 종료되는 것을 추측해볼수 있다.

  -  책을 꼼꼼하게 읽었다면 바로 찾을 수 있었겠지만.. Dispatchers.IO, Default 는 데몬스레드를 생성하기 때문에 main이 종료되어 출력이 되지않았다.

import kotlinx.coroutines.*
import java.lang.IllegalArgumentException
import java.util.concurrent.Executors

fun main(): Unit = runBlocking {
    println("루트 작업 시작")

    launch {
        println("1번 작업 시작")
        val dispatcher = Executors.newSingleThreadExecutor().asCoroutineDispatcher()
        CoroutineScope(dispatcher).launch {

            println("1-1번 작업 시작")

            launch {
                println("1-1-1번 작업 시작")
                delay(1000L)
                println("1-1-1번 작업 종료")
            }

            delay(1000L)
            println("1-1번 작업 종료")
            dispatcher.close()
        }

        launch {
            println("1-2번 작업 시작")

            launch {
                println("1-2-1번 작업 시작")
                delay(100L)
                throw IllegalArgumentException("예외발생!")
            }

            delay(1000L)
            println("1-2번 작업 종료")
        }

        delay(3000L)
        println("1번 작업 종료")
    }

    delay(10000L)
    println("루트 작업 종료")
}
=== 결과 ===
루트 작업 시작
1번 작업 시작
1-1번 작업 시작
1-2번 작업 시작
1-2-1번 작업 시작
1-1-1번 작업 시작
Exception in thread "main" java.lang.IllegalArgumentException: 예외발생!
at com.example.service.Week4Kt$main$1$1$2$1.invokeSuspend(Week4.kt:34)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTaskKt.resume(DispatchedTask.kt:234)
at kotlinx.coroutines.DispatchedTaskKt.dispatch(DispatchedTask.kt:166)
at kotlinx.coroutines.CancellableContinuationImpl.dispatchResume(CancellableContinuationImpl.kt:397)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl(CancellableContinuationImpl.kt:431)
at kotlinx.coroutines.CancellableContinuationImpl.resumeImpl$default(CancellableContinuationImpl.kt:420)
at kotlinx.coroutines.CancellableContinuationImpl.resumeUndispatched(CancellableContinuationImpl.kt:518)
at kotlinx.coroutines.EventLoopImplBase$DelayedResumeTask.run(EventLoop.common.kt:494)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:7)
at com.example.service.Week4Kt.main(Week4.kt)
1-1번 작업 종료
1-1-1번 작업 종료

  -  데몬스레드가 아닌 스레드를 사용하면 위처럼 정상적으로 출력되는 것을 볼수 있는데 주의할점은 명시적으로 종료처리해야 프로세스가 종료된다.

 

다. CoroutineExceptionHandler 

 a.  무엇인가?

   -  위에서 예외 전파를 제한 하는 방법이었다면 CoroutineExceptionHandler는 코루틴의 예외를 처리하는 예외 처리기이다.

 

 b.  상세

  -  CoroutineExceptionHandler 는 인터페이스로 context의 구성요소이므로 Key를 가지는 것을 볼 수 있고 함수로는 handelException을 가지고 있다.

  -  객체 생성의 경우 inline 함수로 CoroutineExceptionHandler를 사용하여 생성할 수 있도록 제공하고 있다.

      => 예외를 처리하는 람다식인 handler를 매개변수로 하여 예외가 발생했을때 어떤 동작을 할지 입력해 예외를 처리할수 있다.   

import kotlinx.coroutines.*
import java.lang.Exception

fun main(): Unit = runBlocking {

    val exceptionHandler = CoroutineExceptionHandler {coroutineContext, throwable ->
        println("예외 잡음!")
    }

    launch(context = exceptionHandler){
        println("1번 작업 시작")
        throw Exception("예외발생")
    }
    delay(1000L)
}
=== 결과 ===
1번 작업 시작
Exception in thread "main" java.lang.Exception: 예외발생
at com.example.service.Week4Kt$main$1$1.invokeSuspend(Week4.kt:16)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  간단하게 생각했을때 위에 처럼 쓰면 되겠구나 했지만 출력에는 예외잡음이라는 내용이 나오지 않았다..

      => 이유는 CoroutineExcpetionHandler는 처리되지 않은 예외만 처리하기 때문이다.

  -  책을 읽으면서 이게 무슨소리지 생각했는데 자식 코루틴에 명시적으로 예외처리하지 않은 경우에 예외는 부모 코루틴으로 전파된다가 배경으로 보면 된다.  

       => CoroutineExcpetionHandler 는 구조화된 여러 코루틴에 설정해도 마지막으로 예외를 전파받는 위치에서만 예외를 처리한다.

  -  주의할점은 CoroutineExcpetionHandler 는 예외를 처리할뿐이지 전파를 제한하지 않는다는 점이다. (try-catch랑 다름!)

import kotlinx.coroutines.*
import java.lang.Exception

fun main(): Unit = runBlocking {

    val exceptionHandler = CoroutineExceptionHandler {coroutineContext, throwable ->
        println("예외 잡음!")
    }

    CoroutineScope(context = exceptionHandler).launch{
        println("1번 작업 시작")
        throw Exception("예외발생")
    }
    delay(1000L)
}
=== 결과 ===
1번 작업 시작
예외 잡음!

  -  아까 예시에서 코루틴 구조화를 깨고 최상위에 CoroutineScope를 만들어서 CoroutineExcpetionHandler를 적용해주면 예외잡음이 출력된다.

import kotlinx.coroutines.*
import java.lang.Exception

fun main(): Unit = runBlocking {

    val exceptionHandler = CoroutineExceptionHandler {coroutineContext, throwable ->
        println("예외 잡음!")
    }
    val supervisor = CoroutineScope(context = SupervisorJob() + exceptionHandler)

    supervisor.launch{
        println("1번 작업 시작")
        throw Exception("예외발생")
    }
    delay(1000L)
}

=== 결과 ===
1번 작업 시작
예외 잡음!

   -  SupervisorJob과 같이 쓰는것도 가능한 이유는 SupervisorJob이 예외를 전파받지 않을뿐 어떤 예외가 발생했는지 정보는 자식코루틴으로 받는다.

 

 c.  try-catch 와 코루틴 예외

import kotlinx.coroutines.*
import java.lang.Exception

fun main(): Unit = runBlocking {

    try {
        launch{
            println("1번 작업 시작")
            throw Exception("예외발생")
        }
    } catch (e : Exception) {
        println("예외잡음!")
    }

    delay(1000L)
}

   -  예외처리는 코루틴 내부에서 해야하는데 빌더함수는 코루틴을 생성할뿐 실제 내부 람다식의 실행은 생성된 코루틴이 스레드로 분배되는 시점에 일어난다.

       => 즉 위의 try-catch는 코루틴 빌더 함수 자체의 실행만 체크하고 람다식은 예외처리 대상이 되지않는다.

import kotlinx.coroutines.*
import java.lang.Exception

fun main(): Unit = runBlocking {
    val async = async {
        println("1번 작업 시작")
        throw Exception("예외발생")
    }

    try {
        async.await()
    } catch (e : Exception) {
        println("예외잡음!")
    }

    delay(1000L)
}
=== 결과 ===
1번 작업 시작
예외잡음!
Exception in thread "main" java.lang.Exception: 예외발생
at com.example.service.Week4Kt$main$1$async$1.invokeSuspend(Week4.kt:9)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:106)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:85)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:59)
at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:38)
at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
at com.example.service.Week4Kt.main(Week4.kt:6)
at com.example.service.Week4Kt.main(Week4.kt)

  -  위의 예시는 비동기 작업의 결과(Deferred)를 호출하는 await 함수에 try-catch 문을 사용해서 예외처리를 하였지만 예외가 전파된것을 볼 수 있다. 

      => async 코루틴 빌더를 사용할때는 전파되는 예외와 await 호출시 노출되는 예외를 모두 처리해야한다!!

 

 d.  전파되지 않는 예외

fun main(): Unit = runBlocking {
    val async = async {
        println("1번 작업 시작")
        throw CancellationException("예외발생")
    }

    try {
        async.await()
    } catch (e : Exception) {
        println("예외잡음!")
    }

    delay(1000L)
}
=== 결과 ===
1번 작업 시작
예외잡음!

  -  아까와 동일한 예시이지만 예외가 부모코루틴으로 전파가 되지않은 것을 볼 수 있는데 코루틴에서는 전파되는 않는 예외가 존재한다.

      => CancellationException은 코루틴의 취소에 사용되는 특별한 예외이기 때문이다.

  -  CancellationException은 특정 코루틴만 취소하는데 사용되며 코루틴 코드상에서 다양하게 쓰이고 있다.


import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    val async = async {
        withTimeout(1000L) {
            println("1번 작업 시작")
            delay(1500L)
        }
    }

    try {
        async.await()
    } catch (e : TimeoutCancellationException) {
        println("예외잡음!")
    }

    delay(1000L)
}
=== 결과 ===
1번 작업 시작
예외잡음!
import kotlinx.coroutines.*

fun main(): Unit = runBlocking {
    val async = async {
        withTimeout(1000L) {
            println("1번 작업 시작")
            delay(1500L)
        }
    }

    async.await()


    delay(1000L)
}
=== 결과 ===
1번 작업 시작
Exception in thread "main" kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
(Coroutine boundary)
at com.example.service.Week4Kt$main$1$async$1$1.invokeSuspend(Week4.kt:9)
at com.example.service.Week4Kt$main$1$async$1.invokeSuspend(Week4.kt:7)
at com.example.service.Week4Kt$main$1.invokeSuspend(Week4.kt:13)
Caused by: kotlinx.coroutines.TimeoutCancellationException: Timed out waiting for 1000 ms
at kotlinx.coroutines.TimeoutKt.TimeoutCancellationException(Timeout.kt:184)
at kotlinx.coroutines.TimeoutCoroutine.run(Timeout.kt:154)
at kotlinx.coroutines.EventLoopImplBase$DelayedRunnableTask.run(EventLoop.common.kt:502)
at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:279)
at kotlinx.coroutines.DefaultExecutor.run(DefaultExecutor.kt:108)
at java.base/java.lang.Thread.run(Thread.java:829)

  -  withTimeOut 함수는 주어진 시간내에 완료되지않으면 TimeoutCancellationException을 발생하며 이 예외는 전파되지않는다.

  -  주의할 사항은 예외가 발생한 코루틴만을 취소시키는데 바로 위의 예시는 runBlocking에서 await을 호출했기때문에 예외가 발생한것이다.

반응형

블로그의 정보

57개월 BackEnd

BFine

활동하기