Retrofit CallAdapter

홍승범·2023년 1월 31일
0

Kotlin

목록 보기
4/8

개요

Retrofit은 HTTP API를 자바 api 형탤 사용할 수 있도록 하는 라이브러리로, kotlin에서는 코루틴과 엮여서 사용하게 된다. suspend 함수 형태로 선언하여 쉽게 사용할 수 있으나, 예외처리부분에서 약간의 귀차니즘이 생긴다. try-catch블록으로 매번 묶어버리기에는 보일러플레이트가 많아진다.

Result?

여기서 Result 클래스가 나타난다. 코틀린의 Result란 동작이 실패하든 성공하든 동작의 결과를 캡슐화 해서 나중에 처리될 수 있도록 하는 것이 목적인 클래스

https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/

원형은 아래와 같다.

@JvmInline value class Result<out T> /* internal constructor */ {
    val isSuccess: Boolean
    val isFailure: Boolean
    fun getOrNull(): T?
    fun exceptionOrNull(): Throwable?
    
    companion object {
        fun <T> success(value: T): Result<T>
        fun <T> failure(exception: Throwable): Result<T>
    }
}

그렇다면 retrofit의 결과를 캡슐화해서 필요할 때 필요한 처리만 하면 어떨까? 라는 생각이 들게 되고, 이를 위해 retrofit CallAdapter를 지원한다.

CallAdapter

CallAdapter 인터페이스의 정의는 아래와 같다

Adapts a Call with response type R into the type of T. Instances are created by a factory which is installed into the Retrofit instance.

Call<R> 을 T 타입으로 변환해주는 인터페이스로 CallAdapter.Factory에 의해 인스턴스가 생성된다.

다음 코드는 adapter를 구현한 예제이다

class ResponseAdapter<T>(private val successType: Type): CallAdapter<T, Call<Result<T>>> {
    override fun responseType(): Type = successType

    override fun adapt(call: Call<T>): Call<Result<T>> {
        return ResponseCall(call)
    }
}

두개의 메서드를 오버라이드한것으로 볼 수 있다.

  • responseType : 어댑터가 http 응답을 자바 오브젝트로 변환할 때 반환값으로 지정할 타입을 리턴하는 메서드. Call<ResponsSeearchMovie> 이 원래 retrofit 서비스 인터페이스의 리턴값이라고 할때 responseType 에서 반환하는 값은 ResponsSeearchMovie 가 된다
  • adapt : 메서드 파라미터로 받은 call에게 작업을 위임하는 T타입 인스턴스를 반환하는 메서드. Call<T>를 받아 Call<Result<T>> 변환해 주는 역할을 하며, 위 코드에서는 ResponseCall이라는 클래스가 그 역할을 한다.

Call Wrapper

다음은 Call<T>Call<Result<T>>로 변경해주는 ResponseCall의 구현이다.

class ResponseCall<T> (private val callDelegate: Call<T>): Call<Result<T>> {
    override fun enqueue(callback: Callback<Result<T>>)  = callDelegate.enqueue(object : Callback<T> {
        override fun onResponse(call: Call<T>, response: Response<T>) {
            response.body()?.let {
                when (response.code()) {
                    in 200..299 -> {
                        callback.onResponse(this@ResponseCall, Response.success(Result.success(it)))
                    }
                    in 400..409 -> {
                        callback.onResponse(this@ResponseCall, Response.success(Result.failure(IllegalArgumentException("cannot find"))))
                    }
                }
            } ?: callback.onResponse(this@ResponseCall, Response.success(Result.failure(IllegalArgumentException("no body"))))
        }

        override fun onFailure(call: Call<T>, t: Throwable) {
            callback.onResponse(this@ResponseCall, Response.success(Result.failure(t)))
            call.cancel()
        }

    })

    override fun cancel() = callDelegate.cancel()
    override fun clone(): Call<Result<T>> = ResponseCall(callDelegate.clone())
    override fun execute(): Response<Result<T>> {
        throw UnsupportedOperationException("unsupported operation")
    }

    override fun isCanceled(): Boolean = callDelegate.isCanceled
    override fun isExecuted(): Boolean = callDelegate.isExecuted
    override fun request(): Request = callDelegate.request()
    override fun timeout(): Timeout = callDelegate.timeout()
}

다른 부분은 파라미터로 받는 Call객체에 위임하면 되지만, enqueue는 실제로 결과를 처리해야할 부분이기 때문에 중요하게 확인해야 한다.

