Hanbit the Developer

Retrofit2 Implementation(2): Top-Down from Retrofit.Builder().build() 본문

Mobile/Android

Retrofit2 Implementation(2): Top-Down from Retrofit.Builder().build()

hanbikan 2024. 2. 29. 17:48

Usage

Retrofit을 생성하는 데 자주 사용되는 가장 기본적인 코드입니다.

Retrofit.Builder()
    .baseUrl(baseUrl)
    .client(okHttpClient)
    .addConverterFactory(GsonConverterFactory.create())
    .build()

Retrofit2.java

Retrofit.Builder

빌더에는 다음과 같은 프로퍼티가 있습니다.

public static final class Builder {
  private final Platform platform;
  private @Nullable okhttp3.Call.Factory callFactory;
  private @Nullable HttpUrl baseUrl;
  private final List<Converter.Factory> converterFactories = new ArrayList<>();
  private final List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>();
  private @Nullable Executor callbackExecutor;
  private boolean validateEagerly;

  // ...
}

먼저 각 프로퍼티가 어떻게 쓰이는지부터 대략적으로 알아보겠습니다.

  • platform: Retrofit이 실행되는 플랫폼(예: 자바 VM, 안드로이드 등)에 대한 정보를 담고 있습니다. 플랫폼에 따라 다른 기본 설정이나 동작을 결정하는 데 사용됩니다.
  • callFactory(okhttp3.Call.Factory): HTTP 호출을 생성하기 위한 팩토리입니다. 주로 OkHttpClient 인스턴스가 여기에 할당되며, Retrofit이 네트워크 요청을 수행할 때 사용됩니다.
  • baseUrl: HTTP 요청의 기본 URL입니다.
  • converterFactories(Converter.Factory): 응답 데이터를 Java 객체로 변환하거나, Java 객체를 request body로 변환할 때 사용되는 converter입니다. GsonConverterFactory를 사용하여 응답 데이터를 UserResponse와 같은 data class로 변환하는 것이 그 예시입니다.
  • callAdapterFactories(CallAdapter.Factory): 기존의 Call 객체를 callbackExecutor, CompletableFuture<Repo>, @SkipCallbackExecutor과 같은 처리를 하는 Call 객체로 변환하는 역할을 합니다.
  • callbackExecutor: 비동기 콜백이 실행될 Executor입니다.
  • validateEagerly: true일 경우 서비스 인터페이스의 메소드가 구성될 때 유효성 검사를 수행합니다

Platform

Retrofit이 실행되는 플랫폼(예: 자바 VM, 안드로이드 등)에 대한 정보를 담고 있습니다. 플랫폼에 따라 다른 기본 설정이나 동작을 결정하는 데 사용됩니다.

Retrofit의 Builder는 platform을 초기화하며 시작됩니다.

public static final class Builder {
  // ...

  Builder(Platform platform) {
    this.platform = platform;
  }

  public Builder() {
    this(Platform.get());
  }

  // ...
}

Constructor

Platform은 get() 함수, PLATFORM 프로퍼티, findPlatform() 함수를 통해 생성됩니다.

class Platform {
  private static final Platform PLATFORM = findPlatform();

  static Platform get() {
    return PLATFORM;
  }

  private static Platform findPlatform() {
    return "Dalvik".equals(System.getProperty("java.vm.name"))
        ? new Android() //
        : new Platform(true);
  }

  private final boolean hasJava8Types;
  private final @Nullable Constructor lookupConstructor;

  Platform(boolean hasJava8Types) {
    this.hasJava8Types = hasJava8Types;

    Constructor lookupConstructor = null;
    if (hasJava8Types) {
      try {
        // Because the service interface might not be public, we need to use a MethodHandle lookup
        // that ignores the visibility of the declaringClass.
        lookupConstructor = Lookup.class.getDeclaredConstructor(Class.class, int.class);
        lookupConstructor.setAccessible(true);
      } catch (NoClassDefFoundError ignored) {
        // Android API 24 or 25 where Lookup doesn't exist. Calling default methods on non-public
        // interfaces will fail, but there's nothing we can do about it.
      } catch (NoSuchMethodException ignored) {
        // Assume JDK 14+ which contains a fix that allows a regular lookup to succeed.
        // See <https://bugs.openjdk.java.net/browse/JDK-8209005>.
      }
    }
    this.lookupConstructor = lookupConstructor;
  }
  // ...
}
  • 시스템 환경이 달빅 머신, 즉 안드로이드 환경이면 Android(Platform의 자식 클래스)를 아니라면 Platform을 반환합니다.
  • Retrofit2가 안드로이드에서 쓰는 줄로만 알았는데 플랫폼을 체크해주는 로직이 있어서 의아했습니다. Retrofit2 github repository에 들어가보니 설명에 다음과 같이 쓰여져 있었습니다.

