코루틴의 예외 전파
코루틴에서 예외가 발생하면, 그 코루틴은 취소되고 예외가 부모 코루틴으로 전파됩니다.
만약 부모 코루틴에서도 예외가 처리되지 않으면, 예외는 루트 코루틴까지 예외가 전파될 수 있습니다.
하지만, 자식 코루틴 중 하나가 예외가 발생하더라도, 다른 코루틴의 실행에는 영향을 주지 않도록 처리해야 하는 경우가 있습니다.
예를들어, 여러개의 파일을 각 코루틴을 이용해 동시에 다운로드 할때, 특정 파일의 다운로드가 실패하더라도, 나머지 파일은 정상적으로 다운로드되어야 합니다.
이를 해결하기 위해 대표적으로 try-catch를 통해 예외를 개별적으로 처리할 수 있으나,
코루틴에서는 이를 더 간단하고 효율적으로 처리할 수 있는 SupervisorJob, SuperviosrSocpe를 제공합니다.
이러한 예외처리 방법을 통해 우리는 부모까지의 에러 전파를 방지하고, 그 아래 자식 코루틴만 취소시킴으로써 에러 전파의 방향을 단방향으로 만들 수 있습니다.
예외 전파 제한
SupervisorJob과 SupervisorScope를 알아보기 전에, 먼저 Job을 통해 예외 전파를 방지하는 방법을 살펴보겠습니다.
어떻게 Job을 통해 예외전파를 방지할 수 있을까요?
코루틴에서 구조적 동시성은 부모-자식 관계를 기반으로 예외와 취소를 전파합니다.
하지만, 구조적 동시성을 깨뜨리면 예외가 부모 코루틴으로 전파되지 않도록 설정할 수 있습니다.
여기서 Job을 활용하면 부모-자식 간의 참조관계를 끊어 예외 전파를 제한할 수 있습니다.
구조적 동시성 포스팅에 대한 포스팅을 보지 않으셨다면 아래 링크를 참조바랍니다.
https://jtm0609.tistory.com/221
[Android] 코루틴(Coroutine)의 구조적 동시성(Structured Concurrency)
구조적 동시성 (Structured Concurrency) 이란?부모 코루틴은 자신의 스코프를 자식에게 전달하고, 자식 코루틴은 해당 스코프에서 호출을 받는다.이러한 부모-자식 간의 관계를 생성하는 것을 structured
jtm0609.tistory.com
Job 객체를 사용한 예외 전파 제한
코루틴은 자신의 부모 코루틴으로 예외를 전파하는 특성이 있기 때문에, 부모 코루틴과의 구조화를 깬다면 예외가 전파되지 않습니다.
즉, 자식 코루틴이 새로운 Job 객체를 가지면 구조적 동시성 관계가 끊어져, 예외가 부모 코루틴으로 전파되지 않게 됩니다.
다음은 Job 객체를 생성하여 예외 전파를 제한하는 예시코드입니다.
fun main() = runBlocking<Unit> {
launch(CoroutineName("Parent Coroutine")) {
launch(CoroutineName("Coroutine1") + Job()) { // 새로운 Job 객체를 만들어 Coroutine1에 연결
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
delay(1000L)
}
/*
// 결과:
Exception in thread "main" java.lang.Exception: 예외 발생
at chapter8.code2.Code8_2Kt$main$1$1$1$1.invokeSuspend(Code8-2.kt:9)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
[main @Coroutine2#4] 코루틴 실행
Process finished with exit code 0
*/
위 코드에서 Parent Coroutine의 자식 코루틴 중 하나인 Coroutine1이 새로운 Job 객체를 생성한 것을 볼 수 있습니다.
이로 인해 부모-자식 참조 관계가 끊어져 구조적 동시성이 깨지게 되었습니다.
이를 구조화하면 아래와 같습니다.
그렇기 때문에 Corotuine3에서 예외가 발생하더라도, 예외는 Parent Coroutine으로 전파가 되지 않습니다.
또한, 구조적동시성이 끊어졌기 때문에, 예외 전파 뿐만아니라 취소 전파도 제한됩니다.
ParentCoroutine에서 cancel()을 통해 취소를 요청하더라도 Corotuine2에만 취소가 전파되고, 나머지 코루틴인 Corotuine1, Coroutine3에는 전파가 발생하지 않습니다.
SupervisorJob 객체를 사용 예외 전파 제한
SupervisorJob 객체는 자식 코루틴으로부터 발생한 예외를 부모 코루틴으로 전파하지 않는 특수한 Job 객체입니다.
이를 활용하면 하나의 자식 코루틴에서 예외가 발생하더라도 다른 자식 코루틴이나 부모 코루틴에 영향을 미치지 않도록 설계할 수 있습니다.
SupervisorJob은 일반 Job 객체와 마찬가지로 parent 인자를 통해 부모 Job과 연결할 수 있습니다. 부모 Job이 지정되지 않으면 SupervisorJob은 루트 Job으로 동작합니다.
다음은 SupervisorJob을 사용하여 자식 코루틴 간의 예외 전파를 제한하는 예제입니다.
fun main() = runBlocking<Unit> {
val supervisorJob = SupervisorJob()
launch(CoroutineName("Coroutine1") + supervisorJob) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
delay(1000L)
}
/*
// 결과:
Exception in thread "main" java.lang.Exception: 예외 발생
at chapter8.code4.Code8_4Kt$main$1$1$1.invokeSuspend(Code8-4.kt:9)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
[main @Coroutine2#3] 코루틴 실행
Process finished with exit code 0
*/
Coroutine1 내부에서 실행된 Coroutine3이 예외를 발생시켰습니다. 하지만 SupervisorJob은 예외를 부모나 다른 자식 코루틴으로 전파하지 않습니다.
따라서, SupervisorJob의 다른 자식 코루틴인 Coroutine2은 취소되지 않고 정상적으로 실행됩니다.
SupervisorJob을 사용하였기 때문에, Coroutine1인 부모인 SupervisorJob에 예외 전파가 되지 않습니다.
이로 인해 다른 코루틴에도 영향이 가지 않게 되었습니다.
Job을 사용해도 예외 전파가 되지 않는다면서요! Job과 SupervisorJob는 무슨차이죠?
Job과 SupervisorJob 모두 코루틴의 실행 단위를 관리하기 위한 객체로, 예외 전파를 방지하기 위해 새로운 Job을 생성하여 구조적 동시성을 끊는 데 사용할 수 있습니다.
하지만 두 객체의 가장 큰 차이는 예외 전파의 방향과 처리 방식에 있습니다.
바로 위에서 사용했던 예제를 바탕으로 Job과 SupervisorJob의 차이를 알아보겠습니다.
차이가 보이시나요?
SupervisorJob을 사용했을 때는 예외가 부모 코루틴으로 전파되지 않습니다.
하지만, Job을 사용했을 때는 예외가 부모 Job으로 전파되어, 다른 자식 코루틴들에게도 영향을 미치게 됩니다.
따라서, 에러의 전파 방향을 자식으로 한정짓는 것이 바로 SupervisorJob의 특징입니다.
이 동작이 가능한 이유는 SupervisorJob의 내부 코드에서 확인할 수 있습니다.
SupervisorJob은 Job과 동일하게 JobImpl을 상속받아 구현되지만, 내부적으로 onChildCancelled라는 설정이 다릅니다.
자식 코루틴이 취소되거나 예외가 발생하면 onChildCancelled()가 호출됩니다.
그리고 부모 코루틴이 해당 예외를 감지하고, 다른 코루틴까지 취소하게 됩니다.
하지만 SupervisorJob은 onChildCancelled가 false로 설정하기 때문에, 자식 코루틴에서 예외가 발생하더라도 해당 예외가 부모 코루틴에게 전파되지 않습니다.
따라서 다른 자식 코루틴의 실행에는 영향을 주지 않게 됩니다.
코루틴의 구조화를 깨지 않고 SupervisorJob 사용하기
Job과 마찬가지로 SupervisorJob도 구조적 동시성을 깨지 않고 사용할 수 있습니다.
이를 위해 SupervisorJob을 생성할 때 인자로 부모 Job 객체를 명시적으로 전달하면 됩니다.
아래는 코드 예시입니다.
fun main() = runBlocking<Unit> {
// supervisorJob의 parent로 runBlocking으로 생성된 Job 객체 설정
val supervisorJob = SupervisorJob(parent = this.coroutineContext[Job])
launch(CoroutineName("Coroutine1") + supervisorJob) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2") + supervisorJob) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
supervisorJob.complete() // supervisorJob 완료 처리
}
/*
// 결과:
Exception in thread "main" java.lang.Exception: 예외 발생
at chapter8.code5.Code8_5Kt$main$1$1$1.invokeSuspend(Code8-5.kt:9)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
[main @Coroutine2#3] 코루틴 실행
Process finished with exit code 0
*/
runBlocking으로 생성된 Job객체를 SupervisorJob의 파라미터인 parent로 설정함으로써 구조적 동시성을 유지하며 부모-자식 관계를 형성할 수 있습니다.
이를 통해 SupervisorJob은 runBlocking 구조 안에서 동작하며, 자식 코루틴 간의 독립성을 보장할 수 있습니다.
Supervisor Job을 CoroutineScope와 함께 사용하기
지금까지는 아래와 같이 Job을 생성한 후 launch() 함수의 파라미터로 전달하여 사용하는 방식을 살펴보았습니다.
fun main() = runBlocking {
val job = SupervisorJob()
launch(job) {
// Child 1
}
launch(job) {
// Child 2
}
}
이 경우 Job은 부모-자식 참조 관계가 끊어진 상태에서 독립적으로 생성된 상태입니다.
하지만, 기존 부모로부터 상속받은 CoroutineContext의 다른 요소들은 여전히 물려받은 상태입니다.
CoroutineSocpe와 결합하여 SupervisorJob을 사용하는 방법도 있습니다.
fun main() = runBlocking {
val scope = CoroutineScope(SupervisorJob())
scope.launch {
// Child 1
}
scope.launch {
// Child 2
}
}
이 경우는 runBlocking과 완전히 독립된 코루틴 스코프를 생성합니다.
독립적인 CoroutineContext 환경에서 실행되며, SupervisorJob을 가진채로 코루틴을 실행하게 됩니다.
SupervisorScope 객체를 사용한 예외 전파 제한
supervisorScope 함수는 supervisorJob 객체를 가진 CoroutineScope 객체를 생성하며,
이 SupervisorJob 객체는 supervisorScope 함수를 호출한 코루틴의 Job 객체를 부모로 가집니다.
즉, supervisorScope 내부에서 실행되는 코루틴은 SupervisorJob과 부모-자식 관계로 구조화됩니다. (구조적 동시성 유지)
fun main() = runBlocking<Unit> {
supervisorScope {
launch(CoroutineName("Coroutine1")) {
launch(CoroutineName("Coroutine3")) {
throw Exception("예외 발생")
}
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
launch(CoroutineName("Coroutine2")) {
delay(100L)
println("[${Thread.currentThread().name}] 코루틴 실행")
}
}
}
/*
// 결과:
Exception in thread "main" java.lang.Exception: 예외 발생
at chapter8.code8.Code8_8Kt$main$1$1$1$1.invokeSuspend(Code8-8.kt:9)
at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
...
[main @Coroutine2#3] 코루틴 실행
Process finished with exit code 0
*/
Coroutine3에서 예외가 발생했지만, supervisorScope 덕분에 Coroutine2는 예외의 영향을 받지 않고 독립적으로 실행되었습니다.
supervisorScope를 사용하면 복잡한 설정 없이도 구조화를 깨지 않고 예외 전파를 제한할 수 있습니다.
즉, SucpervisorJob을 생성할 때처럼 parent 파라미터에 부모 Job 객체를 전달하는 등의 복잡한 설정을 하지 않아도 됩니다.
단순히, supervisorScope를 호출하는 것만으로도 동일한 효과를 얻을 수 있습니다.
안드로이드에서는 SupervisorJob()이 어떻게 사용되나요?
- lifecycleScope
- viewModelScope
안드로이드에서는 viewModelScope와 lifecycleScope를 생성할 때 coroutineContext에 SupervisorJob()를 설정해줍니다.
따라서, 우리가 viewModelScope.launch{} 또는 lifecycleScope.launch{}를 하나의 파일안에서 여러개를 선언하면 SupervisorJob을 부모로하는 자식 코루틴들이 여러개 생성됩니다.
이를 도식화 하면 아래와 같이 되겠죠
그래서 지금까지 ViewModel에서 작업할 때 하나의 코루틴에서 예외가 발생해도 다른 작업들이 취소되지 않았던 것입니다.
참고자료
https://augustin26.tistory.com/90
[코루틴의 정석] 8장. 예외처리
8.1. 코루틴의 예외 전파 8.1.1. 코루틴에서 예외가 전파되는 방식 코루틴에서 예외가 발생하면, 그 코루틴은 취소되고 예외가 부모 코루틴으로 전달된다.부모 코루틴에서도 예외가 처리되지 않
augustin26.tistory.com
https://thdev.tech/kotlin/2019/04/30/Coroutines-Job-Exception/
Kotlin Coroutines Exception 영향도 알아보기 |
I’m an Android Developer.
thdev.tech
https://velog.io/@lyh990517/Android-Coroutine-SupervisorJob-%EC%99%9C-%EC%8D%A8%EC%9A%94
[Android] Coroutine SupervisorJob 왜 써요?
맨날 job만 쓰다가 최근에 선배가 SupervisorJob을 쓰는걸 보고 왜 쓰는 걸까 궁금해졌습니다. 왜 쓰는 걸까요?
velog.io
kotlin Coroutine: supervisorScope vs SupervisorJob 어떤걸 사용하라는거지?
supervisorScope vs SupervisorJob 사용 방법과 실사례를 통해 어떻게 사용했는지 알아봅시다
velog.io
'Android > Coroutine' 카테고리의 다른 글
[Android] 코루틴(Coroutine)의 Channel (0) | 2025.02.12 |
---|---|
[Android] 예외로인한 코루틴 종료 과정 살펴보기 (0) | 2025.02.10 |
[Android] Callback, Reactive 그리고 Coroutine (0) | 2025.02.06 |
[Android] 중단(suspend)함수를 비동기적으로 실행하는 방법도 있을까? (0) | 2025.02.05 |
[Android] 중단(suspend)함수란 무엇이고 어떻게 동작할까? (feat. delay()) (1) | 2025.02.05 |