Exception Handling
이번 장에서는 예외 처리 및 예외에 따른 취소에 대하여 확인합니다. 우리는 이미 중단된 곳에서(suspension points) 취소가 될 경우 CancellationException 이 발생하며, 코루틴의 메커니즘에 의해 무시되는것을 알고 있습니다. 이 장에서는 취소 중에 예외가 발생하거나, 동일한 코루틴의 여러 자식 코루틴에서 예외를 발생하는 경우에 대하여 살펴봅니다.
Exception propagation
코루틴 빌더는 예외상황을
- 자동적으로 외부로 전파(propagation)하거나 > launch, actor
- 사용자에게 노출(exposing)시키는 것 > async, produce
두 가지 맛이 있습니다. (=두 가지 타입으로 나뉜다 정도로 해석 가능합니다.)다른 코루틴의 자식 코루틴이 아닌 코루틴을 만들 때, 이전(propagation 되는 코루틴) 빌더들은 예외가 잡히지 않고 처리하며, 자바의 Thread.uncaughtExceptionHandler 와 유사하게 최종 예외처리를 유저가 처리될 수 있도록 하기도 합니다. 이와 관련된 대표적인 예로, await 또는 receive 가 해당됩니다. (produce, receive 는 Channels 섹션에서 다룹니다.)
위와 관련된 내용은 GlobalScope 를 사용하여 루트 코루틴을 생성하는 간단한 예제로 증명이 가능합니다.
Throwing exception from launch
Exception in thread "DefaultDispatcher-worker-1 @coroutine#2" java.lang.IndexOutOfBoundsException
Joined failed job
Throwing exception from async
Caught ArithmeticException
CoroutineExceptionHandler
예외 핸들러를 커스터마이징함으로써 콘솔에서 표시되지 않은, 잡히지 않은 예외 출력에 대하여 처리 가능합니다. CoroutineExceptionHandler 라는 루트 코루틴의 컨텍스트 요소는 루트 코루틴과 전체 자식 코루틴에서 예외가 발생될 때 catch 와 같이 범용적인 코드블록 사용이 가능합니다. 이것은 Thread.uncaughtExceptionHandler 와 유사합니다. 코루틴은 예외 처리 핸들러가 호출될 때, 이미 종료 되었다는 것을 알 수 있습니다. 일반적으로 핸들러는 로그를 출력하거나, 몇 가지 에러를 표시하거나, 어플리케이션을 종료하거나 다시 시작하기 위하여 사용합니다.
JVM 에서는 CoroutineExceptionHandler를 글로벌하게 예외 처리한 것을 재정의하여 ServiceLoader 를 통하여 등록이 가능합니다. 이는 특별한 예외 처리를 수행하는 핸들러가 등록되어있지 않을 경우 수행되는 Thread.defaultUncaughExceptionHandler 와 유사합니다. 안드로이드에서는 기본적으로 코루틴 글로벌 예외처리 핸들러로 ‘unCaughtExceptionPreHandler’ 가 내장되어있습니다.
CoroutineExceptionHandler 는 일반적으로 잡히지 않는(=검색되지 않는) - 별도 처리되지 않은 예외상황에서 호출됩니다. 특히, 모든 자식 코루틴(다른 context에서 생성된 Job)은 그 부모 코루틴에게 예외 처리하는 것을 위임하며, 그 부모 코루틴 역시 부모 코루틴에게 위임하며, 결국 최상위 코루틴까지 위임됩니다. 따라서, context 에 설정된 CoroutineExceptionHandler 는 절대 사용되지 않습니다.
감시되는 범위에서 실행되는 코루틴은 부모에게 예외를 전파하지 않으며 이 규칙에서 제외됩니다.
Suervision 섹션에서 좀 더 자세한 내용을 다룹니다.
이 예제코드는 다음과 같이 출력됩니다.
CoroutineExceptionHandler got java.lang.AssertionError
Cancellation and exceptions
취소 동작은 예외(exception) 발생과 매우 밀접한 연관이 있습니다.
코루틴은 내부적으로 취소에 대하여 CancellationException 이 발생시키며
이러한 예외는 모든 핸들러에 의해 무시되므로, catch 코드블록으로 확인하게되는 예외처리는 추가적으로 디버깅 목적으로만 사용해야 합니다. Job.cancel을 이용하여 코루틴이 취소될 때, 해당 코루틴은 종료되지만, 그 부모코루틴을 취소하지는 않습니다.
Cancelling child
Child is cancelled
Parent is not cancelled
만약 코루틴이 CancellationException 과 다른 예외상황이 발생된다면,
해당 예외상황과 함께 부모 역시 취소됩니다. 이러한 동작은 재정의 될 수 없으며(=무시될 수 없으며), 코루틴 계증간 구조화된 동시성을 안정적으로 제공합니다. CoroutineExceptionHandler 구현체는 하위 코루틴에는 이용되지 않습니다.
예제의 CoroutineExceptionHandler 는 항상 GlobalScope 에서 생성된
코루틴에 설정되어 있습니다. 메인 코루틴은 설치된 예외 핸들러에도 불구하고 예외적으로 자식 코루틴이 예외에 의하여 종료되면 항상 취소되기 때문에, 주요 작업 실행사항을 코루틴 범위에 CoroutineExceptionHandler 를 설정하는 것은 타당하지 않습니다.
근본적으로 발생된 예외사항은 모든 자식들이 종료되고 나서 그 부모만이 처리하며, 이러한 사항은 다음 예제에서 확인할 수 있습니다.
이 예제는 다음과 같이 출력됩니다.
Second child throws an exception
Children are cancelled, but exception is not handled until all children terminate
The first child finished its non cancellable block
CoroutineExceptionHandler got java.lang.ArithmeticException
Exceptions aggregation
여러 자식 코루틴에서 예외로 실패되는 상황이라면, "첫 번째 예외가 이긴다."는 일반 규칙에 따라 첫 번째 예외를 (CoroutineExceptionHanlder으로) 처리되어지게 됩니다. 첫 예외 발생 후 추가적으로 발생되는 모든 예외는 첫 번째의 예외의 supressed 중 하나로 추가되게 됩니다.
CoroutineExceptionHandler got java.io.IOException with suppressed [java.lang.ArithmeticException]이러한 케머니즘으로 동작하는 것은 오직 Java 1.7 이상 버전에서만 동작됩니다.
JS 와 네이티브에서는 제한적인 상황이지만 앞으로 해결될 예정입니다.
취소 예외의 경우 기본적으로 이러한 메커니즘에 포함되지 않으며, 예외처리 집계에 대하여 투명합니다.
이 예제는 다음과 같이 출력됩니다.
Rethrowing CancellationException with original cause
CoroutineExceptionHandler got java.io.IOException
catch { } 코드블록에서 CancellationException 을 throw 하지 않고
다른 예외를 발생켰을 때 CoroutineExceptionHandler 에서 전달받는 exception.suppressed 를 확인해보세요.
Supervision
앞에서 살펴본 것 처럼 취소는 코루틴 전체 계층구조를 통해 전파되는 양방향 관계입니다. 이제부터는 단방향으로 취소가 필요한 경우를 살펴보겠습니다.
특정 스코프 내에서 동작하는 작업을 가진 UI 컴포넌트는 이러한 필요성을 가진 좋은 예시입니다. UI의 하위 작업중 하나라도 실패하면, UI 구성 요소 전체를 취소 (효과적으로 제거)하는 것은 아니지만, UI 구성요소가 소멸(=destroyed)되어 그 작업(=UI 종속적안 작업)이 취소되면, 더 이상 결과가 필요하지 않으므로 모든 하위 작업을 실패해야 합니다.
또다른 예로 여러개의 하위 작업들을 발생시키고 이들의 작업을 감독해야하는 서버 프로세스로, 이들의 실패를 추적하여 실패한 하위 작업만 재시작해야 하는 경우입니다.
Supervision Job
SupervisorJob 은 이러한 목적에 맞춰 사용할 수 있습니다. 이는 취소가 아래쪽으로만 전파되는 일반 Job과 유사합니다. 이것은 손쉽게 아래 예제 코드로 확인할 수 있습니다.
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 을 이용하여 스코프에서 동시성 범위를 지정할 수 있습니다. 이는 취소를 한 방향으로만 전파하고, 스스로 실패한 경우에만 모든 하위 코루틴을 취소시킵니다. 또한 coroutineScope 처럼 모든 하위 코루틴들이 종료되는 것을 기다립니다.
The child is sleeping
Throwing an exception from the scope
The child is cancelled
Caught an assertion error
Exceptions in supervised coroutines
감독되는(supervision) 코루틴과 일반(regular) 코루틴에서 또 다른 중요한 차이점은 예외처리입니다. 모든 자식 코루틴은 예외에 대하여 스스로 처리해야 합니다. 이러한 차이점은 자식 코루틴에서 예외가 부모 코루틴으로 전파되지 않는 사실에서 비롯됩니다. 이것은 supervisorScope 안에서 직접 실행된 코루틴은 루트 코루틴 스코프에서 설정한 CoroutineExceptionHandler 를 사용하게 된다는 의미입니다. (CoroutineExceptionHandler 설명을 참조하세요.)
The child throws an exception
CoroutineExceptionHandler got java.lang.AssertionError
The scope is completing
The scope is completed
이 예제에서 supervisorScope 을 coroutineScope 으로 변경하고, child 를 “The scope is completed” 전에 join() 후 실행 결과를 확인 해보세요.