A type-safe HTTP client for Android and the JVM

  • 접근성을 무시하고 클래스의 메소드를 호출할 수 있는 Lookup 생성자를 초기화합니다. 이는 이전 글에서 살펴보았던 Retrofit.create() 함수에서 디폴트 메소드일 때 이를 호출하는 데 사용됩니다.(platform.invokeDefaultMethod())

defaultCallbackExecutor

class Platform {
  // ...

  @Nullable
  Executor defaultCallbackExecutor() {
    return null;
  }

  // ...

  static final class Android extends Platform {
    Android() {
      super(Build.VERSION.SDK_INT >= 24);
    }

    @Override
    public Executor defaultCallbackExecutor() {
      return new MainThreadExecutor();
    }

    // ...

    static final class MainThreadExecutor implements Executor {
      private final Handler handler = new Handler(Looper.getMainLooper());

      @Override
      public void execute(Runnable r) {
        handler.post(r);
      }
    }
  }
}

플랫폼이 Android일 경우 메인 루퍼로 쓰레드를 실행하는 MainThreadExecutor를 executor로 지정합니다.

defaultCallAdapterFactories, defaultConverterFactories

class Platform {
  // ...

  List<? extends CallAdapter.Factory> defaultCallAdapterFactories(
      @Nullable Executor callbackExecutor) {
    DefaultCallAdapterFactory executorFactory = new DefaultCallAdapterFactory(callbackExecutor);
    return hasJava8Types
        ? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory)
        : singletonList(executorFactory);
  }

  int defaultCallAdapterFactoriesSize() {
    return hasJava8Types ? 2 : 1;
  }

  List<? extends Converter.Factory> defaultConverterFactories() {
    return hasJava8Types ? singletonList(OptionalConverterFactory.INSTANCE) : emptyList();
  }

  int defaultConverterFactoriesSize() {
    return hasJava8Types ? 1 : 0;
  }

  // ...
}

DefaultCallAdapterFactory, OptionalConverterFactory는 Java 8 이상에서만 사용 가능한 클래스입니다. 따라서 Java 8 지원 여부에 따라 반환되는 리스트가 달라지는 것을 확인할 수 있습니다.

 

isDefaultMethod, invokeDefaultMethod

class Platform {
  // ...

  @IgnoreJRERequirement // Only called on API 24+.
  boolean isDefaultMethod(Method method) {
    return hasJava8Types && method.isDefault();
  }

  @IgnoreJRERequirement // Only called on API 26+.
  @Nullable
  Object invokeDefaultMethod(Method method, Class<?> declaringClass, Object object, Object... args)
      throws Throwable {
    Lookup lookup =
        lookupConstructor != null
            ? lookupConstructor.newInstance(declaringClass, -1 /* trusted */)
            : MethodHandles.lookup();
    return lookup.unreflectSpecial(method, declaringClass).bindTo(object).invokeWithArguments(args);
  }

  // ...

  static final class Android extends Platform {
    // ...

    @Nullable
    @Override
    Object invokeDefaultMethod(
        Method method, Class<?> declaringClass, Object object, Object... args) throws Throwable {
      if (Build.VERSION.SDK_INT < 26) {
        throw new UnsupportedOperationException(
            "Calling default methods on API 24 and 25 is not supported");
      }
      return super.invokeDefaultMethod(method, declaringClass, object, args);
    }

    // ...
  }
}

invokeDefaultMethod의 경우 lookup이 없으면 초기화해준 뒤 method를 invoke합니다.

Call.Factory

