본문 바로가기

안드로이드

[안드로이드] Retrofit 분석

 

안드로이드 개발에서 필수적으로 알아야 하는 Retrofit에 대해 알아보자.

 

우선 Retrofit이 무엇인지 알아야 한다.

 

Retrofit은 OkHttp를 기반으로 한 type-safe 한 HTTP client 오픈소스 라이브러리이다.

쉽게 말해서 안드로이드에서 클라이언트와 서버 간 HTTP 통신을 쉽게 해주는 라이브러리라고 생각하면 될 것 같다.

 

Retrofit 내부 코드에 나와있는 설명을 보면 아래와 같이 나와있다.

 

Retrofit adapts a Java interface to HTTP calls by using annotations on the declared methods to define how requests are made. Create instances using the builder and pass your interface to create to generate an implementation.

 

 

Retrofit은 우리가 작성한 메서드와 어노테이션을 통해 요청이 만들어지는 방법을 정의함으로써 인터페이스를 HTTP 호출에 적용하고,

Builder 패턴을 통해 Retrofit의 Instance를 만들고 우리가 작성한 인터페이스를 전달해서 구현을 생성한다는 것이다.

 

쉽다면 쉽고 어렵다면 어려울 수 있는 개념이지만 코드를 보면 좀 더 쉽게 이해할 수 있다.

 

아래 코드는 간단하게 Retrofit을 사용하는 과정이다.

 

 

// 작성한 메서드와 어노테이션을 통해 GET 요청임을 정의한 service interface
interface SearchBookApi {
    @GET("v3/search/book")
    fun searchBook(
        @Query("query") query: String,
    ): Call<SearchBookResponse>
}

// Builder 패턴으로 Retrofit 인스턴스 생성
val retrofit = Retrofit.Builder()
    .baseUrl("https://api.example.com/")
    .addConverterFactory(GsonConverterFactory.create())
    .build()
    
// create 메서드를 통해 전달받은 service interface API의 구현을 생성한다
val api = retrofit.create(SearchBookApi::class.java)

// 요청을 보내고 응답을 리턴한다
val response = api.searchBook("book").execute()

 

 

먼저 SearchBookApi 인터페이스를 통해 어떤 HTTP 요청을 할 것인지 작성한다.

여기서는 GET 요청을 한 것이며

보통 GET, POST, PUT, PATCH, DELETE 등의 요청 방식을 사용해 작성한다.

 

 

다음은 정의한 인터페이스를 Retrofit에 전달하기 위해

Retrofit 인스턴스를 생성해야 한다.

 

Builder 패턴을 사용해서 인스턴스를 만드는 이유는

Retrofit 클래스 내부 코드를 보면 알 수 있다.

 

Retrofit 생성자는 아래와 같이 외부에서 직접적으로 생성할 수 없게 되어있다.

 

 

// Retrofit.java
public final class Retrofit {
  private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();

  final okhttp3.Call.Factory callFactory;
  final HttpUrl baseUrl;
  final List<Converter.Factory> converterFactories;
  final List<CallAdapter.Factory> callAdapterFactories;
  final @Nullable Executor callbackExecutor;
  final boolean validateEagerly;

  Retrofit(
      okhttp3.Call.Factory callFactory,
      HttpUrl baseUrl,
      List<Converter.Factory> converterFactories,
      List<CallAdapter.Factory> callAdapterFactories,
      @Nullable Executor callbackExecutor,
      boolean validateEagerly) {
    this.callFactory = callFactory;
    this.baseUrl = baseUrl;
    this.converterFactories = converterFactories;
    this.callAdapterFactories = callAdapterFactories;
    this.callbackExecutor = callbackExecutor;
    this.validateEagerly = validateEagerly;
  }
  ...
}

 

 

이 생성자가 호출되는 곳은 Builder 클래스의 build 메서드를 호출할 때이다.

 

 

// Retrofit.java
public Retrofit build() {
    ...
    return new Retrofit(
        callFactory,
        baseUrl,
        unmodifiableList(converterFactories),
        unmodifiableList(callAdapterFactories),
        callbackExecutor,
        validateEagerly);
  }

 

 

이렇게 빌더 패턴으로 만드는 이유는

Retrofit 객체를 생성할 때 필요한 값과 optional 한 값들에 대해 쉽고 직관적으로 다루기 위함이다.

 

Builder 클래스를 만들어서 필요한 파라미터에 대한 값들을 선택적으로 추가하고 최종적으로 build 메서드를 통해 하나의 Retrofit 인스턴스를 만드는 것이다.

 

Retrofit 생성자에 필요한 값들을 보면

callFactory, baseUrl ... 등이 필요하다.

 

build 함수를 호출하기 전에 Builder에서 이 값들을 추가할 수 있는 메서드들을 살펴보자. 

 

 


 

 

clientcallFactory를 설정하는 메서드이다.

 

// The HTTP client used for requests
// convenience method for calling callFactory
public Builder client(OkHttpClient client) {
  return callFactory(Objects.requireNonNull(client, "client == null"));
}

