06. 코루틴 방탈출 3번 문제 풀이

문제

3번 문제는 코루틴에서 에러가 발생했을 때 에러 전파에 관한 문제이다.

// file: "문제 3번.kt"

package roomescape

import kotlinx.coroutines.async
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.test.runTest
import org.junit.jupiter.api.Test
import roomescape.assertion.assertHashcode

class Step3 {

    @Test
    fun `코루틴 예외 전파`() = runTest {
        // given
        val actual: StringBuilder = StringBuilder()

        // when
        val job = launch {
            try {
                launch {
                    delay(150)
                    actual.append(1)
                }
                supervisorScope {
                    val deferred = async {
                        delay(100)
                        throw RuntimeException("E2")
                    }
                    launch {
                        delay(200)
                        throw RuntimeException("E3")
                    }
                    deferred.await()
                }
            } catch (e: Exception) {
                actual.append(e.message)
            }
        }
        job.join()

        // then
        val expected = "" // TODO: 결과값 예상
        /*
            TODO: 간단한 풀이과정 작성
         */

        // assert문 수정하지 마세요!
        assertHashcode(actual, expected)
    }
}

풀이

간단하게 예외 전파에 대해서만 알면 되는데,
일반적으로 자식 코루틴에서 예외가 발생할 경우, 이 예외가 처리되지 않는다면 부모에게 올라가 똑같이 에러가 전파된다.
부모 또한 에러를 전파 받으면 취소되게 되고, 부모 내부에 존재하던 다른 코루틴 또한 취소되게 된다.

다만 본 문제에서 supervisorScope가 사용되었는데, supervisorScope는 자식 코루틴에서 에러가 부모 코루틴으로 전파되지 않는다.

다만 자식 코루틴이 아니라 그냥 supervisorScope 내부에서 일어난 에러에 대해서는 전파가 된다.

이 점을 고려하여 코드를 바라본다면,

runTest를 제외하고, 위에서부터 차례대로

  1. root가 되는 부모 코루틴

  2. launch 코루틴

  3. supervisorScope 코루틴

    1. async를 이용한 에러가 발생하는 코루틴
    2. launch를 이용한 에러가 발생하는 코루틴

이 있다.

조금 더 구체적으로는

  1. job 내의 launch에 해당하는 코루틴이 실행되자마자 150ms 동안 점유를 내준다.

  2. supervisorScope에서 async로 생성한 코루틴이 실행되자마자 100ms 동안 점유를 내준다.

  3. supervisorScope에서 launch로 생성한 코루틴이 실행되자마자 200ms 동안 점유를 내준다.

  4. 100ms가 지나고 async 코루틴에서 E2 에러를 발생시킨다.

  5. 150ms가 지나고 supervisorScope 바깥 launch 코루틴에서 actual에 1을 더한다.

  6. 200ms가 지나고 supervisorScope 내부 launch 코루틴에서 E3 에러를 발생시킨다.

이렇게 정리했을 때 E21E3라고 생각한다면 코드를 유심히 봐야 한다.

supervisorScope의 경우 자식 코루틴의 에러는 전파하지 않지만, scope 내부 자식 코루틴이 아닌 부분에서 발생하는 에러는 전파 및 취소가 된다.

launch 내부에서 발생하는 에러는 자식 코루틴에서 발생하는 에러로 보지만, async에서 에러가 발생했을 때 이것이 터지는 시점은 await으로 값을 수신할 때이다.

즉 deffered.await() 부분에서 에러가 터지게 되는데, 해당 부분은 async 내부의 자식 코루틴이 아니라, supervisorScope 내부에 해당한다.

그리고 supervisorScope 내부의 launch는 내부에서 에러가 발생하므로, 자식 코루틴에서 발생한 것으로 되어 supervisorScope가 에러를 전파하지 않는다.

또한 launch 내부에서 try-catch 등을 통한 에러 핸들링을 해주지 않았으므로 그냥 exception이 터지겠지만,
deffered.await()이 launch에서 에러가 터지는 것보다 더 빠르게 수신되고,

supervisorScope는 에러 전파가 되어 취소 및 자식 코루틴들도 취소 시키면서 에러를 위로 던져버리기 때문에
본 코드에서는 launch에 해당하는 부분의 에러가 throw 될 일이 없다.

정리하면

  1. supervisorScope 내부의 async는 await 부분이 supervisorScope 내부 부분이라 async 내부의 Error가 전파된다.
  2. supervisorScope 내부의 launch는 에러가 자식 코루틴 내부에서 터지므로 전파되지 않아 job의 try-catch로 전달되지 않는다.
  3. 애초에 await을 통한 에러 수신으로 인해 supervisorScope도 에러가 터져 자식들이 취소되어 launch의 에러가 터질 일이 없다.

그렇기 때문에 E21이 정답이 된다.


© 2025. Na2te All rights reserved.