HTTP 호출을 생성하기 위한 팩토리입니다. 주로 OkHttpClient 인스턴스가 여기에 할당되며, Retrofit이 네트워크 요청을 수행할 때 사용됩니다.

다음으로 알아볼 프로퍼티는 Call.Factory입니다. Call.Factory는 인터페이스이며 OkHttpClient가 이것을 구현하고 있습니다.

public interface Call : kotlin.Cloneable {
    public abstract fun cancel(): kotlin.Unit

    public abstract fun clone(): okhttp3.Call

    public abstract fun enqueue(responseCallback: okhttp3.Callback): kotlin.Unit

    public abstract fun execute(): okhttp3.Response

    public abstract fun isCanceled(): kotlin.Boolean

    public abstract fun isExecuted(): kotlin.Boolean

    public abstract fun request(): okhttp3.Request

    public abstract fun timeout(): okio.Timeout

    public fun interface Factory {
        public abstract fun newCall(request: okhttp3.Request): okhttp3.Call
    }
}
open class OkHttpClient internal constructor(
  builder: Builder
) : Cloneable, Call.Factory, WebSocket.Factory {
  // ...

  /** Prepares the [request] to be executed at some point in the future. */
  override fun newCall(request: Request): Call = RealCall(this, request, forWebSocket = false)

  // ...
}

OkHttpClient는 newCall() 함수를 통해 Call을 반환하며 구현체인 RealCall를 반환하게 됩니다.(RealCall은 OkHttp 코드임)

Converter.Factory

응답 데이터를 Java 객체로 변환하거나, Java 객체를 request body로 변환할 때 사용되는 converter입니다. GsonConverterFactory를 사용하여 응답 데이터를 UserResponse와 같은 data class로 변환하는 것이 그 예시입니다.

다음은 Converter 및 Factory의 코드입니다.

public interface Converter<F, T> {
  @Nullable
  T convert(F value) throws IOException;

  abstract class Factory {
    public @Nullable Converter<ResponseBody, ?> responseBodyConverter(
        Type type, Annotation[] annotations, Retrofit retrofit) {
      return null;
    }

    public @Nullable Converter<?, RequestBody> requestBodyConverter(
        Type type,
        Annotation[] parameterAnnotations,
        Annotation[] methodAnnotations,
        Retrofit retrofit) {
      return null;
    }

    public @Nullable Converter<?, String> stringConverter(
        Type type, Annotation[] annotations, Retrofit retrofit) {
      return null;
    }

    protected static Type getParameterUpperBound(int index, ParameterizedType type) {
      return Utils.getParameterUpperBound(index, type);
    }

    protected static Class<?> getRawType(Type type) {
      return Utils.getRawType(type);
    }
  }
}

Factory에서 기본적으로 null을 반환해주고 있습니다.

실제 구현 사례인 GsonConverterFactory의 코드는 다음과 같습니다.

/**
 * A {@linkplain Converter.Factory converter} which uses Gson for JSON.
 *
 * <p>Because Gson is so flexible in the types it supports, this converter assumes that it can
 * handle all types. If you are mixing JSON serialization with something else (such as protocol
 * buffers), you must {@linkplain Retrofit.Builder#addConverterFactory(Converter.Factory) add this
 * instance} last to allow the other converters a chance to see their types.
 */
public final class GsonConverterFactory extends Converter.Factory {
  /**
   * Create an instance using a default {@link Gson} instance for conversion. Encoding to JSON and
   * decoding from JSON (when no charset is specified by a header) will use UTF-8.
   */
  public static GsonConverterFactory create() {
    return create(new Gson());
  }

  /**
   * Create an instance using {@code gson} for conversion. Encoding to JSON and decoding from JSON
   * (when no charset is specified by a header) will use UTF-8.
   */
  @SuppressWarnings("ConstantConditions") // Guarding public API nullability.
  public static GsonConverterFactory create(Gson gson) {
    if (gson == null) throw new NullPointerException("gson == null");
    return new GsonConverterFactory(gson);
  }

  private final Gson gson;

  private GsonConverterFactory(Gson gson) {
    this.gson = gson;
  }

  @Override
  public Converter<ResponseBody, ?> responseBodyConverter(
      Type type, Annotation[] annotations, Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonResponseBodyConverter<>(gson, adapter);
  }

