(Kotlin Coroutines) 011. Flow exceptions

Kotlin Coroutines - Flow exceptions

안타깝게도 코루틴 플로우에서도 예외는 발생한다.

애플리케이션에서 예외의 발생은 막을 수 없는 녀석인 만큼 어떻게 처리하는 지에 대해 알아보자.

1. Collector try and catch

데이터를 소비하는 수집기 측에서의 예외처리는 try-catch 구문을 사용한다.

코투린 플로우수집기에서 try-catch 구문으로 예외 처리를 할 수 있다.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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

출력 결과

1
2
3
4
5
Emitting 1
1
Emitting 2
2
Caught java.lang.IllegalStateException: Collected 2

간단하게 플로우에 해당하는 simple() 블록을 try-catch로 감싸면 된다.

check의 조건이 1이하인 정수이므로, 시퀀스에서 2가 나오는 순간 예외가 발생하게 된다.

참고 check 메서드는 코틀린의 표준 라이브러리에 포함된 메서드이다.
조건에 맞지 않으면 IllegalStateException 예외를 발생시킨다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@kotlin.internal.InlineOnly
public inline fun check(value: Boolean): Unit {
contract {
returns() implies value
}
check(value) { "Check failed." }
}

@kotlin.internal.InlineOnly
public inline fun check(value: Boolean, lazyMessage: () -> Any): Unit {
contract {
returns() implies value
}
if (!value) {
val message = lazyMessage()
throw IllegalStateException(message.toString())
}
}

2. Everything is caught

물론 수집기 외에 영역에서 발생한 예외라도 처리가 가능하다.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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

출력 결과

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

이번 예제는 collect가 아닌 map 블록 내에서 값이 1이하 인지를 검증하도록 check 로직을 이동시켰다.

예외가 발생한 블록은 다르지만 마찬가지로 try-catch로 잡을 수 있음을 확인할 수 있다.

3. Exception transparency

코루틴 플로우에서 어디서든 예외 처리를 할 수 있다는 것은 알게되었다.

어디서든 처리할 수는 있지만 이게 옳은 방향은 아닌데, 플로우에서는 예외 처리에 있어서 투명성을 매우 강조한다.

에를 들어서

1
2
3
4
5
6
7
try {
flow {
// ...
}
} catch(...) {

}

위와 같이 처리하였을 때, 예외 처리가 불가능한 것은 아니지만 캡슐화 규칙에 위배되게 된다.

플로우는 예외 처리의 투명성을 위해 catch 연산자를 제공하며, 이를 통해 캡슐화가 가능하다.

아래 예제를 보자.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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> {
    simple()
        .catch { e -> emit("Caught $e") } // emit on exception
        .collect { value -> println(value) }
}

출력 결과

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

simple()에 붙은 catch 블록을 통해 예외를 emit할 수 있게 되었다.

이제 외부에서 플로우를 바라볼 때, 값이 방출되던 예외가 방출되던 동일한 액션으로 인지할 수 있게 되어 캡슐화가 보장되었다.

4. Transparent catch

물론 catch 연산자가 만능은 아니다.

catch 연산자는 플로우의 upstream에 한정해서만 동작하며, downstream에 대해서는 예외를 잡지않는다.

아래 예제는 다운 스트림에서 발생한 예외를 처리하지 못하는 예시를 보여준다.

import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

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

출력 결과

1
2
3
4
5
6
7
Emitting 1
1
Emitting 2
Exception in thread "main" java.lang.IllegalStateException: Collected 2
at FileKt$main$1$2.emit (File.kt:15)
at FileKt$main$1$2.emit (File.kt:14)
at kotlinx.coroutines.flow.FlowKt__ErrorsKt$catchImpl$2.emit (Errors.kt:158)