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 | Emitting 1 |
간단하게 플로우에 해당하는 simple()
블록을 try-catch
로 감싸면 된다.
check
의 조건이 1이하인 정수이므로, 시퀀스에서 2가 나오는 순간 예외가 발생하게 된다.
참고
check
메서드는 코틀린의 표준 라이브러리에 포함된 메서드이다.
조건에 맞지 않으면IllegalStateException
예외를 발생시킨다.
1 | internal.InlineOnly . |
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 | Emitting 1 |
이번 예제는 collect
가 아닌 map
블록 내에서 값이 1이하 인지를 검증하도록 check
로직을 이동시켰다.
예외가 발생한 블록은 다르지만 마찬가지로 try-catch
로 잡을 수 있음을 확인할 수 있다.
3. Exception transparency
코루틴 플로우에서 어디서든 예외 처리를 할 수 있다는 것은 알게되었다.
어디서든 처리할 수는 있지만 이게 옳은 방향은 아닌데, 플로우에서는 예외 처리에 있어서 투명성을 매우 강조한다.
에를 들어서
1 | try { |
위와 같이 처리하였을 때, 예외 처리가 불가능한 것은 아니지만 캡슐화 규칙에 위배되게 된다.
플로우는 예외 처리의 투명성을 위해 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 | Emitting 1 |
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 | Emitting 1 |