Supervision
이전에 공부한 것처럼, 취소는 Coroutine의 전체 계층을 통해 전파되는 양방향 관계를 가진다.*1 단방향 취소만이 필요한 경우를 살펴보자.
이러한 요구사항에 대한 좋은 예제는 Scope 내부에 Job이 선언된 UI 구성요소이다. 만약 UI의 자식의 작업이 실패되더라도, 언제나 모든 UI 구성요소를 취소(효과적으로 종료)하는 것은 필수적이지 않다. 하지만, UI 구성요소가 파괴되면(그리고 그 Job이 취소되면), 더이상 결과값이 필요 없기 때문에 모든 자식 Job을 취소하는 것은 필수적이다.
다른 예시는 여러 자식 Job을 생성하고 이들의 실행이 감독*2되고 그들의 실패가 추적되어서 실패된 것들만 재시작 해야하는 서버 프로세스이다.
Supervision job
SupervisorJob이 이 복적을 위해 사용될 수 있다. 이는 취소가 아래 방향으로 전파되는 것만 제외하면 일반적인 Job과 비슷하다. 이는 다음 예제를 통해 설명될 수 있다.
val supervisor = SupervisorJob()
with(CoroutineScope(coroutineContext + supervisor)) {
// launch the first child -- its exception is ignored for this example (don't do this in practice!)
val firstChild = launch(CoroutineExceptionHandler { _, _ -> }) {
println("The first child is failing")
throw AssertionError("The first child is cancelled")
}
// launch the second child
val secondChild = launch {
firstChild.join()
// Cancellation of the first child is not propagated to the second child
println("The first child is cancelled: ${firstChild.isCancelled}, but the second one is still active")
try {
delay(Long.MAX_VALUE)
} finally {
// But cancellation of the supervisor is propagated
println("The second child is cancelled because the supervisor was cancelled")
}
}
// wait until the first child fails & completes
firstChild.join()
println("Cancelling the supervisor")
supervisor.cancel()
secondChild.join()
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드의 출력은 다음과 같다 :
The first child is failing
The first child is cancelled: true, but the second one is still active
Cancelling the supervisor
The second child is cancelled because the supervisor was cancelled
Supervision Scope
특정 범위에 대한 동시성을 적용하기 위해 coroutineScope 대신 supervisorScope을 사용할 수 있다. 이는 취소를 한 방향으로만 전파하며, 그 자신이 실패했을 때만 자식 Coroutine들을 취소한다. 또한 coroutineScope 처럼 완료되기 전에 모든 자식들이 완료되는 것을 기다린다.
try {
supervisorScope {
val child = launch {
try {
println("The child is sleeping")
delay(Long.MAX_VALUE)
} finally {
println("The child is cancelled")
}
}
// Give our child a chance to execute and print using yield
yield()
println("Throwing an exception from the scope")
throw AssertionError()
}
} catch(e: AssertionError) {
println("Caught an assertion error")
}
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드의 출력은 다음과 같다 :
The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error
Supervise가 사용된 Coroutine에서의 예외
Job과 SupervisorJob의 또다른 중요한 차이는 예외 처리이다. 모든 자식은 자신의 예외를 예외 처리 메커지니즘에 따라 직접 처리해야 한다. 이 다른점은 자식의 실패가 부모에게 전파되지 않는다는 점이다. 이는 supervisorScope 내부에서 직접 실행된 Coroutine은 root Coroutine과 비슷하게 그들의 Scope내부에 설치된 CoroutineExceptionHandler를 쓰는 것을 뜻한다.(자세한 것은 CoroutineExceptionHandler 섹션을 참조)
val handler = CoroutineExceptionHandler { _, exception ->
println("CoroutineExceptionHandler got $exception")
}
supervisorScope {
val child = launch(handler) {
println("The child throws an exception")
throw AssertionError()
}
println("The scope is completing")
}
println("The scope is completed")
📌 전체 코드는 이곳에서 확인할 수 있습니다.
이 코드의 출력은 다음과 같다 :
The scope is completing
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completed
📖 아래 내용은 독자의 이해를 위해 번역자가 추가한 글입니다.
*1. 자식 Coroutine에서 취소가 발생하면 부모 Coroutine으로 전파되며, 부모 Coroutine에게 다른 자식 Coroutine이 있다면 해당 자식 Coroutine 들도 취소된다.
*2. supervise의 뜻이 감독이다.
이 글은 Coroutines 공식 문서를 번역한 글입니다.
원문 : Coroutine exceptions handling - Supervision
원문 최종 수정 : 2022년 6월 27일