[Android] Retrofit에 CallAdapter를 적용하여 원하는 응답을 받아보자

 

Android 개발을 해봤다면 네트워크 통신을 위해 Retrofit 라이브러리를 사용해 봤을 것입니다. Retrofit은  HTTP API에 대해 직접적인 조작 없이 인터페이스를 사용하여 쉽게 요청을 보낼 수 있고 응답 결과를 자바 오브젝트로 변환해 주는 라이브러리입니다. 기본 Retrofit만으로 응답을 받을 수 있지만, 정확하고 구체적으로 에러를 핸들링하고 싶다면 어떻게 해야 할까요?!

바로 CallAdapter를 적용하여 원하는 형태로 응답을 얻을 수 있습니다. 


1. 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.
public interface CallAdapter<R, T> {
    Type responseType(); // HTTP 응답의 유형을 나타내는 Type 객체를 반환
    T adapt(Call<R> call); // Call 객체를 어떻게 변환할지 정의하는 메서드
}

 

CallAdapater는 Call<R>를 T 타입으로 변환해주는 인터페이스로 CallAdapter.Factory에 의해 인스턴스가 생성됩니다. R은 서버 응답의 유형을 나타내고 T는 Retrofit이 반환될 유형을 나타냅니다. 위 코드를 통해 CallAdapter가 두 개의 메서드를 가진다는 것을 확인할 수 있습니다. 

  • responseType() : Retrofit에게 서버 응답의 유형을 알려주는 역할을 하며, Retrofit은 이를 사용하여 어떻게 응답을 처리할지 결정합니다. 예를 들어, Call<Repo>에 대한 responseType의 반환값은 Repo에 대한 타입이다.
  • adapter(Call <R> call) : Retrofit의 Call 객체를 어떻게 변환할지 정의하며, 메서드의 파라미터로 받은 call에게 작업을 위임하는 T타입 인스턴스를 반환합니다.

 

2. CallAdapter.Factory

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

CallAdapter.Factory는 CallAdapter의 인스턴스를 생성하는 팩토리 클래스로, 레트로핏 서비스 메서드의 리턴 타입에 기반한 인스턴스를 생성합니다.

  abstract class Factory {

    public abstract @Nullable CallAdapter<?, ?> get(
        Type returnType, Annotation[] annotations, Retrofit retrofit);

    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }
 
    protected static Class<?> getRawType(Type type) {
      return Utils.getRawType(type);
    }
  }
}
  • get: 파라미터로 받은 returnType과 동일한 타입을 반환하는 서비스 메서드에 대한 CallAdapter 인스턴스를 반환합니다.
  • getParameterUpperBound : type의 index 위치의 제네릭 파라미터에 대한 upper bound type을 반환합니다.
  • getRawType : type의 rawType을 반환합니다. (the type representing List <? extends Runnable> returns List.class.)

 

3. CallAdapter 사용해 보기

원하는 응답 결과로 래핑 하기 위해서는 위에서 살펴본 개념과 함께 몇 가지 과정이 필요합니다. 

1. 래핑 할 응답 클래스 정의
2. CallAdapter 인터페이스를 구현하는 클래스와 Factory 정의
3. Custom Call 정의

 

3.1 래핑 할 응답 클래스 정의

CallAdapter의 responseType을 결정하기 위해서는 먼저, 래핑 하고자 하는 응답 클래스를 정의해야 합니다. 

저는 NetworkResult라는 sealed class를 만들고 Success, Error, Exception 세 가지로 정의해 주었습니다.

sealed class NetworkResult<T : Any> {
    data class ApiSuccess<T : Any>(val data: T) : NetworkResult<T>()
    data class ApiError<T : Any>(val code: Int, val error: String) : NetworkResult<T>()
    data class ApiException<T : Any>(val e: Throwable) : NetworkResult<T>()
}

 

 

3.2 CallAdapter 인터페이스를 구현하는 Class와 Factory 정의

CallAdapter.Factory는 Retorfit이 API 요청결과를 NetworkResult로 변환할 수 있도록 도와주며 팩토리의 get 메서드에서 반환하고자 하는 타입( = Call <NetworkResult <T>>)을 파라미터로 받기 위해 이를 사용하여 받고자 하는 모델에 대한 타입을 추출할 수 있습니다.

즉, 팩토리에서 responseType을 추출하고 추출한 Type 인스턴스로 CallAdapter를 생성합니다.

