(Kotlin Coroutine) Exception Handling
https://kotlinlang.org/docs/exception-handling.html
Job과 SupervisorJob / coroutineScope와 supervisorScope의 예외 전파 차이
Job과 coroutineScope의 동작
- 코루틴에서 발생한 exception이 코루틴 내부에서 catch 되지 않고 바깥으로 나갔다면, 부모, 자식 양방향으로 전파된다.
- child에서 발생하는 경우, parent를 따라 올라가면서 root coroutine 까지 전부 취소된다.
- parent에서 발생해도, child가 취소된다.
- 즉, child에서 발생하면 sibling 까지 전부 취소된다. (launch, async로 진행 중이던 코루틴 까지)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
runBlocking {
println("scope - start")
launch {
println("launch1 - start")
delay(100)
error("launch1 - ex")
}
launch {
println("launch2 - start")
delay(200)
println("never reached")
}
delay(1000)
println("never reached")
}
scope - start
launch1 - start
launch2 - start
[!info] launch1에서 발생한 ex로 인해 launch2와 parent까지 중단되었다.
SupervisorJob과 supervisorScope의 동작
- UI 컴포넌트 처럼, 하나 실패했다고 root 코루틴까지 올라가면서 parent와 sibling을 다 취소할 필요가 없는 경우. 문제가 발생한 그 코루틴과 child만 취소하면 되는 경우는 supervision을 사용한다.
supervisorScope { }
또는SupervisorJob()
- supervisorScope 하위의 launch {} 블록에서 Exception 발생 시 부모, 자식 양방향으로 전파하지 않고
부모 → 자식
방향 으로만 전파하게 된다. - CoroutineExceptionHandler를 지정했다고 하더라도, supervisorScope가 아니면 제대로 catch되지 않고 전부 취소되므로 이런 use case에서는 supervisorScope를 꼭 사용해주어야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
runBlocking {
supervisorScope {
println("scope - start")
launch {
println("launch1 - start")
delay(100)
error("launch1 - ex")
}
launch {
println("launch2 - start")
delay(200)
println("launch2 - end")
}
delay(1000)
println("scope - end")
}
}
scope - start
launch1 - start
launch2 - start
launch2 - end
scope - end
[!info] launch1 - ex가 발생했지만 launch2와 parent는 정상 수행 되었다.
supervisor는 안쪽 스코프로 상속되지 않는다!
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
runBlocking {
supervisorScope {
// 여기는 supervisorScope다.
launch {
// 과연 여기도 supervisorScope 일까? -- XXXX
println("parent1 - start")
launch {
println("child1 - start")
delay(100)
error("child1 - ex")
}
launch {
println("child2 - start")
delay(200)
println("child2 - end")
}
delay(500)
println("parent1 - end")
}
launch {
println("parent2 - start")
delay(1000)
println("parent2 - end")
}
}
}
parent1 - start
parent2 - start
child1 - start
child2 - start
parent2 - end
[!danger] child1 - ex로 인해 parent1과 child2까지 같이 취소되었다. 즉 바깥쪽 supervisorScope의 영향을 받지 않았다.
반면 supervisorScope의 영향을 받는 parent2는 정상 실행 완료 되었다.
supervisorScope를 사용하는 경우 예외 처리
launch block의 ex 처리는 (root 또는 개별) handler를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
runBlocking {
supervisorScope {
println("scope - start")
launch(CoroutineExceptionHandler { _, _ -> println("### my root coroutine handler") }) {
println("launch1 - start")
error("launch1 - ex")
}
delay(1000)
println("scope - end")
}
}
scope - start
launch1 - start
### my coroutine handler
scope - end
[!info] launch에 handler가 지정되지 않은 경우, parent(root) handler를 호출한다.
물론 parent, sibling 코루틴은 취소되지 않는다.
async block의 ex 처리는 try-catch (또는 root handler)를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
val handler = CoroutineExceptionHandler { _, throwable ->
println("CoroutineExceptionHandler handle this : $throwable")
}
suspend fun aa() = supervisorScope {
val a = async(handler) { // handler 적용되지 않는다. launch와 다르다.
println("pre A")
error("A")
}
try {
a.await()
} catch (e : Exception) {
println("try-catch handle this : $e")
}
val b = async {
kotlin.runCatching {
delay(100)
println("pre B")
error("B")
}.onFailure {
handler.handleException(this.coroutineContext, it)
}
}
val c = async { runCatching {
delay(200)
println("pre C")
error("C")
} }
val cr = c.await()
cr.exceptionOrNull()?.let { handler.handleException(this.coroutineContext, it) }
val d = async {
println("pre D")
error("D")
}
delay(1000)
println("END")
}
pre A
try-catch handle this : java.lang.IllegalStateException: A
pre B
CoroutineExceptionHandler handle this : java.lang.IllegalStateException: B
pre C
CoroutineExceptionHandler handle this : java.lang.IllegalStateException: C
pre D
END
- A는 handler가 붙어있으나, handler를 타지 않는다.
- async는 await 할 때 ex가 전파되는 구조로, async에 handler를 등록해도 적용되지 않는다.
async
builder always catches all exceptions and represents them in the resultingDeferred
object, so itsCoroutineExceptionHandler
has no effect either.- 게다가 생각해보니 async-await는 parent에서 실행 결과를 받아 볼 수 있어야 하기 때문에, 실패한 경우에 대한 처리가 parent에 반드시 들어갈 수 밖에 없다. (실패해서 실행 결과가 없으면 parent도 진행을 못하니)
- B는 코루틴에서 Exception이 발생하는 그 즉시 handler를 호출한다 (launch 처럼)
- C는 await 후 handler를 호출한다. (try - catch 방식)
- D는 부모에서 await 하지 않았으므로 error가 일어나지 않은 것 처럼 처리된다.
[!info] catch 되지 않은 exception은 parent(root) handler로 전달된다.
supervisorScope를 사용하지 않는 경우 예외 처리
launch든 async든 exception 처리에 try-catch를 사용해서는 안된다.
[!warning] supervisorScope가 아닌 환경에서 try-catch를 쓰면 안되는 이유는, child에서 ex가 발생하면 parent 코루틴이 항상 취소되기 때문이다.
문제는 parent 코루틴이 어느 라인에서 종료될지 정확히 예측 할 수 없다는데 있다. 이 것이 언제는 catch를 타고, 언제는 타지 않는 동작의 원인이다.
예제는 async 사용했지만 launch(join)도 동일하다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
runBlocking {
println("scope - start")
val deferred1 = async {
println("async1 - start")
delay(200)
error("async1 - ex")
}
delay(100)
try {
deferred1.await()
println("try - end") // never reached
} catch (e: Exception) {
println("catch - async1")
}
delay(1000)
println("never reached")
}
scope - start
async1 - start
catch - async1
[!info] async1 - ex가 발생하는 시점에 parent는 await 하고 있어 parent가 종료되기 전 catch로 진입해 catch - async1이 출력되었다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
runBlocking {
println("scope - start")
val deferred1 = async {
println("async1 - start")
delay(200)
error("async1 - ex")
}
delay(100)
try {
deferred1.await()
println("try - end")
} catch (e: Exception) {
println("catch - async1 - start")
delay(500)
println("catch - async1 - end")
}
delay(1000)
println("never reached")
}
scope - start
async1 - start
catch - async1 - start
[!info] async1 - ex가 발생하는 시점에 parent는 await 하고 있어 parent가 종료되기 전 catch로 진입해 catch - async1 - start이 출력되었다.
그러나 그 다음 delay(500) 수행하는 와중에 parent가 종료되어 catch - async1 - end 라인은 수행되지 않았다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
runBlocking {
println("scope - start")
val deferred1 = async {
println("async1 - start")
delay(100)
error("async1 - ex")
}
delay(200)
try {
deferred1.await()
println("try - end")
} catch (e: Exception) {
println("catch - async1") // never reached
}
delay(1000)
println("never reached")
}
scope - start
async1 - start
[!info] async1 - ex가 발생하는 시점에 parent는 delay(200) 하고 있어 try 진입 전 parent가 종료되었다.
launch든 async든 ex 처리는 root handler를 사용해야 한다.
1
2
3
4
5
6
7
8
9
10
11
12
CoroutineScope(CoroutineExceptionHandler { _, _ -> println("### my root coroutine handler") }).launch {
println("scope - start")
launch(CoroutineExceptionHandler { _, _ -> println("never reached") }) {
println("launch1 - start")
error("launch1 - ex")
}
delay(100)
println("scope - end")
}
Thread.sleep(200)
scope - start
launch1 - start
### my root coroutine handler
[!info] supervisorScope를 사용하지 않는 경우, 예외는 parent로 전파되고, 이에 대한 처리도 parent(결국 root까지)에 위임한다.
그래서 이러한 자식 코루틴들에게 설치 된CoroutineExceptionHandler
는 절대 사용되지 않는다.
참고
runBlocking에 지정된 handler는 무시된다
- 사유
runBlocking
은try-catch
로 감싸는 것이 맞다.