처리되지 않은 예외를 코루틴 계층 위쪽으로 전파하고 형제 코루틴을 취소하는 것은 애플리케이션에서 구조적 동시성 패러다임을 강제하는 데 도움이 된다.
🔖 18.2.3 슈퍼바이저는 부모와 형제가 취소되지 않게 한다
fun main(): Unit = runBlocking {
supervisorScope {
launch {
try {
while (true) {
println("Heartbeat!")
delay(500.milliseconds)
}
} catch (e: Exception) {
println("Heartbeat terminated: $e")
throw e
}
}
launch {
delay(1.seconds)
throw UnsupportedOperationException("Ow!")
}
}
}
자식 코루틴이 부모 코루틴을 취소하지 못하게 슈퍼바이저가 막는다.
슈퍼바이저는 자식이 실패하더라도 생존
슈퍼바이저는 애플리케이션에서 코루틴 계층의 위쪽에 위치하는 경우가 많다.
📖 18.3 CoroutineExceptionHandler: 예외 처리를 위한 마지막 수단
처리되지 않는 예외는 CoroutineExceptionHandler라는 특별한 핸들러에게 전달된다.
CoroutineExceptionHandler를 코루틴 콘텍스트에 제공하면 처리되지 않은 예외를 처리하는 동작을 커스텀화할 수 있다.
fun main(): Unit = runBlocking {
val supervisor = ComponentWithScope()
supervisor.action()
delay(1.seconds)
}
class ComponentWithScope(dispatcher: CoroutineDispatcher = Dispatchers.Default) {
private val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("[ERROR] ${e.message}")
}
private val scope = CoroutineScope(
SupervisorJob() + dispatcher + exceptionHandler
)
fun action() = scope.launch {
throw UnsupportedOperationException("Ouch!")
}
}
커스텀 코루틴 예외 핸들러를 가진 컴포넌트
private val topLevelHandler = CoroutineExceptionHandler { _, e ->
println("[TOP] ${e.message}")
}
private val intermediateHandler = CoroutineExceptionHandler { _, e ->
println("[INTERMEDIATE] ${e.message}")
}
@OptIn(DelicateCoroutinesApi::class)
fun main() {
GlobalScope.launch(topLevelHandler) {
launch(intermediateHandler) {
throw UnsupportedOperationException("Ouch!")
}
}
Thread.sleep(1000)
}
코루틴 계층의 최상위에 있는 예외 핸들러만 호출된다.
🔖 18.3.1 CoroutineExceptionHandler를 launch와 async에 적용할 때의 차이점
class ComponentWithScope(dispatcher: CoroutineDispatcher = Dispatchers.Default) {
private val exceptionHandler = CoroutineExceptionHandler { _, e ->
println("[ERROR] ${e.message}")
}
private val scope = CoroutineScope(
SupervisorJob() + dispatcher + exceptionHandler
)
fun action() = scope.launch {
async {
throw UnsupportedOperationException("Ouch!")
}
}
}
fun main() {
val supervisor = ComponentWithScope()
supervisor.action()
delay(1.seconds)
}
슈퍼바이저의 직접적인 자식이면서 예외를 던지는 async 코루틴
최상위 코루틴이 async로 시작되면 이 예외를 처리하는 책임은 await()를 호출하는 Deferred 소비자에게 있다.
📖 18.4 플로우에서 예외 처리
class UnhappyFlowException : Exception()
val exceptionalFlow = flow {
repeat(5) { number ->
emit(number)
}
throw UnhappyFlowException()
}
5개의 숫자를 방출한 후 예외를 던지는 플로우
일반적으로 플로우의 일부분에서 예외가 발생하면 collect에서 예외가 던져진다.
🔖 18.4.1 catch 연산자로 업스트림 예외 처리
fun main() = runBlocking {
exceptionalFlow
.catch { cause ->
println("\nHandled: $cause")
emit(-1)
}
.collect {
println("$it ")
}
}
catch는 플로우에서 발생한 예외를 처리할 수 있는 중간 연산자
이 함수에 연결된 람다 안에서 플로우에 발생한 예외에 접근할 수 있다.
fun main() = runBlocking {
exceptionalFlow
.map {
it + 1
}
.catch { cause ->
println("\nHandled: $cause")
}
.onEach {
throw UnhappyFlowException()
}
.collect()
}
catch는 업스트림에 대해서만 작동하며, 플로우 처리 파이프라인의 앞쪽에서 발생한 예외들만 잡아낸다.
🔖 18.4.2 술어가 참일 때 플로우의 수집 재시도: retry 연산자
val unreliableFlow = flow {
println("Starting the flow!")
repeat(10) { number ->
if (Random.nextDouble() < 0.1) throw CommunicationException()
emit(number)
}
}
fun main() = runBlocking {
unreliableFlow
.retry(5) { cause ->
println("\nHandled: $cause")
cause is CommunicationException
}
.collect { number ->
println("$number")
}
}
retry 연산자는 업스트림의 예외를 잡는다.
재시도할 때는 업스트림 연산자가 보두 다시 실행된다.
📖 18.5 코루틴과 플로우 테스트
🔖 18.5.1 코루틴을 사용하는 테스트를 빠르게 만들기: 가상 시간과 테스트 디스패처
class PlaygroundTest {
@Test
fun testDelay() = runTest {
val startTime = System.currentTimeMillis()
delay(20.seconds)
println(System.currentTimeMillis() - startTime)
}
}