Android

Retrofit Call, 제대로 사용하기 - (2)

winter223 2022. 4. 4. 16:32

 

응답 에러를 wrapping 하기

앞서 소개한 Retrofit suspend 함수는 HTTP status code 혹은 각종 조건에 따라서 에러를 throw 하고 있다. 따라서 해당 함수를 사용하기 위해서는 try-catch 문 등으로 에러를 잡아줘야한다. 혹시 실수로 try-catch 를 사용하지 않을 경우, 앱이 비정상 종료될 수 있다. 조금 더 안전하게 에러를 wrapping 해서 반환받을 수는 없을까 ?

 

확장함수 사용

앞서 Retrofit suspend 함수는 내부적으로 Call.await() 를 호출해서 enqueue()를 실행하고 있음을 알게 되었다. 이 await() 함수를 사용해서 다음과 같이 확장함수를 만들 수 있다.

interface GitHubService {
  @GET("users/{user}/repos")
  fun listRepos(@Path("user") user: String): Call<List<Repo>>
}

// 성공 시 데이터, 실패 시 에러를 wrapping 하고 있는 클래스
sealed class ApiResponse<out T> {
    data class Success<T>(val data: T): ApiResponse<T>()
    data class Failure(val error: Throwable): ApiResponse<Nothing>()
}

// Call -> ApiResponse
suspend fun <T : Any> Call<T>.toApiResponse(): ApiResponse<T> {
    return try {
        ApiResponse.Success(await())
    } catch (t: Throwable) {
        ApiResponse.Failure(t)
    }
}

// ViewModel
viewModelScope.launch {
    when (val response = gitHubService.listRepos("winter223").toApiResponse()) {
        is ApiResponse.Success -> {
            val listRepos = response.data
            // Ui 작업
        }
        is ApiResponse.Failure -> {
            when (response.error) {
                is HttpException -> {
                    // 에러 응답 코드에 따른 동작 처리
                }
                else -> {
                    // 네트워크 에러이거나 API 응답이 잘못된 경우가 많음
                    // UnexpectedError
                }
            }
        }
    }
}

이렇게 하면 toApiResponse() 를 실행할 때 Retrofit suspend 함수를 호출할 때와 마찬가지로, 내부적으로 enqueue()를 실행한다. 그리고 발생하는 에러를 잡아 ApiResponse.Failure 로 감싸서 반환한다. 따라서 해당 함수를 호출할 때 try-catch 문으로 감싸지 않아도 된다. 타입 검사 때문에 코드량은 조금 늘어난 것 같지만 실수로 앱을 종료시키는 일은 없을 것이다. (타입 검사 또한 ApiResponse 내에 유틸 함수를 추가해서 제거할 수 있다.)

 

CallAdapter 사용

Call 을 변조하지 않는 경우의 플로우

위의 예제에선 ViewModel 에서 직접 HTTP API 인터페이스 함수를 호출하고, viewModelScope 내에서 toApiResponse()로 변환해주었다. 매번 API를 호출할 때마다 toApiResponse()를 호출하는 것이 꽤 번거로울 수도 있다. Repository 를 사용한다면 그 안에서 변환 작업을 할 수 있겠지만, 진작에 반환 값으로 ApiResponse 가 내려오면 훨씬 편할 것이다. 

 

그렇게 하기 위해서는 다음과 같이 suspend 키워드와 ApiResponse<T> 반환타입을 사용해서 인터페이스 함수를 정의해야한다.

interface GitHubService {
  @GET("users/{user}/repos")
  suspend fun listRepos(@Path("user") user: String): ApiResponse<List<Repo>>
}

그럼 지난 포스팅에서 알아봤듯이 내부적으로 Call 로 감싸진 다음, Call.await() 로 enqueue() 가 실행될 것이라 기대된다. 

//HttpServiceMethod
@Override
final @Nullable ReturnT invoke(Object[] args) {
    // Call로 wrapping
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    // adapt() 호출
    return adapt(call, args);
}

//HttpServiceMethod.SuspendForBody
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
    private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
    ...
    @Override
    protected Object adapt(Call<ResponseT> call, Object[] args) {
        // callAdapter 를 통해 주어진 call 변조
        call = callAdapter.adapt(call);

        Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];

        // Call enqueue
        try {
            return isNullable
            ? KotlinExtensions.awaitNullable(call, continuation)
            : KotlinExtensions.await(call, continuation);
        } catch (Exception e) {
            return KotlinExtensions.suspendAndThrow(e, continuation);
        }
    }
}

