코틀린 Flow 공식문서 읽기 스터디 4.5차

Mendel·2023년 11월 20일
0

이번 범위

Flow exceptions

emit하는 과정 혹은 연산자(중간 연산자, 종료 연산자 등) 안에서 예외를 던질 때, Flow 수집이 종료될 수 있다.

collector try and catch

수집기(collector)는 예외를 다루기 위해 코틀린의 try/catch 블록을 이용할 수 있다.

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")
    } 
}            

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

collect라는 종료 연산자에서 발생한 예외를 잘 잡아서 처리하고, 수집이 완전히 종료된 것을 알 수 있다.

Everything is caught

emitter(=방출로직)나 중간 연산자 혹은 종료 연산자 내에서 발생하는 모든 예외를 포착한다고 위에서 언급했다.
그리고 이 예외를 포착하면서 수집은 멈추게 된다고 했다. 이 내용이 진짜인지 테스트하는 코드다. 아래는 중간 연산자에서 예외를 발생시키고 수집이 종료되는 모습을 확인할 수 있다.

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(=방출로직) 코드의 이런 예외 처리 행동이 담긴 코드를 어떻게 캡슐화할 수 있는가? => catch 중간 연산자 사용하기

플로우는 예외에 대해 투명해야 한다. 여기서 투명하다는 말은 flow빌더 블록 안에서 try/catch로 예외를 잡아놓고서 밖에서는 아무일도 없던 척을 하지 말라는 것임. 투명하게 안에서 예외가 발생했다는 것을 외부에도 공개하라는 의미다.

이런 방출로직에서 예외 투명성을 지키기 위해, catch라는 중간 연산자가 나왔다. 즉, flow빌더 블록 안에서 try/catch하지 말고 그 포착하는 로직을 중간 연산자로 빼내라는 것임. 이로써, 방출로직의 예외 처리 로직의 캡슐화를 할 수 있게 된다.

catch연산자 본문 블록에서는 예외를 인자로 받는다. 이 안에서 예외를 분서갛고 예외 타입에 따라 분기처리를 할 수도 있다. 아래는 대표적인 catch를 활용하는 사용예시들이다.

  • throw 연산자를 통해 예외 다시 던지기
  • catch 블록에서 emit을 사용해서 값을 방출하기
  • 예외를 무시하거나 로그로 기록을 남기거나 등등에 사용하기

사용 예시들 중 두 번째가 무슨 말인지 와닿지 않을 수 있다. 아래 예시 코드를 봐보자.

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"
    }

...

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

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

수집기 쪽에 try/catch가 없는데도 불구하고 예외 처리가 잘 된 것을 알 수 있다. 즉, catch로 emit을 하면, 해당 방출까지만 catch를 기준으로 한 다운 스트림에 전파하고, 수집하고 마무리(=수집을 멈춘다)한다.

만약 위의 예제로 아직 catch의 emit이 와닿지 않는다면 아래 예제를보자.

    flow {
        for (i in 1..3) {
            println("Emitting $i")
            emit(i) // emit next value
        }
    }
    .map { value ->
        check(value <= 1) { "Crashed on $value" }                 
        println("string $value")
        value
    }
    .catch { e -> emit(5) }
    .collect{
        println("collect $it")
    }

Emitting 1
string 1
collect 1
Emitting 2
collect 5

catch에서 5를 방출하고 그게 collect에게 수집까지되고 이후로는 수집을 멈춘 것을 알 수 있다.

Catching declaratively (선언적으로 포착하기)

catch연산자는 모든 예외를 처리하려는 욕구를 갖고 있기 때문에 플로우 흐름 구조 상에서 맨 마지막으로 등장하는 것이 사실 가독성 측면에서 가장 적절하다. 이런 가독성을 챙기고 싶다면, onEach를 활용할 수 있다.
기존의 collect블록에 있던 코드를 onEach블록 안에 옮기고, onEach 중간 연산자를 catch연산자 바로 앞에 놓는 방식. 그러면, catch연산자 안에는 정말 예외처리와 관련된 정제된 코드만 남게 되므로 각 중간 연산자 별 역할 분리도 잘 되고 코드 가독성이 좋아진다는 특징을 살릴 수 있다.

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

참고
코틀린 플로우 예외 처리 공식문서
코틀린 공식문서 읽기 - 미디엄

profile
이것저것(안드로이드, 백엔드, AI, 인프라 등) 공부합니다

0개의 댓글