본문 바로가기

안드로이드

[Android] Coroutine, Retrofit을 활용한 비동기 네트워킹 처리 중 에러 핸들링

개요

안드로이드에서 비동기 처리를 하는 대표적인 방법 중 하나는 Retrofit과 Coroutine을 활용하는 것이다. 이 과정에서 다양한 네트워크 오류 상황에 대응하기 위한 다양한 에러 핸들링 방법에 대해 소개하는 게시글이다.

 

1. try.. catch 블럭

가장 전통적인 방법이다. 단순히 네트워킹이 필요한 위치에 try..catch 블럭을 활용하면 된다.

fun updateProfile(file: File) = viewModelScope.launch(Dispatchers.IO) {
    try{
        repository.updateProfile(file)
    }catch(e: Exception){
    	//에러 처리 코드
    }
}

가장 흔하고 간단한 방법이지만 한 앱에서 여러 개의 네트워킹 처리 함수가 존재할텐데, 각 함수 내부에 모두 try... catch를 사용하여 보일러 플레이트가 늘어나고, 코드가 비대해진다.

 

2. Wrapper Class

두번째 방법은 Retrofit에 대한 반환 결과로 모델을 직접 반환하는 것이 아닌, 에러 핸들링이 가능한 Wrapper Class를 활용하여 처리하는 것이다. 이는 마치 기존 Retrofit이 Call<T>, Response<T>를 반환하는 것을 생각하면 쉽다.

api.getUser().enqueue(object : Callback<User> {
     override fun onResponse(call: Call<User>, response: Response<User>) {
        if(response.isSuccessful())
            val userInfo = response.body()
    }
    override fun onFailure(call: Call<User>, t: Throwable) {
        Log.d("error", t.message.toString())
    }
})

위에서는 명시적으로 reponse가 성공적인 경우에만 반환을 받는다.

 

이를 Coroutine+Retrofit에 적용시키면 서비스 호출 쪽에서는 다음과 같이 코드를 작성할 수 있다.

fun updateProfile(file: File) = viewModelScope.launch(Dispatchers.IO) {
    val result : ApiResponse<User> =  handleApi( { repository.updateProfile(file)  } )
    when(result){
    	is ApiResult.Success -> //
        is ApiResult.Error -> //
    }
}

 

위가 가능하기 위해서는 ApiResponse라는 Sealed Class를 작성하고 handleApi라는 Api를 처리하고 ApiResponse<T>를 반환하는 보조 함수를 작성해주어야 한다.

sealed class ApiResponse<out T : Any?>

data class Success<out T : Any?>(val data: T) : ApiResponse<T>()

data class ApiError(val exception: Exception) : ApiResponse<Nothing>()
suspend fun <T : Any> handleApi(
    call: suspend () -> Response<T>,
    errorMessage: String = "Some errors occurred, Please try again later"
): ApiResponse<T> {
    try {
        val response = call()
        if (response.isSuccessful) {
            isConnectedToNetwork = true
            response.body()?.let {
                return Success(it)
            }
        }
        response.errorBody()?.let {
            try {
                val errorString  = it.string()
                val errorObject = JSONObject(errorString)
                return ApiError(
                    RuntimeException(if(errorObject.has("message")) errorObject.getString("message") else "Error occurred, Try again Later"))
            } catch (ignored: JsonSyntaxException) {
                return Error(RuntimeException(errorMessage))
            }
        }
        return Error(RuntimeException(errorMessage))
    } catch (e: Exception) {
        if (e is IOException) {
            isConnectedToNetwork = false
        }
        return Error(RuntimeException(errorMessage))
    }
}

 

3. Coroutine Exception Handling

세번째 방법은 Coroutine의 Context로 CoroutineExceptionHandling를 전달하는 것이다. 이를 활용하면 1번의 try..catch 상용구가 늘어나는 문제도 해결할 수 있다. 또한, 2번 처럼 많은 class나 함수를 작성할 필요도 줄어든다. 필자는 아래와 같이 Retrofit을 활용한 네트워킹을 위해 BaseViewModel에 CoroutineExceptionHandler를 작성해주고, 이를 상속하는 ViewModel에서는 viewModelScope의 CoroutineContext로 CoroutineExceptionHandler를 전달해주는 방식으로 개발하고 있다. 또한, Activity나 Fragment에서는 직접 구현한 FetchState라는 Enum class를 관찰하여 Toast 메시지 등을 보여준다.

 

BaseViewModel.kt

abstract class BaseViewModel() : ViewModel() {
    protected val _fetchState = MutableLiveData<FetchState>()
    val fetchState : LiveData<FetchState>
        get() = _fetchState

    protected val exceptionHandler = CoroutineExceptionHandler{ _, throwable ->
        throwable.printStackTrace()

        when(throwable){
            is SocketException -> _fetchState.postValue(FetchState.BAD_INTERNET)
            is HttpException -> _fetchState.postValue(FetchState.PARSE_ERROR)
            is UnknownHostException -> _fetchState.postValue(FetchState.WRONG_CONNECTION)
            else -> _fetchState.postValue(FetchState.FAIL)
        }
    }

}

 

BaseViewModel 구상 클래스

class GameResultViewModel @Inject constructor(
    ....
) : BaseViewModel() {

    ....

    fun fetchGameResults() = viewModelScope.launch(exceptionHandler) {
        val results = gameRepository.fetchGameResults(userId,offset)
    }
 }

 

 

참고