ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [번역] Retrofit과 Coroutine으로 safeApiCalls 만들기
    Programming/Android 2022. 1. 20. 20:06

    https://dev.to/eagskunst/making-safe-api-calls-with-retrofit-and-coroutines-1121

     

    Making safe API calls with Retrofit and Coroutines

    After the 2.6.0 release, Retrofit has official support for coroutines. The migration from other adapt...

    dev.to

    이 포스트는 위 아티클(원문)을 번역한 글입니다.

    일부 오역이 있을 수 있습니다.

     

     

     

    2.6.0 릴리즈 이후, Retrofit은 coroutine을 공식적으로 지원합니다. 다른 어댑터( ex. RxJava)로부터 마이그레이션 하는 것이 쉬워졌습니다. 

     

    coroutine 적용 전 :

    @GET("movie/{category}")
    fun getMovieListForCategory(@Path("category") category: String) : Single<MovieListResponse>

    coroutine 적용 후 :

    @GET("movie/{category}")
    suspend fun getMovieListForCategory(@Path("category") category: String) : MovieListResponse

    번경한 interface를 CoroutineContext 내부에서 호출하면 됩니다.

     

    obejct를 감싼 wraaper 대신에 object를 직접 받고 있습니다. 이 방법은 꽤 유용하지만, 오류를 판별하는 방법을 제공하지 않는다는 단점이 있습니다.

     

    물론 try-catch를 써도 됩니다만, 모든 single call에 try-catch를 다는 것은 최적의 방법이 아니고, 장기적으로 보았을때 나쁜 방법입니다. 또, 어느 누가 모든 request 에 try-catch를 달길 원할까요 ? 

     

    그래서 제가 제 작업과 개인 프로젝트를 위해 만든 솔루션을 여러분들께 보여드리려고 합니다.

     

    먼저, 우리가 가진 문제를 정의해봅시다.

     

    1. 우리는 안전하게 호출 해야 하므로 모든 call은 try-catch 가 적용되어야합니다.

    2. 우리는 모든 call이 잘 작동할 수 있는 것이 필요합니다.

    3.우리는 call 이 실패할 경우를 대비하여 무엇인가 반환해야합니다.

    4. 우리는 호출이 실패하면 어떻게든 오류나 메세지를 내보내야합니다.

     

    먼저 처음 두 문제의 경우,  call을 핸들링 하기 위한 static class나 abstract class를 만들 수 있습니다. 하지만 어떤 값이 리턴되는지 어떻게 알까요 ? 모든 object가 다르고, 각각에 맞는 방법을 찾아야하기 때문에 generic function를 사용할 수 있습니다.

     

    suspend fun <T> safeApiCall() : T

     

    이제 우리의 함수가 어떤 건지 알게 되었습니다.

    우리가 직면한 문제는 모든 인터페이스에서 작동하도록 하는 방법이 무엇인지 입니다.

     

    많은 생각을 하고 ,Retrofit 저장소와 아티클을 읽고 난 뒤, 강력한 코틀린의 고차 함수가 떠올랐습니다. 파라미터로 함수를 전달하면 그 솔루션이 모든 인터페이스에서 작동합니다. 

     

    suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T

     

    자 이런 모습이 되었네요.

    이제 try-catch로 responseFunction를 감싸고, 그것을 return 하기만 하면 됩니다. 하지만 우리는 이제 세번째 문제를 직면합니다. 이건 간단합니다. non-nullable object 대신에 nullable object를 리턴하면 됩니다.

     

    suspend inline fun <T> safeApiCall(responseFunction: suspend () -> T): T?{
        return try{
                responseFunction.invoke()//Or responseFunction()
               }catch (e: Exception){
                e.printStackTrace()
                null
               }
    }

     

    완성 ! 우리는 모든 call 에 안전하게 핸들링 할 수 있는 함수를 만들었어요. 이제 우리가 할 수 있는 최적화와, 에러를 다른 class들에게 알리는 방법을 살펴보겠습니다.

     

    우리는 세 종류의 에러를 핸들링할겁니다. 

    HttpExceptions, SocketTimeoutException 그리고 IOException 

    이를 위해 핸들링하기 쉽도록 enum 클래스를 만들겁니다. 

    Int 형을 사용해도 상관없어요

     

    enum class ErrorType {
        NETWORK, // IO
        TIMEOUT, // Socket
        UNKNOWN //Anything else
    }

     

    HttpException의 경우 API에서 오류 메세지를 가져오는 함수를 만들어보겠습니다. 여기서 오류 응답을 위한 API와 오류 코드를 사용하는 JSON 객체의 종류를 확인해야합니다.

     

    companion object {
        private const val MESSAGE_KEY = "message"
        private const val ERROR_KEY = "error"
    }
    
    fun getErrorMessage(responseBody: ResponseBody?): String {
        return try {
            val jsonObject = JSONObject(responseBody!!.string())
            when {
                jsonObject.has(MESSAGE_KEY) -> jsonObject.getString(MESSAGE_KEY)
                jsonObject.has(ERROR_KEY) -> jsonObject.getString(ERROR_KEY)
                else -> "Something wrong happened"
            }
        } catch (e: Exception) {
            "Something wrong happened"
        }
    }

     

    이제 우리는  오류를 알리는 역할을 하는 클래스에 interface를 implement 할거에요

     

    interface RemoteErrorEmitter {
        fun onError(msg: String)
        fun onError(errorType: ErrorType)
    }

     

    마지막으로 우리의 try-catch문을 수정해봅시다.

    최적화를 위해서 reponseFunction을 호출 하고 catch block에서 context가 Dispatcher.Main으로 변경될 때 coroutine context를 Dispatcher.IO로 변경합니다

    class MovieListViewModel(api: MovieListApi): ViewModel(), RemoteErrorEmitter {
      val movieListLiveData = liveData {
          val response = ApiCallsHandler.safeApiCall(this@MovieListViewModel){
              api.getMovieListForCategory("popular")
           }
          emit(response)
      }
      //RemoteErrorEmitter implementation...
    }

    . UI thread 수정시에 예외가 없도록 main dispatcher를 사용합니다.

     

    suspend inline fun <T> safeApiCall(emitter: RemoteErrorEmitter, crossinline responseFunction: suspend () -> T): T? {
        return try{
            val response = withContext(Dispatchers.IO){ responseFunction.invoke() }
            response
        }catch (e: Exception){
            withContext(Dispatchers.Main){
                e.printStackTrace()
                Log.e("ApiCalls", "Call error: ${e.localizedMessage}", e.cause)
                when(e){
                    is HttpException -> {
                        val body = e.response()?.errorBody()
                        emitter.onError(getErrorMessage(body))
                    }
                    is SocketTimeoutException -> emitter.onError(ErrorType.TIMEOUT)
                    is IOException -> emitter.onError(ErrorType.NETWORK)
                    else -> emitter.onError(ErrorType.UNKNOWN)
                }
            }
            null
        }
    }

     

    이제 진짜 끝났어요 ! 이제 coroutine을 적용한 Http call 에서 발생한 예외를 처리할 수 있는 클래스가 생겼습니다.

     

    다음은 viewModel에 적용한 예제입니다.

    class MovieListViewModel(api: MovieListApi): ViewModel(), RemoteErrorEmitter {
      val movieListLiveData = liveData {
          val response = ApiCallsHandler.safeApiCall(this@MovieListViewModel){
              api.getMovieListForCategory("popular")
           }
          emit(response)
      }
      //RemoteErrorEmitter implementation...
    }

     

     

    더 많은 예제는 이곳에서 확인할 수 있습니다.

     

    https://github.com/eagskunst/MoviesApp

     

    GitHub - eagskunst/MoviesApp: An application that show a list of categories, a list of movies,the details of the movie and let t

    An application that show a list of categories, a list of movies,the details of the movie and let the user save it on his own list. - GitHub - eagskunst/MoviesApp: An application that show a list of...

    github.com

     

Designed by Tistory.