  @Override
  public Converter<?, RequestBody> requestBodyConverter(
      Type type,
      Annotation[] parameterAnnotations,
      Annotation[] methodAnnotations,
      Retrofit retrofit) {
    TypeAdapter<?> adapter = gson.getAdapter(TypeToken.get(type));
    return new GsonRequestBodyConverter<>(gson, adapter);
  }
}

CallAdapter.Factory

기존의 Call 객체를 callbackExecutor, CompletableFuture<Repo>, @SkipCallbackExecutor과 같은 처리를 하는 Call 객체로 변환하는 역할을 합니다.

public interface CallAdapter<R, T> {
  Type responseType();
  T adapt(Call<R> call);

  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);
    }
  }
}

  • CallAdapter: Call<R>를 T로 변환하는 클래스이다.
  • responseType(): response body를 자바 오브젝트로 변환할 때의 타입을 반환한다. 예를 들어 반환 타입이 Call<Repo>일 경우 Repo를 반환해야 한다.
  • adapt(): call에게 위임하여 T 타입의 인스턴스를 반환하는 함수이다.
  • Factory.get(): CallAdapter를 반환한다.
  • Factory.getParameterUpperBound(): index번째 제네릭의 upper bound 타입을 반환한다. 예를 들어 Map<String, ? extends Runnable>에 인덱스 1을 넣을 경우 Runnable이 반환된다.(0을 넣으면 String이 반환된다.)
  • Factory.getRawType(): type에 대한 raw class를 반환한다. 예를 들어 type이 List<? extends Runnable>을 나타낸다면 List.class가 반환된다.

참고로 retrofit.callAdapterFactories(List<CallAdapter.Factory>)에는 디폴트로 CompletableFutureCallAdapterFactory, DefaultCallAdapterFactory가 들어가게 됩니다. 이때 CompletableFuture는 Java 8 이상부터 지원되기 때문에 버전이 맞지 않을 때에는 사용되지 않습니다.

// Platform.java
List<? extends CallAdapter.Factory> defaultCallAdapterFactories(
    @Nullable Executor callbackExecutor) {
  DefaultCallAdapterFactory executorFactory = new DefaultCallAdapterFactory(callbackExecutor);
  return hasJava8Types
      ? asList(CompletableFutureCallAdapterFactory.INSTANCE, executorFactory)
      : singletonList(executorFactory);
}

DefaultCallAdapterFactory

가장 기본적인 CallAdapter의 구현입니다.

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) {
    if (getRawType(returnType) != Call.class) {
      return null;
    }
    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);

    final Executor executor =
        Utils.isAnnotationPresent(annotations, SkipCallbackExecutor.class)
            ? null
            : callbackExecutor;

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

      @Override
      public Call<Object> adapt(Call<Object> call) {
        return executor == null ? call : new ExecutorCallbackCall<>(executor, call);
      }
    };
  }
  // ...
}
  • 예외 처리: 타입이 다르거나, 리턴 타입이 Call인 경우
  • SkipCallbackExecutor 어노테이션이 없는 경우(일반적인 경우): adapt에서 ExecutorCallbackCall 인스턴스 반환(ExecutorCallbackCall은 아래에서 다룹니다.)
  • SkipCallbackExecutor 어노테이션이 있는 경우: Retrofit 객체의 callbackExecutor를 사용하지 않고 현재 쓰레드에서 그대로 호출한다.(주로 동기 호출 시 사용)

다음으로 ExecutorCallbackCall의 구현입니다. 응답을 retrofit.callbackExecutor에서 처리하도록 해줍니다.

final class DefaultCallAdapterFactory extends CallAdapter.Factory {
  // ...

  static final class ExecutorCallbackCall<T> implements Call<T> {
    final Executor callbackExecutor;
    final Call<T> delegate;

    ExecutorCallbackCall(Executor callbackExecutor, Call<T> delegate) {
      this.callbackExecutor = callbackExecutor;
      this.delegate = delegate;
    }

