
이번 글에선 네트워크 요청에 Flow를 적용한 사례를 소개 드리고자 합니다.
문제 상황

기존 Api 요청 구조는 ViewModel / Repository / DataSource / Retrofit 등 한 곳에서 예외가 발생하면 바로 ExceptionHandler로 전달되는 구조였습니다.
위 구조에는 아래와 같은 단점이 있습니다.
1. 예외 처리 구조 파악의 어려움
기존 코드에선 아래와 같이 BaseViewModel에서 launch를 재정의하여 Coroutine ExceptionHandler를 삽입해주고 있습니다.
fun launch(
dispatcher: CoroutineDispatcher = Dispatchers.Main.immediate,
start: CoroutineStart = CoroutineStart.DEFAULT,
exceptionHandler: CoroutineExceptionHandler = this.exceptionHandler,
block: suspend CoroutineScope.() -> Unit
): Job {
return viewModelScope.launch(dispatcher + exceptionHandler, start, block)
}
이 코드의 문제점은 호출하는 곳에서는 ExceptionHandler가 할당되는지 알 수 없다는 것입니다.
fun loadSampleData(userId: String) {
launch { // 호출부에선 ExceptionHandler의 존재를 알 수 없음
sampleRepository.getSampleData(userId)
/*...*/
}
}
즉, BaseViewModel의 구조를 기억하고 있지 않은 이상 예외처리가 누락될 수 있는 구조입니다.
2. 커스텀 예외 처리의 번거로움
Api 통신 과정 중에 발생하는 예외에 대해 커스텀하려면 아래와 같은 방법으로 가능합니다.
fun loadSampleData(userId: String) {
val newExceptionHandler = CoroutineExceptionHandler { context, throwable ->
// 예외 처리 로직
}
launch(exceptionHandler = newExceptionHandler) {
sampleRepository.getSampleData(userId)
/*...*/
}
}
// or
fun loadSampleData(userId: String) {
launch {
try {
sampleRepository.getSampleData(userId)
} catch (e: Exception) {
// 예외 처리 로직
}
}
}
다만, 매번 ExceptionHandler를 정의하는 것도, try-catch를 씌워서 처리하는 것도 깔끔해보이진 않습니다.
특히 Coroutine 내에서 try-catch를 통해 예외를 잡아버리는 구조는 Coroutine의 구조적 동시성을 깨트릴 수 있기 때문에 개인적으로 좋아하지 않습니다.
해결 방안
위 문제를 해결하기 위해 네트워크 요청에 Flow를 적용해보았습니다.

사실 Flow와 같은 스트림을 단일 요청인 Http에 적용하는 것이 적합하다고 생각하진 않습니다.
다만, 성능상 오버헤드가 크지 않고 예외 처리에 대한 컨벤션을 중요시 했기 때문에 좋은 대안이 될 것이라 생각했습니다.
Flow를 적용한 이유는 아래와 같습니다.
- Flow를 통해 네트워크 통신 중 발생하는 예외에 대한 파이프라인을 제공합니다.
- 예외 투명성 원칙으로 예외가 Flow를 통해 전달될 것임을 암시합니다.
코드 구조 설명
fun loadSampleData(userId: String) {
launch {
sampleRepository.getSampleData(userId)
.catch { throwable ->
// 커스텀 예외 처리
// catch에서 처리되지 않은 예외는 ExceptionHandler로 전달
}
.firstResult(
onSuccess = { data ->
// 서버와 통신 성공 했으며, 데이터도 정상적으로 받아온 경우
},
onFailure = { exception ->
// 서버와 통신은 성공 했으나, 서버에서 에러 응답을 보낸 경우
},
)
}
}
1. 데이터 구독
- Http는 단일 요청이므로 collect가 아닌 first를 사용합니다.
2. 응답 콜백 기준
- onSuccess
- 서버 통신에 성공 했으며, 데이터도 정상적으로 받아온 경우입니다.
- onFailure
- 마찬가지로 서버 통신은 성공 했으나, 서버에서 에러 응답을 반환한 경우입니다.
- 에러 응답은 CommonException으로 매핑 되어 전달됩니다.
- catch
- 서버와의 통신 과정에서 예외가 발생한 경우입니다.
- ex) 네트워크 연결 끊김, 로직에서 예외 발생 등
- 서버와의 통신 과정에서 예외가 발생한 경우입니다.
마치며
이번 글에선 Flow를 통해 네트워크 요청 중에 발생하는 예외에 대해 파이프라인을 구축한 사례를 공유 드렸습니다.
어디까지나 현 프로젝트 상황에 맞게 고안된 구조이며, 일반적으로 단일 요청에 대해 스트림을 사용하는 것은 권장되지 않습니다.
위 구조가 좋다기보다는 기존의 문제를 해결하기 위해 이런 구조를 고안했구나 정도로 생각해 주시면 좋을 것 같습니다.
다음 글에선 조금 더 구체적인 코드로 어떻게 적용하였는지 소개 드릴 예정입니다.
Reference