이번 범위
emit하는 과정 혹은 연산자(중간 연산자, 종료 연산자 등) 안에서 예외를 던질 때, Flow 수집이 종료될 수 있다.
수집기(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라는 종료 연산자에서 발생한 예외를 잘 잡아서 처리하고, 수집이 완전히 종료된 것을 알 수 있다.
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
emitter(=방출로직) 코드의 이런 예외 처리 행동이 담긴 코드를 어떻게 캡슐화할 수 있는가? => catch 중간 연산자 사용하기
플로우는 예외에 대해 투명해야 한다. 여기서 투명하다는 말은 flow빌더 블록 안에서 try/catch로 예외를 잡아놓고서 밖에서는 아무일도 없던 척을 하지 말라는 것임. 투명하게 안에서 예외가 발생했다는 것을 외부에도 공개하라는 의미다.
이런 방출로직에서 예외 투명성을 지키기 위해, catch라는 중간 연산자가 나왔다. 즉, flow빌더 블록 안에서 try/catch하지 말고 그 포착하는 로직을 중간 연산자로 빼내라는 것임. 이로써, 방출로직의 예외 처리 로직의 캡슐화를 할 수 있게 된다.
사용 예시들 중 두 번째가 무슨 말인지 와닿지 않을 수 있다. 아래 예시 코드를 봐보자.
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에게 수집까지되고 이후로는 수집을 멈춘 것을 알 수 있다.
catch연산자는 모든 예외를 처리하려는 욕구를 갖고 있기 때문에 플로우 흐름 구조 상에서 맨 마지막으로 등장하는 것이 사실 가독성 측면에서 가장 적절하다. 이런 가독성을 챙기고 싶다면, onEach를 활용할 수 있다.
기존의 collect블록에 있던 코드를 onEach블록 안에 옮기고, onEach 중간 연산자를 catch연산자 바로 앞에 놓는 방식. 그러면, catch연산자 안에는 정말 예외처리와 관련된 정제된 코드만 남게 되므로 각 중간 연산자 별 역할 분리도 잘 되고 코드 가독성이 좋아진다는 특징을 살릴 수 있다.
simple()
.onEach { value ->
check(value <= 1) { "Collected $value" }
println(value)
}
.catch { e -> println("Caught $e") }
.collect()