Retrofit에 CallAdapter를 적용하는 법

seong-hwan Kim
shDev
Published in
7 min readApr 27, 2021

개요

레트로핏은 HTTP API에 대해 직접적인 조작 없이 인터페이스를 사용하여 쉽게 요청을 보낼 수 있고 응답 결과를 자바 오브젝트로 변환해주는 라이브러리이다. 또한 코틀린을 사용한다면 API 호출 시 내부적으로 요청이 이루어지기 때문에 따로 콜백을 정의할 필요 없이 바로 응답 객체를 받을 수 있다.

간단한 응답과 요청에 대해서는 위의 방법으로도 충분하지만, 프로젝트를 진행하며 약간의 부족함을 느꼈다.

만약 API 호출 시 에러가 발생하거나, 기대하지 않는 응답 코드가 온다면 어떻게 처리해야 할까? 뷰 모델, 유스케이스, 리포지토리, … 뭐가 됐든 모든 API 호출마다 try-catch로 예외를 처리하는 것은 불 필요한 작업일 뿐더러, 내 기준에선 그다지 우아한 방법이 아니라고 생각했다. 내가 원하는 방식은 요청 결과를 Result로 래핑하여 API를 호출하는 위치에서 is Success, is Failure에 따라 동작을 변경하고 싶었다.

여기서는 내가 응답 결과를 변경하기 위해 레트로핏의 CallAdapter를 적용하며 겪은 삽질기를 소개한다.

Class

먼저 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.

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

CallAdapter는 두 개의 메서드를 가진다.

Type responseType();T adapt(Call<R> call);
  • responseType: 어댑터가 HTTP 응답을 자바 오브젝트로 변환할 때 반환값으로 지정할 타입을 리턴하는 메서드. 예를 들어 Call<Repo>에 대한 responseType의 반환값은 Repo에 대한 타입이다.
  • adapt: 메서드의 파라미터로 받은 call에게 작업을 위임하는 T 타입 인스턴스를 반환하는 메서드.

CallAdapter.Factory

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

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

서비스 인터페이스 메서드의 기반한 인스턴스의 정확한 의미를 파악하긴 힘들지만, 팩토리의 get 메서드에서 파라미터로 받는 returnType에 서비스 메서드의 리턴 타입이 전달된다는 것을 말하는 듯 하다.

CallAdapter.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을 반환한다. 예를 들어 getParameterUpperBound(1, Map<String, ? extends Runnable>)은 Runnable Type을 반환한다.
  • getRawType: type의 raw type을 반환한다. (raw type: 제네릭 파라미터가 생략된 타입. List<? extends Runnable>의 raw type은 List를 말한다.)

구현

내가 기대하는 방식으로 CallAdapter를 구현하기 위해선 두 가지 작업이 필요하다.

  1. CallAdpater의 responseType을 어떻게 구현할 지
  2. 응답 결과를 Result로 어떻게 래핑할 지

먼저 어댑터의 구현을 위한 뼈대를 세우고 각각의 작업을 달성한 방법을 알아보자.

Base

가장 먼저 응답을 래핑하는 데이터 클래스부터 정의하자.

  • Success: state code == 200
  • Failure: state code != 200
  • NetworkError: 네트워크 에러 발생
  • Unexpected: 기타

이후 CallAdapter 인터페이스를 구현하는 클래스와 Factory를 정의한다. 여기서는 R 타입의 응답을 Call<Result<R>>로 래핑해야 하기 때문에 CallAdapter<R, Call<Result<R>>>을 구현하는 클래스를 정의했다.

responseType

내가 작성한 API가 Call<Result<Group>>을 반환해야 한다면 어댑터의 responseType은 Group을 반환해야 한다. Call<Result<Group>>이라는 타입은 팩토리의 get 메서드에서 파라미터로 받기 때문에 이를 사용하여 Group에 대한 타입을 추출할 수 있다.

팩토리에서 responseType을 추출하고 추출한 Type 인스턴스로 CallAdapter를 생성하도록 작성되었다.

응답 결과를 Result로 래핑

CallAdapter의 adapt 메서드는 기존의 Call<R>을 파라미터로 받아 Call<Result<R>>로 래핑한 인스턴스를 반환한다. 이를 위해 Call<Result<R>>을 구현하는 Custom Call을 정의하자.

ResultCall의 핵심은 enqueue 메서드이다. 나머지 메서드는 파라미터로 받은 기존의 Call<R> 인스턴스에게 작업을 위임한다.

래핑된 ResultCall 인스턴스에서 enqueue 메서드를 호출하면 먼저 파라미터로 받은 Call 인스턴스의 enqueue를 호출하여 응답 결과를 얻는다. 이후 응답 결과를 분석하여 정상적으로 성공 했는지, 또는 실패했는지, 에러가 발생했는지에 따라 Result 객체를 생성하여 콜백에 전달한다.

각각의 경우를 보면 모든 콜백에 Response.success를 호출하는 것을 볼 수 있다. response가 isSuccessful이 아니라면 의미상으론 Response.error를 호출하는것이 맞을 듯 하나 이미 이에 대해선 위임한 Call 인스턴스에서 처리되고, 반환된 Result 객체에서 타입을 검사하여 동작하기 때문에 문제 없을듯 하다. 다만 코틀린의 suspend function을 사용하지 않고 Call<Result<R>>을 반환받는다면 모든 status code가 200으로 처리되기 때문에 주의가 필요하지 싶다.

준비는 끝났다. ResultCallAdapter의 adapt 메서드에서 ResultCall(call)을 반환하도록 수정하면 코루틴에서 응답 결과를 Result로 래핑하여 얻을 수 있다.

참고

  1. Create Retrofit CallAdapter for Coroutines to handle response as states
  2. retrofit/ErrorHandlingAdapter

--

--