    @Override
    public void enqueue(final Callback<T> callback) {
      Objects.requireNonNull(callback, "callback == null");

      delegate.enqueue(
          new Callback<T>() {
            @Override
            public void onResponse(Call<T> call, final Response<T> response) {
              callbackExecutor.execute(
                  () -> {
                    if (delegate.isCanceled()) {
                      // Emulate OkHttp's behavior of throwing/delivering an IOException on
                      // cancellation.
                      callback.onFailure(ExecutorCallbackCall.this, new IOException("Canceled"));
                    } else {
                      callback.onResponse(ExecutorCallbackCall.this, response);
                    }
                  });
            }

            @Override
            public void onFailure(Call<T> call, final Throwable t) {
              callbackExecutor.execute(() -> callback.onFailure(ExecutorCallbackCall.this, t));
            }
          });
    }

    @Override
    public boolean isExecuted() {
      return delegate.isExecuted();
    }

    @Override
    public Response<T> execute() throws IOException {
      return delegate.execute();
    }

    @Override
    public void cancel() {
      delegate.cancel();
    }

    @Override
    public boolean isCanceled() {
      return delegate.isCanceled();
    }

    @SuppressWarnings("CloneDoesntCallSuperClone") // Performing deep clone.
    @Override
    public Call<T> clone() {
      return new ExecutorCallbackCall<>(callbackExecutor, delegate.clone());
    }

    @Override
    public Request request() {
      return delegate.request();
    }

    @Override
    public Timeout timeout() {
      return delegate.timeout();
    }
  }
}
  • enqueue(): 기존의 Call 인스턴스를 delegate로 활용하고 있으며 응답이 오면 callbackExecutor에서 콜백을 호출한다.
  • 변형이 필요 없는 경우에는 delegate 호출

요약하면 DefaultCallAdapterFactory는 SkipCallbackExecutor를 처리해주는 역할, Retrofit의 callbackExecutor에서 Call의 응답 처리를 하게 만드는 역할을 합니다.

CompletableFutureCallAdapterFactory

다음으로 CompletableFutureCallAdapterFactory의 구현입니다. 이 클래스는 Java의 CompletableFuture를 지원하기 위해 사용되는 팩토리입니다.

get()

@Nullable
public CallAdapter<?, ?> get(Type returnType, Annotation[] annotations, Retrofit retrofit) {
    if (getRawType(returnType) != CompletableFuture.class) {
        return null;
    } else if (!(returnType instanceof ParameterizedType)) {
        throw new IllegalStateException("CompletableFuture return type must be parameterized as CompletableFuture<Foo> or CompletableFuture<? extends Foo>");
    } else {
        Type innerType = getParameterUpperBound(0, (ParameterizedType)returnType);
        if (getRawType(innerType) != Response.class) {
            return new BodyCallAdapter(innerType);
        } else if (!(innerType instanceof ParameterizedType)) {
            throw new IllegalStateException("Response must be parameterized as Response<Foo> or Response<? extends Foo>");
        } else {
            Type responseType = getParameterUpperBound(0, (ParameterizedType)innerType);
            return new ResponseCallAdapter(responseType);
        }
    }
}
  • 예외 처리: CompletableFuture가 아닌 경우, 제네릭으로 감싸지 않은 경우, CompletableFuture<Response>인 경우(Response에 제네릭을 두지 않음)
  • CompletableFuture 내부 타입이 Response이면 ResponseCallAdapter 반환
  • CompletableFuture 내부 타입이 Response가 아니면 BodyCallAdapter 반환

BodyCallAdapter

@IgnoreJRERequirement
private static final class BodyCallAdapter<R> implements CallAdapter<R, CompletableFuture<R>> {
  private final Type responseType;

  BodyCallAdapter(Type responseType) {
    this.responseType = responseType;
  }

  @Override
  public Type responseType() {
    return responseType;
  }

  @Override
  public CompletableFuture<R> adapt(final Call<R> call) {
    CompletableFuture<R> future = new CallCancelCompletableFuture<>(call);
    call.enqueue(new BodyCallback(future));
    return future;
  }

  @IgnoreJRERequirement
  private class BodyCallback implements Callback<R> {
    private final CompletableFuture<R> future;

    public BodyCallback(CompletableFuture<R> future) {
      this.future = future;
    }

    @Override
    public void onResponse(Call<R> call, Response<R> response) {
      if (response.isSuccessful()) {
        future.complete(response.body());
      } else {
        future.completeExceptionally(new HttpException(response));
      }
    }