그러나 야속하게도, ResponseBody 를 ApiResponse() 에 매핑하는 과정에서 ApiResponse()를 생성할 수 없다는 에러와 함께 앱이 종료된다. 

이유는 다음과 같다.

(콜스택이 많이 모든 코드를 첨부할 수 없어서 간략하게 나타내었다. 궁금하다면 위 에러를 일으킨 다음, 직접 콜스택을 따라가보자.)

1. 별도로 등록된 CallAdapter 가 없다면, Retrofit 은 DefaultCallAdapter 를 사용한다.

2. DefaultCallAdapter 는 대부분의 경우 Call 을 변조하지 않는다. (@SkipCallbackExecutor 어노테이션을 사용한다던가 등)

3. Call 을 변조하지 않으면, invoke() 함수에서 생성한 OkHttpCall 을 그대로 사용한다.

4. OkHttpCall 은 enqueue()를 구현할 때, onResponse 에서 응답 json 을 파싱해서 인터페이스 함수의 반환 타입 객체로 무작정 매핑하고자 한다.

5. ApiResponse 는 sealed 클래스이므로, abstract 하다. 따라서 인스턴스화 할 수 없기 때문에 에러가 발생한다.

6. 이는 try-catch 문으로 잡혀서 Callback.onFailure()를 호출한다.

7. Call.await() 에서는 위 OkHttpCall 의 enqueue()를 호출하기 때문에 onFailure()를 타고, resumeWithException() 가 실행되어 앱이 종료되는 것이다.

 

4 에 해당하는 OkHttpCall 의 enqueue() 부분을 살펴보면 다음과 같다.

// OkHttpCall.java
@Override
public void enqueue(final Callback<T> callback) {
  //..
  call.enqueue(
      // Retrofit.Callback 이 아닌, OkHttp3.Callback 을 사용
      new okhttp3.Callback() {
        @Override
        // Retrofit.Response 타입이 아닌 OkHttp3.Response 타입이 제공된다.
        // 해당 타입은 rawResponse 이므로, 직접 파싱해야한다.
        public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
          Response<T> response;
          try {
            // 응답값 파싱 후 반환 타입으로 매핑
            response = parseResponse(rawResponse);
            // ApiResponse 는 인스턴스화 불가 -> 에러 캐치
          } catch (Throwable e) {
            throwIfFatal(e);
            // callFailure() 호출
            callFailure(e);
            return;
          }

          try {
            callback.onResponse(OkHttpCall.this, response);
          } catch (Throwable t) {
            throwIfFatal(t);
            t.printStackTrace(); // TODO this is not great
          }
        }

        @Override
        public void onFailure(okhttp3.Call call, IOException e) {
          callFailure(e);
        }

        // 실패한 경우, Callback.onFailure() 호출
        private void callFailure(Throwable e) {
          try {
            callback.onFailure(OkHttpCall.this, e);
          } catch (Throwable t) {
            throwIfFatal(t);
            t.printStackTrace();
          }
        }
      });
}

보다시피 OkHttp3.Callback 을 사용하고 있기 때문에 rawResponse 를 받아서 직접 파싱하고 매핑하며, 이 과정에서 에러가 발생하면 onFailure() 로 처리한다. 참고로 Retrofit.Callback 은 Retrofit.Response 타입으로 받기 때문에 이미 기본 데이터 타입으로 파싱 및 매핑되어 전달된다.

 

결론을 말하자면 OkHttp3.Call 은 ApiResponse 에 응답을 매핑할 줄 모른다. 따라서 우리가 저 enqueue() 를 재작성해서 ApiResponse 로 직접 매핑해줘야한다. 그걸 할 수 있는 게 바로 CallAdapter이다.

 

CallAdapter와 CallAdapterFactory 작성하기

위에서 Call.await() 를 호출하기 전, CallAdapter.adapt() 함수로 Call 을 변조하는 것을 알 수 있었다. 이 CallAdapter 는 어디서 생성되어 온 것일까 ? 바로 Retrofit 을 생성하는 과정에서, 개발자가 직접 구현해서 적용할 수 있다. 이를 적용하기 위해서는 Retrofit.Builder() 가 제공하는 addConverterFactory() 라는 함수를 사용하면 된다.

Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(/**CallAdapter.Factory**/)
    .build()

