Kotlin/Coroutines / / 2023. 5. 3. 00:55

[Kotlin Coroutine] Flow 공식 문서 번역 및 정리 (4)


  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다. 👍
  • Kotlin Coroutine 공식 문서 내의 Asynchronous Flow 페이지를 공부하며 이해하기 쉽도록 한글 문맥으로 더 자연스럽게 번역 및 정리한 글입니다.

 

Flow Exceptions

 

Flow 수집은 발행자(Emitter) 또는 연산자 내부의 코드에서 Exception이 throw 될 때, Exception 발생과 함께 완료됩니다. 이러한 Exception을 처리하는 방법에는 여러 가지가 있습니다.

 

Collector try and catch

 

콜렉터는 try/catch 블록을 사용하여 exception을 처리할 수 있습니다.

 

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i) // emit next value
    }
}

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value ->         
            println(value)
            check(value <= 1) { "Collected $value" }
        }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}

 

실행 결과 collect 연산자에서 성공적으로 exception을 catch 하여 처리하고, 그 이후에는 더 이상 플로우에서 값이 발행(emit)되지 않음을 확인할 수 있습니다.

Emitting 1
1
Emitting 2
2
Caught java.lang.IllegalStateException: Collected 2

 

 

Everything is caught

 

이전 예제에서는 발행자(Emitter) 또는 intermediate operator, terminal operatior 내부에서 발생하는 모든 예외를 캐치했습니다. 다음 예제는 발행된 값을 String으로 맵핑하는데, 이 과정에서 코드가 예외를 발생시킵니다.

 

fun simple(): Flow<String> = 
    flow {
        for (i in 1..3) {
            println("Emitting $i")
            emit(i) // emit next value
        }
    }
    .map { value ->
        check(value <= 1) { "Crashed on $value" }                 
        "string $value"
    }

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value -> println(value) }
    } catch (e: Throwable) {
        println("Caught $e")
    } 
}

 

실행 결과 마찬가지로 예외가 캐치되고 수집이 중단됩니다.

Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: Crashed on 2

 


 

Exception transparency (예외 투명성)

 

하지만 발행자(Emitter) 코드가 예외 처리 동작을 캡슐화하는 방법은 무엇일까요?
Flow는 예외에 대해 투명해야 합니다. try/catch 블록 내부 flow { ... } 빌더에서 값을 발행하는 것은 예외 투명성 위반이 됩니다. 이는 이전 예제에서와 같이 콜렉터가 예외를 throw 할 때 항상 try/catch로 catch 할 수 있도록 보장합니다.

발행자(Emitter)는 예외 처리를 캡슐화하는 방법으로 catch 연산자를 사용할 수 있습니다. catch 연산자의 본문은 예외를 분석하고 캐치된 예외가 무엇이냐에 따라 다른 방식으로 대응할 수 있습니다. 이렇게 하면 예외 투명성을 유지하면서 예외 처리를 캡슐화할 수 있습니다.

 

  • throw를 통하여 Exception은 재발생(rethrow)될 수 있습니다.
  • catch 블록에서 emit을 사용하여 값 발행으로 변환할 수 있습니다.
  • Exception을 무시하거나, 로깅하거나, 다른 코드에서 처리할 수 있습니다.

예를 들어, 다음과 같이 Exception을 catch 했을 때 발행할 텍스트를 설정할 수 있습니다.

 

simple()
    .catch { e -> emit("Caught $e") } // emit on exception
    .collect { value -> println(value) }

 

예시 코드의 출력 결과는 try/catch 블록이 더 이상 존재하지 않아도 이전과 동일합니다.

Emitting 1
string 1
Emitting 2
Caught java.lang.IllegalStateException: Crashed on 2

 

 

Transparent catch

 

catch 중간 연산자는 예외 투명성을 따르며, 업스트림 예외만 포착합니다. 즉, catch 위의 모든 연산자에서 발생한 예외는 포착할 수 있지만, 그 아래에서는 포착할 수 없습니다. 따라서 catch 아래에 있는 collect { ... } 내부 블록에서 예외가 발생하면 catch 연산자에 의해 포착되지 않습니다.

 

fun simple(): Flow<Int> = flow {
    for (i in 1..3) {
        println("Emitting $i")
        emit(i)
    }
}

fun main() = runBlocking<Unit> {
    simple()
        .catch { e -> println("Caught $e") } // does not catch downstream exceptions
        .collect { value ->
            check(value <= 1) { "Collected $value" }                 
            println(value) 
        }
}

실행 결과 catch 연산자가 있음에도 불구하고 "Caught..." 메시지가 인쇄되지 않으며, Exception이 발생합니다.

Emitting 1
1
Emitting 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2 at ...

 

 

Catching declaratively (선언적 캐칭)

 

예외를 처리하는 catch 연산자의 선언적 성격과 모든 예외를 처리하는 방법을 결합할 수 있습니다. collect 연산자의 본문을 onEach로 이동시키고 catch 연산자 앞에 놓습니다. 이렇게 하면 flow의 수집을 매개변수 없이 collect를 호출하여 실행할 수 있습니다.

simple()
    .onEach { value ->
        check(value <= 1) { "Collected $value" }                 
        println(value) 
    }
    .catch { e -> println("Caught $e") }
    .collect()

 

실행 결과 "Caught..." 메시지가 출력되므로 try/catch 블록을 명시적으로 사용하지 않고도 모든 예외를 catch 한 것을 볼 수 있습니다.

Emitting 1
1
Emitting 2
Caught java.lang.IllegalStateException: Collected 2

 


[Kotlin Coroutine] Flow 공식 문서 번역 및 정리 (5) - 마지막 편 에서 계속됩니다.


 

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유