현재 프로젝트에서는 api를 통해 데이터를 받거나 로컬에서 데이터를 받을 때 flow
를 사용하여 ViewModel
에서 받고 모든 예외
는 ViewModel
에서 처리하는 전략을 사용중이다.
api를 통해서 데이터를 받아올 때 단순한 api와의 연결도 있지만, 복잡한 비즈니스 로직의 경우 레포지토리
에서 여러 datasource
들을 이용해 처리하는 경우가 종종 생기곤 했다.
예를 들어, api를 받아올 때 로컬에 저장되어있는 데이터를 api 요청을 할 때 사용하는 경우가 있다. 이 때, 로컬에 값이 저장되어 있지 않으면 예외를 던져야한다.
여기서 문제가 발생했다...
나는 분명 viewModel에서 함수를 요청하고 catch로 예외를 받고 있었다... 하지만 앱이 튕기는 문제가 발생했네… 왜이럴까?
지금부터 flow
에 대한 나의 개념부족으로 인해 고생했던 2가지 문제에 대해서 공유하고자 한다.
fun Main() {
test().catch{
println(it)
}.collect()
}
private fun test(): Flow<Int> {
if(로컬 데이터 없음) throw IOException()
return test2()
}
private fun test2(): Flow<Int> {
return flowOf(3)
}
위의 코드는 내가 겪은 문제를 간략화시킨 코드이다. api 요청을 하기 전에 로컬의 데이터를 불러와서 이를 api 요청에 사용하는 코드이다.
이제 보면 정말 바보같은 코드지만 이때 당시에는 throw를 던지면 호출한 곳에서 당연히 받을 수 있다는 막연한 생각을 했다...
💡 Catches exceptions in the flow completion and calls a specified action with the caught exception.코틀린 공식 문서에서 flow
의 catch
는 플로우 안에서 exception
이 발생하면 이를 잡고 특정 action을 호출할 수 있다고 한다.
막연히 tyr/catch
나 runcatching
처럼 호출한 함수에서 던져진 예외를 모두 잡아서 처리할 수 있다고 생각했지만, 개념이 좀 다른 것 같다. flow
로 흐르는 파이프라인 안에서 던져진 예외에 대해서만 예외를 잡을 수 있다고 새로 이해했다!!
위의 코드에서는 플로우 안에서 예외를 던지는 게 아니기 때문에 catch를 통해 잡지 못함을 알 수 있다.
위의 코드를 해결하기 위해 아래처럼 예외를 던질 때 flow
를 통해 던지도록 구현했다…(더 좋은 방법이 있으면 댓글로 말해주면 반영하겠습니다!)
fun Main() {
test().catch{
println(it)
}.collect()
}
private fun test(): Flow<Int> {
return flowOf(Unit).map{ throw IOException() }
//return flow { emit(throw IOException()) }
return test2()
}
private fun test2(): Flow<Int> {
return flowOf(3)
}
가장 기본적인 것이지만 나처럼 throw를 하면 아래에서 당연히 받을 수 있다고 생각하지 말자…
두 번째도 위의 케이스와 유사하게 호출한 함수의 throw를 당연히 밑에서 받을 수 있겠다는 생각을 했다...
usecase
에서 flow
를 반환하는 여러 함수를 순차적으로 방출하면서 조건을 만족하는 최초의 값을 downstream으로 내려주는 케이스를 작성하다가 오류가 발생했다!!
fun getOneData(): Flow<Data?> {
val func1Result = someRepository.do1()
val func2Result = someRepository.do2()
val data = flowOf(fun1Result, func2Result)
.flattenConcat()
.map {
//doSomething
}
.firstOrNull {
//specific condition I want
}
return flow {
emit(data?.lastOrNull())
}
}
위의 코드는 특정한 조건을 만족하는 데이터를 받는 다면 이를 밑으로 방출하고 특정한 데이터가 없다면 null을 방출하는 의도로 작성했다.
여기서 당연히 fun1Result와 func2Result에서 예외가 발생한 경우 getOneData()를 호출한 viewModel에서 당연히 잡을 수 있겠다고 생각하였다.
하지만, firstOrNull 코드가 collect를 하는 함수이며 이 전에 catch를 하지 않으면 튕기는 문제가 발생!!
하지만, 나는 viewModel로 예외를 내려줘서 viewModel에서 예외를 처리하고 싶다...
고민 결과 여기서 값을 collect해서 값을 다시 방출하지 않고 flow 그대로 밑으로 전달하도록 했다. 그러면 여기서 발생하는 예외는 viewModel에서 잡을 수 있다!!
fun getOneData(): Flow<Data?> {
val func1Result = someRepository.do1()
val func2Result = someRepository.do2()
return flow {
val data = flowOf(fun1Result, func2Result)
.flattenConcat()
.map {
//doSomething
}
.firstOrNull {
//specific condition I want
}?.lastOrNull()
emit(data)
}
}
이번에 많은 crash를 경험하면서 깨달은 것은 flow는 마치 하나의 파이프라인이라는 것이다!!
이 파이프 라인에서 예외가 발생하면 파이프라인의 끝에서 발견할 수 있지만 파이프라인 밖에서 예외를 던지거나 파이프라인이 끊기게 되면 내가 원하는 최종 구간에서 발견할 수 없다.
이를 항상 염두하자!!!