addConverterFactory() 의 파라미터로 CallAdapter.Factory 객체를 넣어주면 된다. 이 팩토리 객체는 CallAdapter 를 생성할 수 있는 는 객체이다. 팩토리 패턴을 따른 것으로, Retrofit 과 CallAdapter 사이의 의존성을 약하게 만들어주는 역할을 한다. 어떤식으로 의존성을 약하게 만드는지는 아래에서 조금 더 알아보자.

 

그런데 setCallAdapterFactory() 와 같이 set 이 아닌 add 접두어를 사용한 이유가 뭘까? 그렇다. 하나의 Retrofit 인스턴스에는 여러 개의 CallAdapterFactory 를 등록할 수 있다.  Retrofit 이 HTTP API 인터페이스 함수를 호출하는 과정에서 등록된 여러 개의 CallAdapter.Factory 중에서 적절한 것을 찾고, 이를 생성해 Call 을 변조시킨다. Retrofit 은 어떻게 적절한 CallAdapter.Factory 를 찾을 수 있을까 ?

 

지난 포스팅에서 HTTP API 인터페이스 함수를 호출할 때 적절한 HttpServiceMethod 를 찾아 Call 객체를 생성한다는 것을 살펴보았다. 이 과정에서 HttpServiceMethod 를 생성하기 전에 적절한 CallAdapter 를 찾는다.

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
    boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;

    // 리턴 타입 및 어노테이션 정보 추출 과정
    Annotation[] annotations = method.getAnnotations();
    Type adapterType;
    if (isKotlinSuspendFunction) {
        Type[] parameterTypes = method.getGenericParameterTypes();
        Type responseType =
        Utils.getParameterLowerBound(
            0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
        
        adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
        annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
    } else {
        adapterType = method.getGenericReturnType();
    }

    // 리턴타입과 어노테이션 등으로 CallAdapter 찾기
    CallAdapter<ResponseT, ReturnT> callAdapter =
    createCallAdapter(retrofit, method, adapterType, annotations);
    
    // ... HttpServiceMethod 생성 및 반환
}

HTTP API 인터페이스 함수의 정보를 담고 있는 Method 로 부터 어노테이션과 리턴 타입을 꺼내오고, 이를 바탕으로 createCallAdapter() 를 호출해서 적절한 CallAdapter 를 찾고 있다. 해당 함수는 최종적으로 파라미터로 주어진 Retrofit 인스턴스의 nextCallAdapter() 라는 함수를 호출하는데, 구현부는 다음과 같다.

//Retrofit.java
public CallAdapter<?, ?> nextCallAdapter(
@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {

    int start = callAdapterFactories.indexOf(skipPast) + 1;
    for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
        // CallAdater.Factory.get() 으로 
        // 해당 리턴 타입과 어노테이션을 처리할 수 있는지 판단한다.
        CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
        if (adapter != null) {
            return adapter;
        }
    }
    throw new IllegalArgumentException(/**찾지 못했다는 에러메시지**/);
}

해당 Retrofit 인스턴스가 가지고 있는 CallAdapter.Factory 자료구조를 순회하면서 주어진 리턴 타입과 어노테이션을 처리할 수 있는 CallAdapter.Factory 를 찾는다. 이때 null 이 아닌 CallAdapter 를 반환하는 CallAdapter.Factory 가 있다면, 주어진 리턴 타입과 어노테이션을 처리할 수 있는 것으로 판단하고 해당 CallAdapter 를 채택한다. 하지만 등록된 모든 CallAdapter 를 살펴봤지만 이를 처리할 수 있는 것을 찾지 못했다면 예외가 발생한다. 그러나 아래에서 살펴보겠지만, Retrofit 은 위에서 잠깐 언급한 DefaultCallAdapterFactory 를 기본적으로 등록해두어서 예외까지 발생하는 경우는 거의 없다.

 

아무쪼록 여기서 알 수 있는 것은 다음과 같다.

- 리턴 타입과 어노테이션만으로 이를 처리할 수 있을지 없을지를 판단하는 함수는 CallAdapter.Factory.get() 이다.

  - 처리할 수 있으면 CallAdapter 를 반환한다.

  - 처리할 수 없다면 null 을 반환한다.

