BottleH Blog

Kotlin in Action - 18장 오류 처리와 테스트

    Tags

  • Kotlin
Kotlin in Action - 18장 오류 처리와 테스트 thumbnail

📖 18.1 코루틴 내부에서 던져진 오류 처리

  • 일시 중단 함수나 코루틴 빌더 안에 작성한 코드도 예외를 발생시킬 수 있다.
fun main(): Unit = runBlocking { try { launch { throw UnsupportedOperationException("Ouch!") } } catch (u: UnsupportedOperationException) { println("Handled $u") } }
  • 코루틴 빌더는 실행할 새로운 코루틴을 생성하는데, 이 새로운 코루틴에서 발생한 예외는 catch 블록에 의해 잡히지 않는다
  • 위 코드는 예외가 잡히지 않는다.
fun main(): Unit = runBlocking { launch { try { throw UnsupportedOperationException("Ouch!") } catch (u: UnsupportedOperationException) { println("Handled $u") } } }
  • launch에 전달되는 람다 블록안에서는 잡힌다.
fun main(): Unit = runBlocking { val myDeferredInt: Deferred<Int> = async { throw UnsupportedOperationException("Ouch!") } try { val i: Int = myDeferredInt.await() println(i) } catch (u: UnsupportedOperationException) { println("Handled: $u") } }
  • await를 감싼 try-catch에서 예외를 잡지만 동시에 예외를 출력한다.
  • 자식 코루틴은 잡히지 않은 예외를 항상 부모 코루틴에 전파한다.

📖 18.2 코틀린 코루틴에서의 오류 전파

  • 한 자식의 실패가 부모의 실패로 이어진다.
  • 자식의 실패로 인해 시스템 전체가 실패하면서 멈추지는 말아야 하는 경우를, 자식이 부모의 실행을 감독한다고 말한다.

🔖 18.2.1 자식이 실패하면 모든 자식을 취소하는 코루틴

fun main(): Unit = runBlocking { 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!") } } Heartbeat! Heartbeat! Heartbeat terminated: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job="coroutine#1":BlockingCoroutine{Cancelling}@130f889 Exception in thread "main" java.lang.UnsupportedOperationException: Ow!
  • 형제 코루틴 중 하나가 예외를 던지면 하트비트 코루틴도 취소된다.
  • 같은 스코프 안에서 동시성 계산을 함께 수행하고 공통의 결과를 반환하는 코루틴 그룹에게 아주 유용하다.

🔖 18.2.2 구조적 동시성은 코루틴 스코프를 넘는 예외에만 영향을 미친다

fun main(): Unit = runBlocking { launch { try { while (true) { println("Heartbeat!") delay(500.milliseconds) } } catch (e: Exception) { println("Heartbeat terminated: $e") throw e } } launch { try { delay(1.seconds) throw UnsupportedOperationException("Ow!") } catch (u: UnsupportedOperationException) { println("Caught $u") } } }
  • 예외가 발생한 다음에도 하트비트 코루틴이 계속 텍스트를 출력
  • 처리되지 않은 예외를 코루틴 계층 위쪽으로 전파하고 형제 코루틴을 취소하는 것은 애플리케이션에서 구조적 동시성 패러다임을 강제하는 데 도움이 된다.

🔖 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) } }
  • runTest는 속도를 높이기 위해 특별한 테스트 디스패처와 스케줄러를 사용
  • runTest의 디스패처는 단일 스레드
@OptIn(ExperimentalCoroutinesApi::class) @Test fun testDelay() = runTest { var x = 0 launch { delay(500.milliseconds) x++ } launch { delay(1.seconds) x++ } println(currentTime) delay(600.milliseconds) assertEquals(1, x) println(currentTime) delay(500.milliseconds) assertEquals(2, x) println(currentTime) }
  • delay를 통해 가상 시계 진행
@OptIn(ExperimentalCoroutinesApi::class) @Test fun testDelay() = runTest { var x = 0 launch { x++ launch { x++ } } launch { delay(200.milliseconds) x++ } runCurrent() assertEquals(2, x) advanceUntilIdle() assertEquals(3, x) }
  • 미래의 어느 시점에 실행하도록 예약된 코루틴까지 실행하려면 advanceUntilIdle 함수를 사용할 수 있다.

🔖 18.5.2 터빈으로 플로우 테스트

val myFlow = flow { emit(1) emit(2) emit(3) } @Test fun doTest() = runTest { val results = myFlow.toList() assertEquals(3, results.size) }
  • 플로우를 리스트로 수집해서 확인할 수 있다.
@Test fun doTest() = runTest { val results = myFlow.test { assertEquals(1, awaitItem()) assertEquals(2, awaitItem()) assertEquals(3, awaitItem()) awaitComplete() } }
  • 터빈은 서드파티 라이브러리지만 필수적이다.
  • 플로우가 방출한 모든 원소가 테스트에 의해 적절히 소비되도록 보장
Written by@BottleH
Back-End Developer

GitHub