프로젝트 진행 중 서버와 통신할 때 예외 처리에 대한 중복되는 코드가 상당히 늘어났다. 따라서 API의 결과를 Sealed Class로 감싸 예외에 대한 분기를 처리하려고 한다.
sealed class ApiResult<out T> {
data class Success<out T>(val value: T) : ApiResult<T>()
data class Error(
val exception: Throwable? = null,
val message: String? = ""
) : ApiResult<Nothing>()
object Empty : ApiResult<Nothing>()
}
위 ApiResult Class를 Repository에서 분기해보자.
suspend fun getSchools() = runCatching {
ApiService.getSchools()
}.fold(
onSuccess = { response ->
if (response.isSuccessful && response.body() != null) {
// 정상 응답
Success(response.body())
}
else {
// 실패 응답 (서버와 통신은 성공)
Failure(response.code(), response.message())
},
// 서버와 통신 실패
onFailure = { exception ->
NetworkError()
}
},
)
하지만 모든 repository에서 위와 같은 응답에 대한 분기 처리를 해야하기 때문에 보일러플레이트 코드가 늘어나 마음에 들지 않았다. 서버에 대한 모든 응답을 ApiResult Class를 상속받는 class/object 타입으로 처리하기 때문에, 응답값을 분기 처리하는 함수를 생성하였다.
/**
* Retrofit API 호출 시, flow로 변환하고 성공, 실패, 빈 응답에 대한 처리를 위한 함수
*/
fun <T> safeFlow(apiFunc: suspend () -> Response<T>): Flow<ApiResult<T>> = flow {
try {
val response = apiFunc.invoke()
if (response.isSuccessful) {
val body = response.body() ?: throw NullPointerException("Response body is null")
emit(ApiResult.Success(body))
}
} catch (e: NullPointerException) {
emit(ApiResult.Empty)
} catch (e: HttpException) {
emit(ApiResult.Error(e))
} catch (e: Exception) {
emit(ApiResult.Error(e, e.message))
}
}
suspend 익명 함수를 인자로 받아 처리하여 응답에 따라 ApiResult Class 형식으로 분기 처리하는 함수이다.
마지막으로 위 safeFlow()에서 생산한 값을 처리해보자.
fun handleResponse(
emptyMsg: String = "데이터가 없습니다.",
errorMsg: String = "인터넷 연결을 확인해주세요.",
onError: (String) -> Unit,
onSuccess: (T) -> Unit
) {
when (this@ApiResult) {
is Success -> onSuccess(value)
is Error -> onError(errorMsg)
is Empty -> onError(emptyMsg)
}
}
safeFlow에서 내보낸 값에 따라 성공시 서버에서 넘겨준 값을, 예외 발생시 메세지를 넘겨주는 함수이다.
이제 ViewModel에서 사용해보자.
fun getSchools() {
viewModelScope.launch {
memberRepository.getSchools().collectLatest {
it.handleResponse(
onError = { _errorMsg.value = it },
onSuccess = { _applyContent.value = it }
)
}
}
}