Jwhyee / kotlin-coroutine-study

코틀린 코루틴 북 스터디 📚
4 stars 1 forks source link

4부 코틀린 코루틴 적용하기 - 1 #9

Open lee-ji-hoon opened 2 months ago

lee-ji-hoon commented 2 months ago

26, 27장

lee-ji-hoon commented 2 months ago

책을 읽다가 coroutineScope 를 사용하면 자식 코루틴에서 발생한 예외는 coroutineScope가 생성한 코루틴을 중단하게 되어 모든 자식 코루틴을 취소한 뒤 예외를 던지게 만든다.

위 내용이 있어서 2주 전에 디스코드에 올렸던 의문점이 있는 코드에서 뭔가 정답을 찾을 수 있을거 같아 다시 디버깅 해보았다.

suspend fun getData(): Int {
    delay(1.seconds)
    throw Exception("tawet")
    return 10
}

suspend fun main() = coroutineScope {
    val result = runCatching {
        val test = async { getData() }
        test.await()
    }

    result.onSuccess {
        println("onSuccess: $it")
    }.onFailure { e ->
        println("onFailure: $e")
    }

    delay(3.seconds)
    println("Process completed")
}

실무에서 이런 비슷한 코드가 viewModelScope + runCatching인데 터지는 문제가 생겼다.

  1. viewModelScope 에서 withContext 가 아닌 async 로 열었으니 SupervisorJob 은 자식이 못받는 것은 이전에 봤었다.
  2. 그렇기에 exception 이 부모로 전파가 되지만 runCatching 이니까 onFailure 에서 잡혀야 한다.

이렇게 생각의 흐름이 흘렀는데 실제로 2번 까지는 맞았는데 하나가 더 있었다.

  1. onFailure 부분이 동작을 하고 Exception이 또 터져서 앱이 죽어버렸다.

그래서 이거에 대해서 조금 더 살펴보고자 한다.

image

우선 이런형태로 Exception이 나고 Process 자체가 죽어버리는데, 내가 지금 의심이 되는건 Exception이 전파가 되고 async 블럭 안에서 Exception이 또 터져서 비동기적으로 Exception이 나면서 runCatching이 동작을 안하는게 아닐까? 라는 생각이 들었다.

suspend fun getData(): Int {
    delay(5.seconds)
    throw Exception("tawet")
    return 10
}

suspend fun main() = coroutineScope {
    val result = runCatching {
        launch { getData() }
    }

    result.onSuccess {
        println("onSuccess: $it")
    }.onFailure { e ->
        println("onFailure: $e")
    }

    delay(3.seconds)
    println("Process completed")
}

그래서 이런 형태로 바꾸고 테스트를 해보았다.

  1. async > launch
  2. getDatadelay"Process completed" 보다 더 늦게 끝나게 delay를 5초로 변경

이렇게 하자 신기한 결과가 나왔다.

image

이런 결과가 나와서 그러면 이제는 runCatching 보다 늦게 끝나서 그런건가 하고 getData()delay 를 없애고 실행하자 맨 처음 테스트 처럼 그냥 프로세스 자체가 죽어버렸다.

Scope의 Exception은 전파가 된다.

지금 보면 coroutineScope의 자식으로 launch 혹은 async 가 있는 형태인데 exception은 전파가 되기 때문에 coroutineScope의 block의 exception이 coroutineScope 자체까지 전파가 되기 때문이 아닐까? 라는 생각하고 밖에 try catch를 해보았다.

suspend fun main() {
    try {
        coroutineScope {
            val result = runCatching {
                launch { getData() }
            }

            delay(1.seconds)

            result.onSuccess {
                println("onSuccess: $it")
            }.onFailure { e ->
                println("onFailure: ${e.stackTrace.iterator().forEachRemaining { println(it)}}")
            }

            delay(3.seconds)
            println("Process completed")
        }
    } catch (e: Exception) {
        println("======================================")
        println("catch e > ${e.stackTrace.iterator().forEachRemaining { println(it) }}")
    }
}
image

그러면 이런 결과를 받을 수 있는데 catch는 되고 프로세스 자체가 죽지는 않는 내가 원하는 방향으로 잡혔다. 그래서 supervisorJob이 아니거나 supervisorScope으로 열지 않았다면 최상위 부모까지 error가 전파가 되고 부모 Coroutine 자체에서 Exception이 터지기 때문에 내부 block에 runCatching을 잡아도 의미가 없다.