IT/Kotlin

Coroutine Flow - 3부

물통꿀꿀이 2020. 11. 21. 23:57

세번째 시간을 가져보자.

 

Exception

떨어질래야 떨어질 수 없는 예외처리 부분이다.

Flow는 내부 코드 상에서 (+ operator) 예외가 발생하면 예외와 함께 종료된다.

 

그럼 일반적으로 통용되는 방법으로 예외를 핸들링해보면 아래와 같이 코드를 작성 할 수 있다.

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

실제로 위의 코드를 실행해보면, "2"에서 에러가 발생하고 끝이난다. 그런데 flow는 예외에 있어서 투명(transparant) 해야하는데 해당 코드는 예외 투명성(exception transparency)을 위반한다. 

 

현 시점에서 예외 투명성이라는 부분이 잘 이해가 가지 않는다. 추측해보면, try/catch 구문을 항상 사용하면서 예외를 핸들링해야 하기 때문에 내부 예외가 다 드러나서 즉, encapsulation이 되지 않기 때문에 flow의 원칙을 어긋나는 것으로 이해가 되지만, 정확히는 와닿지 않는다. (만약 아시는 분은 코멘트 부탁드립니다..) 

보다 정확한 내용 전달을 위해 실제 원문을 아래 첨부하겠다.

더보기

Flows must be transparent to exceptions and it is a violation of the exception transparency to emit values in the flow { ... } builder from inside of a try/catch block. This guarantees that a collector throwing an exception can always catch it using try/catch as in the previous example.

catch

이에 flow 내부적으로 투명성있게 처리하기 위한 operator인 catch가 등장했다.

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

간단히 위의 코드처럼 작성하면 된다.

그러나!! catch operator의 문제는 simple에서 발생하는 upstream만 핸들링한다는 것이다.

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

이렇게 collect에서 예외가 발생하면... catch가 있으나마나이다.

여기서 생각할 수 있는 우회로는 collect에서 작업을 하지 않고 catch에서 작업을 먼저하고 넘기는 것이다. 아래처럼

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

Completion

Completion이라는 의미는 2가지이다. 정상적인 종료와 전에 살펴봤던 예외까지.

이렇게 flow가 종료될 때, flow에 방점(?)을 찍는 2가지 imperative와 declarative 방식이 있다.

 

먼저 imperative 방식으로 finally를 넣는 것이다.

fun simple(): Flow<Int> = (1..3).asFlow()

fun main() = runBlocking<Unit> {
    try {
        simple().collect { value -> println(value) }
    } finally {
        println("Done")
    }
}  

다음으로 declarative 방식으로 onCompletion operator를 사용하는 것이다.

simple()
    .onCompletion { println("Done") }
    .collect { value -> println(value) }

operator의 장점은 모든 예외도 함께 받을 수 있다는 점이다. 

fun simple(): Flow<Int> = flow {
    emit(1)
    throw RuntimeException()
}

fun main() = runBlocking<Unit> {
    simple()
        .onCompletion { cause -> if (cause != null) println("Flow completed exceptionally") }
        .catch { cause -> println("Caught exception") }
        .collect { value -> println(value) }
} 

즉, opCompletion은 nullable Throwable을 받을 수 있기 때문에 opCompletion 내부 코드에서 로그를 찍어 볼 수 있다.

(물론 catch를 통해 예외 핸들링은 필요하다.)

 

사실 생각해보면, 굳이 operator를 사용하지 않아도 핸들링은 가능하다. try/catch/finally 처럼...

그래서 어떤게 더 좋은지 고민할 수 도 있지만, 공식문서는 스타일대로 사용할 것을 권장한다. 개인적으로도 장단점이 있긴한데, 코틀린의 특성을 따라가기 위해서는 declarative 방식이...(순전 개인적인 생각)

 

Reference

- kotlinlang.org/docs/reference/coroutines/flow.html#asynchronous-flow