    @Override
    public void onFailure(Call<R> call, Throwable t) {
      future.completeExceptionally(t);
    }
  }
}
  • adapt(): enqueue() 호출 및 CompletableFuture 반환
  • BodyCallback: 성공 시 CompletableFuture.complete() 호출, 실패 시 CompletableFuture.completeExceptionally() 호출

ResponseCallAdapter

@IgnoreJRERequirement
  private static final class ResponseCallAdapter<R>
    implements CallAdapter<R, CompletableFuture<Response<R>>> {
  private final Type responseType;

  ResponseCallAdapter(Type responseType) {
    this.responseType = responseType;
  }

  @Override
  public Type responseType() {
    return responseType;
  }

  @Override
  public CompletableFuture<Response<R>> adapt(final Call<R> call) {
    CompletableFuture<Response<R>> future = new CallCancelCompletableFuture<>(call);
    call.enqueue(new ResponseCallback(future));
    return future;
  }

  @IgnoreJRERequirement
  private class ResponseCallback implements Callback<R> {
    private final CompletableFuture<Response<R>> future;

    public ResponseCallback(CompletableFuture<Response<R>> future) {
      this.future = future;
    }

    @Override
    public void onResponse(Call<R> call, Response<R> response) {
      future.complete(response);
    }

    @Override
    public void onFailure(Call<R> call, Throwable t) {
      future.completeExceptionally(t);
    }
  }
}
  • adapt(): enqueue() 호출 및 CompletableFuture 반환
  • ResponseCallback: onResponse()에서 CompletableFuture.complete(Response) 호출, onFailure()에서 CompletableFuture.completeExceptionally(Throwable) 호출

이렇게 CallAdapter의 구현체까지 확인하였습니다. 이로써 Retrofit Builder 프로퍼티의 주요 클래스, Platform, Call, Converter, CallAdapter를 모두 알아보았습니다. 각각의 역할을 요약하면 다음과 같습니다.

  • Platform: 플랫폼을 고려하여 executor 지정함. defaultCallAdapterFactories(), invokeDefaultMethod() 등 유틸리티 함수 포함
  • Call.Factory: HTTP 요청에 사용됨. 구현 예시: OkHttpClient
  • Converter.Factory: response 및 request를 처리하는 converter를 반환함. 구현 예시: GsonConverterFactory
  • CallAdapter.Factory: Call 객체에 callbackExecutor, CompletableFuture, @SkipCallbackExecutor를 처리하여 반환. 구현 예시: DefaultCallAdapterFactory, CompletableFutureCallAdapterFactory

Retrofit.java

Builder.build()

이제 앞서 알아본 프로퍼티가 어떻게 초기화되는지 알아보겠습니다. 다음은 Retrofit 인스턴스를 생성하는 빌드 함수입니다.

/**
 * Create the {@link Retrofit} instance using the configured values.
 *
 * <p>Note: If neither {@link #client} nor {@link #callFactory} is called a default {@link
 * OkHttpClient} will be created and used.
 */
public Retrofit build() {
  if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }

  okhttp3.Call.Factory callFactory = this.callFactory;
  if (callFactory == null) {
    callFactory = new OkHttpClient();
  }

  Executor callbackExecutor = this.callbackExecutor;
  if (callbackExecutor == null) {
    callbackExecutor = platform.defaultCallbackExecutor();
  }

  // Make a defensive copy of the adapters and add the default Call adapter.
  List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories);
  callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor));

  // Make a defensive copy of the converters.
  List<Converter.Factory> converterFactories =
      new ArrayList<>(
          1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize());

  // Add the built-in converter factory first. This prevents overriding its behavior but also
  // ensures correct behavior when using converters that consume all types.
  converterFactories.add(new BuiltInConverters());
  converterFactories.addAll(this.converterFactories);
  converterFactories.addAll(platform.defaultConverterFactories());

  return new Retrofit(
      callFactory,
      baseUrl,
      unmodifiableList(converterFactories),
      unmodifiableList(callAdapterFactories),
      callbackExecutor,
      validateEagerly);
}
  • OkHttpClient가 등록되지 않았으면 기본 생성자로 생성하여 사용합니다.
  • callbackExecutor 또한 등록되지 않았으면 defaultCallbackExecutor를 사용합니다.(Android 플랫폼의 경우 MainThreadExecutor)
  • callAdapterFactories, converterFactories를 초기화합니다.