class NetworkResultCallAdapterFactory() : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
    	
        //returnType의 rawType이 Call인지 확인
        if (getRawType(returnType) != Call::class.java) {
            return null
        }

	// returnType에서 1번째 제네릭 인자를 얻는다.
        val callType = getParameterUpperBound(0, returnType as ParameterizedType)
        if (getRawType(callType) != NetworkResult::class.java) {
            return null
        }

	// NetworkResult의 ParameterizedType을 얻어 CallAdapter를 생성
        val resultType = getParameterUpperBound(0, callType as ParameterizedType)
        return NetworkResultCallAdapter(resultType)
    }
    
    companion object {
        fun create(): NetworkResultCallAdapterFactory = NetworkResultCallAdapterFactory()
    }
}

 

Factory에서 CallAdapter를 생성하였으니 CallAdapter를 상속하는 클래스를 정의해 보도록 하겠습니다.

class NetworkResultCallAdapter(
    private val resultType: Type,
) : CallAdapter<Type, Call<NetworkResult<Type>>> {

    override fun responseType(): Type = resultType

    override fun adapt(call: Call<Type>): Call<NetworkResult<Type>> {
        return NetworkResultCall(call)
    }
}

CallAdapter는 반환할 responseType을 지정하며, adpater메서드는 Call <R>을 파라미터로 받아 Call <NetworkResult <R>> 로 래핑 한 인스턴스를 반환합니다. 이를 위해서 Call <NetworkResult <R>>을 구현하는 Custom Call을 정의해야 합니다.

 

 

3.3 Custom Call 정의

Retrofit 응답을 위임하려면 아래와 같이 Call 인터페이스를 구현하는 사용자 정의 Retrofit Call 클래스를 작성해야 합니다.

class NetworkResultCall<T : Any>(
    private val proxy: Call<T>,
) : Call<NetworkResult<T>> {

    override fun execute(): Response<NetworkResult<T>> = throw NotImplementedError()
    override fun enqueue(callback: Callback<NetworkResult<T>>) {
        proxy.enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                val body = response.body()
                if (response.isSuccessful && body != null) {
                    callback.onResponse(
                        this@NetworkResultCall, 
                        Response.success(NetworkResult.ApiSuccess(body)
                        )
                    )
                } else {
                    callback.onResponse(
                        this@NetworkResultCall,
                        Response.success(NetworkResult.ApiError(response.code(), error = response.message())
                        )
                    )
                }
            }

            override fun onFailure(call: Call<T>, t: Throwable) {
                val networkResult = NetworkResult.ApiException<T>(t)
                callback.onResponse(this@NetworkResultCall, Response.success(networkResult))
            }
        })
    }

    //NetworkResultCall을 복제하고, 내부의 proxy 또한 복제하여 동일한 요청을 여러 번 사용할 수 있도록 합니다.
    override fun clone(): Call<NetworkResult<T>> = NetworkResultCall(proxy.clone(), coroutineScope)
    override fun request(): Request = proxy.request()
    override fun timeout(): Timeout = proxy.timeout()
    override fun isExecuted(): Boolean = proxy.isExecuted
    override fun isCanceled(): Boolean = proxy.isCanceled
    override fun cancel() { proxy.cancel() }
}

NetworkResultCall 인스턴스에서 enqueue 메서드를 호출하면 먼저 파라미터로 받은 Call 인스턴스인 proxy의 enqueue를 호출하여 응답 결과를 얻습니다. 그 후, 응답 결과 분석을 통해 성공, 실패, 에러 여부에 따라 커스텀한 NetworkResult 객체를 생성하여 콜백에 전달합니다. 위 코드를 보면 모든 경우에 Response.success를 호출하는 것을 확인할 수 있는데요, 저희는 우선 모든 결과를 NetworkResult 형태로 받고 싶기 때문에 이를 Response.success에 담은 후, NetworkResult에서 검사하여 Success, Fail 여부를 결정하게 됩니다. 

따라서, Factory에 adapt 메서드에서 위 NetworkResultCall를 반환하여 응답 결과를 새롭게 정의한 NetworkResult로 래핑 할 수 있게 됩니다.

 

 

4. Retrofit Builder에 CallAdapterFactory를 적용하기

val retrofit = Retrofit.Builder()
  .baseUrl(BASE_URL)
  .addConverterFactory(MoshiConverterFactory.create())
  .addCallAdapterFactory(NetworkResultCallAdapterFactory.create())
  .build()

만들어둔 NetworkResultCallAdapterFactory를 Retrofit 빌더에 다음과 같이 적용할 수 있습니다.!

 

 

기존 코틀린에서 제공하는 runCatching이나 retrofit만으로도 응답을 받을 수 있지만, API 호출 시 기대하지 않는 응답코드가 올 경우 try-catch 지옥에 빠질 수 있기 때문에 이러한 경우 CallAdapter를 사용해보는 것도 좋은 것 같습니다!

 

 

Reference

https://proandroiddev.com/modeling-retrofit-responses-with-sealed-classes-and-coroutines-9d6302077dfe

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