enqueue 메소드는, 인자로받은 Call 객체의 enqueue를 실행시켜 그 결과를 Call<Result<T>>로 변경하는 역할을 한다.

여기서 잘 살펴봐야 할 점이 있는것이, onFailuer 콜백이든 onResponse의 200번대가 아닌 코드에 대한 처리든 모두 Response의 success로 결과를 넘겨주고 있다.

onFailure에서 예외를 던지면, Api를 사용하는 입장에서 또 try-catch등으로 예외처리가 들어가야 할 테니, 그러지 말고 결과 및 예외를 success 로 Result로 포장해서 줄테니 알아서 사용해라 라는 의미다.

CallAdapater.Factory

정의는 다음과 같다.

Creates CallAdapter instances based on the return type of the service interface methods.

리턴 타입에 기반해 retrofit의 서비스 메소드에서 어댑터를 생성하는 팩토리 인스턴스이다.

팩토리 클래스의 구현은 다음과 같다.

class ResponseAdapterFactory: CallAdapter.Factory() {
    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        if (Call::class.java != getRawType(returnType)) return null
        check(returnType is ParameterizedType)

        val responseType = getParameterUpperBound(0, returnType)
        if (getRawType(responseType) != Result::class.java) return null
        check(responseType is ParameterizedType)

        val successType = getParameterUpperBound(0, responseType)
        return ResponseAdapter<Any>(successType)
    }
}

중요 메서드들이다

  • get : 파라미터로 받은 returnType과 동일한 타입을 반환하는 어댑터를 생성해 반환한다.
  • getRawType : type의 raw type을 반환
    - raw type이란 제네릭 파라미터가 생략된 타입 예를 들어 List<? extends Runnable> 의 raw type은 List가 된다.
  • getParameterUpperBound : 타입의 인덱스 위치의 제네링 파라미터의 upper bound 타입을 반환한다
    - getParameterUpperBound(1, Map<String, ? extends Runnable>) 의 예를 들면, 인덱스가 1이므로 ? extends Runnable 에서 upper bound이므로 Runnable 이 된다.

어댑터 get 메소드의 동작은 다음과 같은 순서로 이루어진다.

  1. get메소드의 파라미터인 returnType의에 대해서 해당 타입의 raw type이 Call인지 확인
  2. Call이면 그 타입이 제네릭인자를 가지는지 확인 : returnType is ParameterizedType이 이를 의미함
  3. 리턴타입에서 첫번째 제네릭 인자를 얻음.
  4. 첫번째 제네릭 인자의 raw type이 Result인지 확이
  5. Result가 맞다면, 역시 해당인자가 제네릭인자를 가지는지 확인한다.
  6. Result의 첫번째 제네릭 인자를 리턴타입으로 하는 어댑터를 생성한다.

여기까지 어댑터를 생성했다.

사용

어댑터 클래스를 인터페이스 생성시 추가한다.

Retrofit.Builder().baseUrl("https://api.themoviedb.org/3/")
            .addConverterFactory(Json {
                ignoreUnknownKeys = true
                isLenient = true
                coerceInputValues = true
            }.asConverterFactory("application/json".toMediaTypeOrNull()!!))
            .addCallAdapterFactory(ResponseAdapterFactory())
            .client(logClient).build().create(MovieInformationInterface::class.java)

인터페이스 선언시 메소드는 항상 suspend fun으로 리턴 타입은 Result로 래핑한 타입을 선언한다

      @GET("search/movie")
        suspend fun searchMovieWithPage(@Query("query") queryString: String,
                                        @Query("page") page: Int,
                                        @Query("api_key") apiKey:String = BuildConfig.TMDB_API_KEY,
                                        @Query("include_adult") includeAdult: Boolean = true): Result<ResponseSearchMovie>

사용시에는 아래와 같이 사용하면 된다

al searchedMovies = movieInformationService.searchMovie(URLEncoder.encode("Dark Knight", "utf-8"))
                searchedMovies.onSuccess {
                    totalPages = it.totalPages
                    currentPage = it.page

                }.onFailure {
                    L.e("Call Fail so end loading")
                }

https://medium.com/shdev/retrofit%EC%97%90-calladapter%EB%A5%BC-%EC%A0%81%EC%9A%A9%ED%95%98%EB%8A%94-%EB%B2%95-853652179b5b
https://dev-repository.tistory.com/105
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin/-result/

profile
그냥 사람

0개의 댓글