이때 특이한 점은 converterFactories의 맨 앞에 BuiltInConverters를 넣어준다는 점입니다. BuiltInConverters의 필요성은 다음과 같습니다.(GPT4)

왜 BuiltInConverters가 필요한가?

  • 보편적인 처리: 모든 HTTP 응답이나 요청이 JSON 형태로 되어 있는 것은 아닙니다. 예를 들어, 단순한 문자열이나 바이너리 데이터를 처리해야 할 경우가 있습니다. **BuiltInConverters**는 이러한 비-JSON 데이터를 처리할 수 있는 기본적인 변환기를 제공합니다.
  • 폴백 메커니즘: 사용자가 등록한 변환기가 특정 응답 타입을 처리할 수 없을 때, Retrofit은 **BuiltInConverters**를 폴백 변환기로 사용합니다. 이는 Retrofit이 변환 과정에서 발생할 수 있는 예외 상황을 자연스럽게 처리할 수 있게 해 줍니다.
  • 경량화된 변환 처리: 간단한 응답이나 요청에 대해 복잡한 JSON 변환기를 사용하는 것은 오버헤드일 수 있습니다. **BuiltInConverters**를 사용하면 이러한 간단한 경우에 대한 빠르고 효율적인 처리가 가능합니다.

responseBodyConverter()

지금까지는 Retrofit이 어떻게 초기화되는지 살펴보았습니다. 다음은 앞서 알아본 Converter, CallAdapter와 같은 Retrofit의 멤버가 외부에서 어떻게 사용되는지를 알아보겠습니다.

먼저 다음과 같이 단순하게 프로퍼티 내용을 외부에 노출하는 함수가 있습니다.

public List<Converter.Factory> converterFactories() {
  return converterFactories;
}

반면에 CallAdapter와 Converter의 경우에는 nextCallAdapter(), nextRequestBodyConverter(), nextResponseBodyConverter() 함수에서 상대적으로 복잡하게 처리됩니다.

다음은 그 예시인 responseBodyConverter() 및 nextResponseBodyConverter() 함수의 코드입니다.

public <T> Converter<ResponseBody, T> responseBodyConverter(Type type, Annotation[] annotations) {
  return nextResponseBodyConverter(null, type, annotations);
}