// Specify a custom call factory for creating call instances
// Calling client automatically sets this value
public Builder callFactory(okhttp3.Call.Factory factory) {
  this.callFactory = Objects.requireNonNull(factory, "factory == null");
  return this;
}


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

 

client 메서드를 호출하면 callFactory를 호출하게 되고 Builder를 리턴한다.

두 메서드는 결국 Call 객체를 생성하는 역할이다.

 

두 메서드는 선택적 호출이다. 

build 메서드를 보면 callFactory가 null 일 경우 디폴트 값으로 OkHttpClient를 사용하기 때문이다.

 

 

다음은 base url을 설정하는 메서드이다.

 

 

public Builder baseUrl(URL baseUrl) {
  Objects.requireNonNull(baseUrl, "baseUrl == null");
  return baseUrl(HttpUrl.get(baseUrl.toString()));
}

public Builder baseUrl(String baseUrl) {
  Objects.requireNonNull(baseUrl, "baseUrl == null");
  return baseUrl(HttpUrl.get(baseUrl));
}

public Builder baseUrl(HttpUrl baseUrl) {
  Objects.requireNonNull(baseUrl, "baseUrl == null");
  List<String> pathSegments = baseUrl.pathSegments();
  if (!"".equals(pathSegments.get(pathSegments.size() - 1))) {
    throw new IllegalArgumentException("baseUrl must end in /: " + baseUrl);
  }
  this.baseUrl = baseUrl;
  return this;
}


public Retrofit build() {
  if (baseUrl == null) {
    throw new IllegalStateException("Base URL required.");
  }
}

 

Retrofit 객체가 사용할 기본 url을 설정해주어야 한다. 설정된 기본 url을 바탕으로 모든 API 요청을 한다.

위 2개의 메서드는 모두 HttpUrl을 사용하는 baseUrl 메서드를 호출한다.

 

baseUrl을 설정할 때에는 주의할 점이 있다.

baseUrl은 항상 "/" 로 끝나야 한다. 아닐 경우 IllegalArgumentException 예외가 발생한다.

또한, baseUrl은 항상 완전한 URL 형식이어야 한다.

 

baseUrl을 설정하는 메서드는 필수적으로 호출해주어야 한다.

build 메서드를 보면 baseUrl이 null 일 경우 예외가 발생하기 때문이다.

 

 

다음은 callbackExecutor를 설정하는 메서드이다.

 

 

public Builder callbackExecutor(Executor executor) {
    this.callbackExecutor = Objects.requireNonNull(executor, "executor == null");
    return this;
}


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

 

callbackExecutor는 retrofit에서 비동기 작업에 대한 스레드를 지정한다.

 

안드로이드에서 Retrofit은 기본적으로 callback을 메인 스레드에서 처리한다. 만약 다른 스레드에서 callback을 처리하고 싶다면 커스텀하게 지정해 주면 된다.

 

이 메서드는 선택적으로 호출하면 된다.

build 메서드를 보면 callbackExecutor를 설정해주지 않은 경우 디폴트 callback executor를 설정해주기 때문이다.

 

 

 

다음은 converter factory를 설정하는 메서드이다.

 

 

// Add converter factory for serialization and deserialization of objects.
public Builder addConverterFactory(Converter.Factory factory) {
  converterFactories.add(Objects.requireNonNull(factory, "factory == null"));
  return this;
}


public Retrofit build() {
    // 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());
}

 

converter는 Retrofit을 통해 HTTP 통신을 할 때 주고받는 데이터의 변환(serialization, deserialization)을 위해 사용된다.

서버에서 받는 데이터를 객체로 변환하거나 객체를 서버로 전송할 때 사용된다.

데이터 형식에 따라 여러가지 converter를 추가해 주는 방식으로 사용할 수 있다.

 

만약 Json 형태의 데이터를 사용한다면 GsonConverterFactory를 추가해주면 된다.

 

이 메서드는 선택적으로 호출하면 된다.

build 메서드를 보면 default converter들을 추가해 주기 때문이다.

 

 

다음은 CallAdapterFactory를 설정하는 메서드이다.

 

 

// Add a call adapter factory for supporting service method return types other than Call
public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
  callAdapterFactories.add(Objects.requireNonNull(factory, "factory == null"));
  return this;
}


public Retrofit build() {
    // 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));
}

 

Retrofit은 기본적으로 HTTP 통신에 대한 callback을 Call 객체로 리턴한다.

 

Call Adapter Factory를 지정하면 callback에 대한 여러 가지 객체의 타입을 리턴할 수 있게 된다.

 

예를 들어, RxJava3를 사용한다면 RxJava3CallAdapterFactory를 지정해서 Call 타입의 객체를

RxJava3에서 제공하는 콜백 객체(Single, Maybe, Flowable, Observable 등)로 변환할 수 있다.

 

이 메서드는 선택적으로 호출하면 된다.

build 메서드를 보면 default call adapter factory들을 추가해 주기 때문이다.

 

 


 

 

지금까지는 Retrofit Builder를 통해 retrofit의 인스턴스를 만드는 것에 대한 내용이었다.

 