- 순차적으로 순회하기 때문에, 이를 처리할 수 있는 CallAdapter 가 두 개 이상이라 하더라도, 처음에 등록된 것이 채택된다. 즉, addCallAdapterFactory() 를 호출하는 순서가 중요하다.

 

그럼 CallAdapter.Factory 가 어떻게 구성되어 있는지 살펴보자.

abstract class Factory {
    /**
     * Returns a call adapter for interface methods that return {@code returnType}, or null if it
     * cannot be handled by this factory.
     */
    public abstract @Nullable CallAdapter<?, ?> get(
    Type returnType, Annotation[] annotations, Retrofit retrofit);

    /**
     * Extract the upper bound of the generic parameter at {@code index} from {@code type}. For
     * example, index 1 of {@code Map<String, ? extends Runnable>} returns {@code Runnable}.
     */
    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
        return Utils.getParameterUpperBound(index, type);
    }

    /**
     * Extract the raw class type from {@code type}. For example, the type representing {@code
     * List<? extends Runnable>} returns {@code List.class}.
     */
    protected static Class<?> getRawType(Type type) {
        return Utils.getRawType(type);
    }
}

위에서 살펴본 get() 함수만이 필수로 구현해야할 추상 메서드이다. 주석에도 적혀있듯이 주어진 인터페이스 함수를 처리할 수 있는 CallAdapter 를 반환하고, 해당 팩토리로 처리할 수 없다면 null 을 반환해야한다. 즉, "저는 이러이러한 리턴 타입과 어노테이션을 처리할 수 있는 CallAdapter 를 생성합니다." 라는 것을 명확하게 나타내야한다. 그 외 protected 함수들은 주어진 리턴 타입을 분석하는데 사용할 수 있는 유틸 함수들로 이루어져있다.

 