public <T> Converter<ResponseBody, T> nextResponseBodyConverter(
  @Nullable Converter.Factory skipPast, Type type, Annotation[] annotations) {
Objects.requireNonNull(type, "type == null");
Objects.requireNonNull(annotations, "annotations == null");

int start = converterFactories.indexOf(skipPast) + 1;
for (int i = start, count = converterFactories.size(); i < count; i++) {
  Converter<ResponseBody, ?> converter =
      converterFactories.get(i).responseBodyConverter(type, annotations, this);
  if (converter != null) {
    //noinspection unchecked
    return (Converter<ResponseBody, T>) converter;
  }
}
  • conveterFactories에서 skipPast를 찾은 뒤 그것의 다음 요소부터 반복문을 시작합니다. skipPast가 없을 경우(= skipPast가 null인 경우)에는 인덱스 0부터 시작합니다.
  • responseBodyConverter()를 수행한 뒤 적절한 컨버터가 반환되었다면 그것을 반환합니다.

다음은 BuiltInConverters.responseBodyConverter()의 구현입니다.

final class BuiltInConverters extends Converter.Factory {
  /** Not volatile because we don't mind multiple threads discovering this. */
  private boolean checkForKotlinUnit = true;

  @Override
  public @Nullable Converter<ResponseBody, ?> responseBodyConverter(
      Type type, Annotation[] annotations, Retrofit retrofit) {
    if (type == ResponseBody.class) {
      return Utils.isAnnotationPresent(annotations, Streaming.class)
          ? StreamingResponseBodyConverter.INSTANCE
          : BufferingResponseBodyConverter.INSTANCE;
    }
    if (type == Void.class) {
      return VoidResponseBodyConverter.INSTANCE;
    }
    if (checkForKotlinUnit) {
      try {
        if (type == Unit.class) {
          return UnitResponseBodyConverter.INSTANCE;
        }
      } catch (NoClassDefFoundError ignored) {
        checkForKotlinUnit = false;
      }
    }
    return null;
  }

  // ...
}
  • 타입이 ResponseBody와 같은 경우, 메소드에 Streaming 어노테이션이 붙어있다면 StreamingResponseBodyConverter, 아니라면 BufferingResponseBodyConverter의 인스턴스를 반환한다. 이때 ResponseBody는 HTTP 응답 결과의 raw byte를 담고 있는 클래스를 의미한다. 즉 이 경우는 다른 자바 클래스로 변환할 필요가 없는 경우이다.
  • Void 및 Unit 타입일 경우 각각 VoidResponseBodyConverter, UnitResponseBodyConverter의 인스턴스를 반환한다.
  • 해당하지 않을 경우 null을 반환한다.

null이 반환되는 경우 BuiltInConverters가 적절한 컨버터가 아니라는 의미이므로 nextResponseBodyConverter()의 반복문이 진행되며 적절한 컨버터를 찾게 됩니다.

 

Retrofit은 이렇게 빌더 패턴을 통해 초기화된 Call.Fatory, HttpUrl, CallAdapter.Factory, Converter.Factory, Executor를 외부에 공개하고 있습니다. 특히 이전 글에서 살펴보았던 HttpServiceMethod.parseAnnotations() 함수에서 callFactory, callAdapter(), responseBodyConverter()를 사용하게 됩니다.

Retrofit 구현 요약

Retrofit.Builder().build()

  • callFactory = OkHttpClient
  • callAdapterFactories = CompletableFutureCallAdapterFactory, DefaultCallAdapterFactory
  • convertFactories = BuiltInConverters, GsonConverterFactory

retrofit.create(ApiService::class.java).invoke(): 서비스 인터페이스 함수 호출

  • Proxy.newProxyInstance → InvocationHandler → invoke(): loadServiceMethod() → return ServiceMethod.parseAnnotations()
  • ServiceMethod.parseAnnotations() 내부
    • RequestFactory.parseAnnotations(): 어노테이션을 해석하여 RequestFactory 생성
    • return HttpServiceMethod.parseAnnotations()
  • HttpServiceMethod.parseAnnotations() 내부
    • adapterType = Call<Repo>(Call<Repo>, Repo, Response<Repo> → Call<Repo>)
    • callAdapter = Retrofit.nextCallAdapter(adapterType) → callAdapterFactories[i].get()(null이면 다음 팩토리 찾는 식으로 적절한 팩토리 찾기)
    • responseConverter = Retrofit.nextResponseBodyConverter(responseType)
    • callFactory = Retrofit.callFactory
    • return HttpServiceMethod: suspend 함수 여부 및 반환 타입에 따라 반환되는 구현체 타입이 달라짐. suspend 함수가 아닌 경우 → CallAdapted, Response<Repo> → SuspendForResponse, Repo → SuspendForBody
    • SuspendForResponse, SuspendForBody에서의 adapt() 함수: Kotlin await() 함수를 통해 결과값 기다린 뒤 반환
  • (이렇게 해서 ServiceMethod가 반환되었으며, Retrofit.create() 함수로 돌아와서 invoke() 수행)
  • (ServiceMethod.invoke()가 구현되어 있는 HttpServiceMethod.invoke() 함수 호출): call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter); return adapt(call, args); → CallAdapted, SuspendForResponse, SuspendForBody에 구현되어 있는 adapt() 함수 호출 → 최종적으로 Call<Repo> or Response<Repo> or Repo가 반환됨

마치며

클래스가 너무 많고 서로 이곳저곳에서 엮여있어서 분석하기 매우 까다로웠던 것 같습니다. 그래도 이전 글에서 맞춰지지 않았던 퍼즐이 얼추 맞춰진 것 같습니다.

추가로 RxJava 등 다양한 반환 타입을 지원하는 여러 어댑터가 있으니 참고하면 좋을 듯 합니다.(예시: https://github.com/square/retrofit/blob/trunk/retrofit-adapters/rxjava3/README.md, https://github.com/skydoves/retrofit-adapters)