<Coroutine> 5. 코루틴의 예외처리
by BFine참고 & 출처 : https://www.yes24.com/Product/Goods/125014350
가. 코루틴의 예외
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을 호출했기때문에 예외가 발생한것이다.
'공부 > Coroutine' 카테고리의 다른 글
<Coroutine> 7. 코루틴 단위 테스트 (1) | 2024.07.23 |
---|---|
<Coroutine> 6. 일시 중단 함수와 코루틴 멀티스레드 (0) | 2024.07.07 |
<Coroutine> 4. 구조화된 코루틴 (2) | 2024.06.30 |
<Coroutine> 3. 코루틴 컨텍스트(Coroutine Context) (0) | 2024.06.22 |
<Coroutine> 2. CoroutineDispatcher와 CoroutineBuilder 그리고 async (2) | 2024.06.16 |
블로그의 정보
57개월 BackEnd
BFine