이제 정의한 service API 인터페이스에 대한 구현체를 retrofit 인스턴스의 create 메서드를 통해 얻을 수 있다.

 

// create 메서드를 통해 전달받은 service interface API의 구현을 생성한다
val api = retrofit.create(SearchBookApi::class.java)

 

create 메서드를 살펴보면 아래와 같다.

 

파라미터로 받아온 service는 우리가 정의한 service API 인터페이스이다.

 

 

// Retrofit.java
public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    return (T)
        Proxy.newProxyInstance(
            service.getClassLoader(),
            new Class<?>[] {service},
            new InvocationHandler() {
              private final Platform platform = Platform.get();
              private final Object[] emptyArgs = new Object[0];

              @Override
              public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
                  throws Throwable {
                // If the method is a method from Object then defer to normal invocation.
                if (method.getDeclaringClass() == Object.class) {
                  return method.invoke(this, args);
                }
                args = args != null ? args : emptyArgs;
                return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });
  }

 

 

먼저 validateServiceInterface 함수를 통해 api service가 인터페이스인지 아닌지 판별하고 아닐 경우 IllegalArgumentException을 던진다.

 

// Retrofit.java
private void validateServiceInterface(Class<?> service) {
    if (!service.isInterface()) {
      throw new IllegalArgumentException("API declarations must be interfaces.");
    }

    ...

    if (validateEagerly) {
      Platform platform = Platform.get();
      for (Method method : service.getDeclaredMethods()) {
        if (!platform.isDefaultMethod(method) && !Modifier.isStatic(method.getModifiers())) {
          loadServiceMethod(method);
        }
      }
    }
  }

 

 

이후, dynamic Proxy 패턴을 통해 정의한 인터페이스에 대한 구현 객체(Proxy class)를 동적으로 생성해 런타임에 리턴한다.

newProxyInstance 메서드를 통해 Proxy 클래스의 인스턴스를 생성할 수 있다.

 

newProxyInstance 메서드의 파라미터를 보면

생성될 Proxy class가 정의할 classLoader, 구현할 interfaces, 호출을 처리할 handler가 있는 것을 알 수 있다.

 

// Proxy.java
public static Object newProxyInstance(
    ClassLoader loader,
    Class<?>[] interfaces,
    InvocationHandler h
)

 

 

InvocationHandler는 인터페이스로 아래와 같이 구현을 해주어야 한다.

invoke의 파라미터로 proxy 클래스 자신과, 구현할 인터페이스의 method, 호출할 method의 args가 필요하다.

 

new InvocationHandler() {
  private final Platform platform = Platform.get();
  private final Object[] emptyArgs = new Object[0];

  @Override
  public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
      throws Throwable {
    // If the method is a method from Object then defer to normal invocation.
    if (method.getDeclaringClass() == Object.class) {
      return method.invoke(this, args);
    }
    args = args != null ? args : emptyArgs;
    return platform.isDefaultMethod(method)
        ? platform.invokeDefaultMethod(method, service, proxy, args)
        : loadServiceMethod(method).invoke(args);
  }
});

 

 

이후 아래 코드와 같이 loadServiceMethod 함수에 method를 넣어주게 된다.

 

loadServiceMethod는 내부적으로 내가 정의한 annotation들을 파싱해서 저장한다.

코드에서 Map 타입의 cache 변수를 통해 메소드가 저장되어 있는지 판별한다.

 

// Retrofit.java

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();

ServiceMethod<?> loadServiceMethod(Method method) {
  ServiceMethod<?> result = serviceMethodCache.get(method);
  if (result != null) return result;

  synchronized (serviceMethodCache) {
    result = serviceMethodCache.get(method);
    if (result == null) {
      result = ServiceMethod.parseAnnotations(this, method);
      serviceMethodCache.put(method, result);
    }
  }
  return result;
}

 

 

만약 캐싱되어 있는 데이터가 없다면 SeviceMethod 클래스의 parseAnnotations 함수를 호출한다.

 

// ServiceMethod.java

static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

    Type returnType = method.getGenericReturnType();
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(
          method,
          "Method return type must not include a type variable or wildcard: %s",
          returnType);
    }
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }

    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
}

 

parseAnnotations 함수 내부적으로는

RequestFactory.parseAnnotations 함수를 통해 인터페이스 메서드의 어노테이션을 파싱해 HTTP Request에 대한 정보를 가져온다.

 

이후 HttpServiceMethod.parseAnnotations 함수를 호출해 리턴한다.

 

HttpServiceMethod는 우리가 요청한 HTTP request의 정보들을 가지고 있다.

 

 

결론적으로 Retrofit 인스턴스의 create 함수를 호출하면

Proxy 패턴을 통해 생성된 인터페이스의 구현체를 리턴받는다.

 

// 요청을 보내고 응답을 리턴한다
val response = api.searchBook("book").execute()

 

이후 인터페이스에서 정의한 함수를 호출하면 위에서 살펴본 InvocationHandler의 invoke 함수가 호출되는 구조이다.

 

 

이제 리턴 받은 Call 객체를 통해 동기, 비동기적 요청에 대한 함수를 원하는 대로 사용하면 된다.