개발/안드로이드

[안드로이드] Retrofit은 어떻게 동작할까?

창이2 2023. 2. 12. 23:27

안녕하세요.

이번 포스팅은 안드로이드의 Retrofit Library의 동작 방식에 대한 분석을 이야기해 보려고 합니다.

Retrofit을 사용할때 가끔씩

어떻게 인터페이스를 정의한것만으로 네트워크 통신이 가능하지?

라는 생각을 했습니다.

 

그래서 라이브러리를 분석하는 중

InvocationHandler와 newProxyInstance 를 보고 Dynamic Proxy 패턴에 대해 알게 되고 

이것이 Retrofit을 동작하는데 중요한 역할을 한다는 것을 알게 되었습니다.

일단 Retrofit의 동작원리를 이해하기 전 Dynamic Proxy 패턴에 대해 이해가 필요합니다.

잘 이해가 안가시면 아래 글을 참고해주세여!

 

https://dog-footprint.tistory.com/94

 

[자바 디자인패턴] 5. Dynamic Proxy 패턴

안녕하십니까아. 이번 포스팅은 Dynamic Proxy 패턴에 대해서 써보려고 합니다. Dynamic Proxy는 Proxy를 동적으로 생성하여 사용하는 패턴입니다. Dynamic Proxy를 사용하는 경우는 대게 제네릭한 행동을 할

dog-footprint.tistory.com

일단 Retrofit이 어떻게 생성되는지 부터 보겠습니다.

제너릭 타입 T 를 받고 있는데 이 T는 우리가 설정한 API Interface 입니다. 

Proxy.newProxyInstance

를 사용하여 Dynamic Proxy 패턴을 구현한 것을 알 수 있습니다.

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

 

첫번째로 create 대상이 인터페이스인지 아닌지를 체크합니다.

validateServiceInterface(service);

만약 인터페이스라면 프록시를 생성하고 아니라면 에러를 일으킵니다. 

 

 

Platform

이라는 객체가 있는데 내부 구현을 보니 Platform에 따라 메소드 호출을 다르게 하는 것을 알 수 있습니다.

Platform 을 구할때 아래와 같이 달빅머신이면 Android를 아니면 Platform 을 구하도록 되어있습니다.  

내부에 hasJava8Types를 갖고 있는데 이 여부에 따라 ConverFactory를 다르게 가져가는것으로 보입니다.

 

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

 

api를 호출하면 serviceMethodCache 에 해당 메소드정보를 가져오는데 캐싱된게 없다면 메소드를 파싱하여 캐시정보를 저장합니다.

 

Retrofit.java

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

 

이때 parseAnnotations 함수에서

private void parseMethodAnnotation(Annotation annotation)
private void parseHttpMethodAndPath(String httpMethod, String value, boolean hasBody)
private Headers parseHeaders(String[] headers)

 

등등 수행하여 해당 메소드 정보를 캐싱하게 됩니다.

 

이후

HttpServiceMethod의 invoke를 통해 호출을 하여 결과를 받습니다.

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

 

HttpServiceMethod의 invoke 메소드에 실질적으로 okhttpCall이 들어가는 것을 볼 수 있습니다.

HttpServiceMethod.java

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

 

부족한 부분에 대해 지적해주시면 감사하겠습니다!