그럼 Retrofit 이 기본적으로 사용하고 있는 DefaultCallAdapterFactory 는 이를 어떻게 구현하고 있을까 ?

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  private final @Nullable Executor callbackExecutor;

  DefaultCallAdapterFactory(@Nullable Executor callbackExecutor) {
    this.callbackExecutor = callbackExecutor;
  }

  @Override
  public @Nullable CallAdapter<?, ?> get(
      Type returnType, Annotation[] annotations, Retrofit retrofit) {
      
    // 리턴 타입이 Call 이어야 함
    if (getRawType(returnType) != Call.class) {
      return null;
    }
    
    // Call 이 제네릭 타입이어야 함
    if (!(returnType instanceof ParameterizedType)) {
      throw new IllegalArgumentException(
          "Call return type must be parameterized as Call<Foo> or Call<? extends Foo>");
    }
    
    // 제네릭 타입 추출
    final Type responseType = Utils.getParameterUpperBound(0, (ParameterizedType) returnType);

    // SkipCallbackExecutor 어노테이션이 붙었다면 Executor 를 사용하지 않음
    // 없다면 생성자로 전달받은 callbackExcecutor 사용 (nullable)
    final Executor executor =
        Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
            ? null
            : callbackExecutor;

    // CallAdapter 생성
    return new CallAdapter<Object, Call<?>>() {
      @Override
      public Type responseType() {
        return responseType;
      }

      @Override
      public Call<Object> adapt(Call<Object> call) {
        // Executor 를 사용하지 않는다면 Call 을 변조하지 않고 그대로 반환
        // 사용한다면 Call 을 재작성하는 ExecutorCallBackCall 을 사용
        return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }

 

 

반환 타입 Call<T> or Call<out T> 라면 모두 처리할 수 있다고 주장하고 있다. 이러니 어지간하면 처리할 CallAdapterFactory 를 찾지 못하는 일이 없다는 것이다. 

 

이제 이걸 참고해서 ApiResponse 타입을 처리할 수 있는 CallAdapterFactory 를 만들어보자.

class ApiResponseCallAdapterFactory : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        // Call<ApiResponse<T>> 형태여야하기 때문에, Call 타입이어야 함
        if (getRawType(returnType) != Call::class.java) {
            return null
        }

        // 제네릭 타입이어야 함
        if (returnType !is ParameterizedType) {
            return null
        }
        
        // 제네릭 타입 추출
        val responseType = getParameterUpperBound(0, returnType)

        // 추출한 제네릭 인자가 ApiResponse 타입이어야 한다.
        if (getRawType(responseType) != ApiResponse::class.java) {
            return null
        }

        // ApiResponse 또한 제네릭 타입이어야 한다.
        if (responseType !is ParameterizedType) {
            return null
        }

        return ApiResponseCallAdapter()
    }

위에서 언급했듯이, invoke() 호출 시 Call 로 wrapping 되기 때문에 Call<ApiResponse<T>> 타입을 처리할 수 있다는 조건문으로 부터 시작해서 제네릭 타입까지 마친 후에, 우리가 생각하는 인터페이스 함수가 맞다면 비로소 ApiResponseCallAdapter 를 반환한다.

 

그럼 CallAdapter 는 어떻게 구성되어 있을까 ?

public interface CallAdapter<R, T> {
  /**
   * Returns the value type that this adapter uses when converting the HTTP response body to a Java
   * object. For example, the response type for {@code Call<Repo>} is {@code Repo}. This type is
   * used to prepare the {@code call} passed to {@code #adapt}.
   *
   * <p>Note: This is typically not the same type as the {@code returnType} provided to this call
   * adapter's factory.
   */
  Type responseType();

  /**
   * Returns an instance of {@code T} which delegates to {@code call}.
   *
   * <p>For example, given an instance for a hypothetical utility, {@code Async}, this instance
   * would return a new {@code Async<R>} which invoked {@code call} when run.
   *
   * @Override
   * public <R> Async<R> adapt(final Call<R> call) {
   *   return Async.create(new Callable<Response<R>>() {
   *     @Override
   *     public Response<R> call() throws Exception {
   *       return call.execute();
   *     }
   *   });
   * }
   */
  T adapt(Call<R> call);

adapt() 는 주어진 Call 을 변조해서 원하는 타입으로 반환할 수 있다는 것은 계속 언급되어 와서 알 것이다. 그 외의 responseType() 은 주석에 쓰여진대로 실질적으로 변환된 Response body 를 반환해주어야한다. 이는 단순하게, Retrofit 이 지원하지 않는 타입이냐 아니냐를 검사하는 정도에만 사용되는듯하다. 이는 CallAdapterFactory 의 get 에서 타입을 추출하고 있기 때문에 이를 CallAdapter 로 전달하면 될 것이다.

 

이런 요구사항에 맞워서 ApiResponseCallAdapter 를 작성하면 다음과 같다. 

class ApiResponseCallAdapterFactory : CallAdapter.Factory() {

    override fun get(
        returnType: Type,
        annotations: Array<out Annotation>,
        retrofit: Retrofit
    ): CallAdapter<*, *>? {
        //...
        // 마지막으로 ApiResponse 의 제네릭 타입을 꺼낸다.
        val actualResponseType = getParameterUpperBound(0, responseType)

        return ApiResponseCallAdapter(actualResponseType)
    }

class ApiResponseCallAdapter(
    private val returnType: Type
) : CallAdapter<R, Call<ApiResponse<R>>> {
    override fun responseType(): Type {
        return returnType
    }

    // Call -> ApiResponseCall 로 변조
    override fun adapt(call: Call<R>): Call<ApiResponse<R>> = ApiResponseCall(call)
}

// OkHttp3.Call 을 대체하게 될 Call
class ApiResponseCall<T : Any>(private val delegate: Call<T>) : Call<ApiResponse<T>> {
	
    // Call.await() 와 동일한 구조
    override fun enqueue(callback: Callback<ApiResponse<T>>) {
        delegate.enqueue(object : Callback<T> {
            override fun onResponse(call: Call<T>, response: Response<T>) {
                if (response.isSuccessful) {
                    val body = response.body()
                    if (body == null) {
                        val invocation = call.request().tag(Invocation::class.java)!!
                        val method = invocation.method()
                        val e = KotlinNullPointerException(
                            "Response from " +
                                    method.declaringClass.name +
                                    '.' +
                                    method.name +
                                    " was null but response body type was declared as non-null"
                        )
                        callback.onResponse(
                            this@ApiResponseCall,
                            Response.success(ApiResponse.Failure(e))
                        )
                    } else {
                        callback.onResponse(
                            this@ApiResponseCall,
                            Response.success(ApiResponse.Success(body))
                        )
                    }
                } else {
                    callback.onResponse(
                        this@ApiResponseCall,
                        Response.success(ApiResponse.Failure(HttpException(response)))
                    )
                }
            }

            override fun onFailure(call: Call<T>, t: Throwable) {
                callback.onResponse(
                    this@ApiResponseCall,
                    Response.success(ApiResponse.Failure(t))
                )
            }
        })
    }

    override fun isExecuted(): Boolean {
        return delegate.isExecuted
    }

    // 동기 호출을 지원하지 않음
    override fun execute(): Response<ApiResponse<T>>? {
        throw UnsupportedOperationException("ApiResponseCall doesn't support execute")
    }

    override fun cancel() {
        delegate.cancel()
    }

    override fun isCanceled(): Boolean = delegate.isCanceled

    override fun clone(): Call<ApiResponse<T>> = ApiResponseCall(delegate.clone())

    override fun request(): Request = delegate.request()

    override fun timeout(): Timeout = delegate.timeout()
}

 

이렇게 무식하게 파싱 및 매핑하는 OkHttp3.Call 을 대체할 수 있는 ApiResponseCall 을 만들어서 CallAdapter.adapt() 가 이를 반환할 수 있도록 했다. ApiResponseCall 은 OkHttp3.Call 과 마찬가지로 Call 을 상속하고 있으며, enqueue() 및 각종 함수들을 재작성한다. 이는 주어진 Call 을 OkHttp3.Callback 이 아닌 Retrofit.Callback 으로 받으면서, 이미 기본 데이터 타입으로 매핑된 응답을 상황에 따라 적절하게 ApiResponse로 직접 매핑해주었다.

 

특이한 점은 성공이든 실패든 callback.onResponse() 를 호출하고, Response.success 로 ApiResponse 를 감싸고 있다는 것이다. 왜냐하면 이는 결국엔 또다시 Call.await() 확장함수를 거칠 것이기 때문이다. 이렇게 해야만 Call.await() 가 사용하는 콜백의 onResponse 가 호출될 것이고, isSuccessful 조건문 안에서, 우리가 반환한 값을 그대로 내보낼 것이다.

return Retrofit.Builder()
    .baseUrl(BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .addCallAdapterFactory(ApiResponseCallAdapterFactory())
    .build()
    .create(GithubService::class.java)

이제 이렇게 CallAdapterFactory 를 등록해두면, HTTP API 인터페이스 함수의 반환 타입을 ApiResponse<T> 로 두어도 알아서 결과 값이 wrapping 되어 반환된다.

 

마치며

두 개의 포스팅에 걸쳐 Retrofit 의 Call 에 대해서 알아봤다. 알아본 내용을 요약하자면 다음과 같다.

 

별도 라이브러리 없이 Call과 enqeueue()를 그대로 사용하지 말자.

- 연쇄, 병렬 호출에 취약하다.

- 에러 처리가 번거롭다.

- 확장함수나 suspend 함수를 사용하자.

개발자가 Call 을 직접 사용하지 않더라도 Retrofit 은 HTTP API 인터페이스 함수를 호출할 때 내부적으로 Call 을 사용한다.

- HttpServiceMothod.invoke() - > OkHttp3.Call 로 wrapping -> HttpServiceMethod.adapt() -> CallAdapter 로 Call 변조 -> Call.await() 로 enqueue()

Retrofit suspend 함수를 사용한다면, Coroutine Context Switching 이 불필요하다.

- 내부적으로 enqueue() 를 사용하기 때문에 백그라운드 스레드에서 실행된다.

에러 캐치가 번거롭다면, seald class 를 통해 성공 및 에러에 대한 결과를 wrapping 한 객체로 변환할 수 있다.

- 확장함수나 CallAdapter 를 사용하자.

CallAdapter/CallAdapterFactory 를 활용하자

- OkHttp3.Call 은 무식하다. 등록된 ResponseBodyConverter 를 사용해 무작정 매핑하려 든다.

- 반환받고 싶은 타입이 기본 데이터타입이 아니고 복잡한 것이라면 (seald class, Flow, Observable ..) CallAdapter 를 고려하자.

- CallAdapterFactory 는 하나 이상 등록할 수 있다.

- get() 함수는 어떤 인터페이스 함수를 처리할 수 있는지 명확히 나타내는 함수이다.

 

'Android' 카테고리의 다른 글

Retrofit Call, 제대로 사용하기 - (1)  (0) 2022.04.01