사용
개발자는 Interface에 추상적인 api 함수들을 Retrofit Annotaion과 함께 정의한다.
interface NaverApi {
@GET(RetrofitService.NEWS_END_POINT)
suspend fun getSearchNews(
@Header("X-Naver-Client-Id") clientId : String = BuildConfig.NAVER_CLIENT_ID,
@Header("X-Naver-Client-Secret") clientPw : String = BuildConfig.NAVER_CLIENT_SECRET,
@Query("query") query : String,
@Query("sort") sort : String,
): Call<NewsResponse>
}
Retrofit Builder에 Url, Client, Convert 등 등을 설정해준다.
private val retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(provideHttpClient())
.addConverterFactory(createGsonConverterFactory())
.build()
val naverService = retrofit.create(NaverApi::class.java)
그리고 create 함수로 정의한 API Interface를 넘긴다.
개발자는 API Service의 구현체를 직접 만들지 않는다.
Interface를 정의하기만 하고 사용할 때에는 Retrofit.create을 이용해서 Api Service의 구현체를 받아온다.
이 받아오는 구현체의 내부 동작을 이해하려 한다.
Retrofit.create 함수의 매개변수를 보면 Class<T!> 라는 Java Class를 받는다.
여기에 리플렉션을 이용해 Interface::class.java로 java클래스로 바꾸어준다.
리플렉션(::)은 런타임의 클래스를 참조하는 기술
KClass 참조하여 java Class로 변환해주는 확장 프로퍼티를 사용해 자바 클래스로 넘겨준다.
https://kotlinlang.org/api/latest/jvm/stdlib/kotlin.jvm/java.html
Retrofit.create
create 함수의 내부, API endpoints를 정의한 service interface의 구현체를 만들어준다
public <T> T create(final Class<T> service) { // 넘겨준 NaverApi Interface의 java Class
validateServiceInterface(service);
return (T) // T (NaverApi)로 타입 변환 후 Proxy 반환
Proxy.newProxyInstance( // Proxy 인스턴스 생성
service.getClassLoader(), // NaverApi Class가 로드된 클래스로더 넘겨주기
new Class<?>[] {service}, // NaverApi Class 넘겨주기
new InvocationHandler() { // InvocationHandler 정의하여 넘겨주기
...
});
}
validateServiceInterface(service)
전달 받은 인자가 Interface인지 확인하고, 아닐 시 IllegalArgumentException을 발생
private void validateServiceInterface(Class<?> service) {
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
Deque<Class<?>> check = new ArrayDeque<>(1);
check.add(service);
...
코드에 Proxy.newProxyInstance를 사용하는데 일단 Proxy에 대해 알아본다.
Proxy
타겟이 되는 클래스의 기능을 확장하거나, 클래스에 대한 접근을 제어하기 위해 사용하는 클래스이다.
아래와 같은 Hello Interface가 있고, 이것을 구현한 HelloTarget 클래스가 있을때
interface Hello {
fun sayHello(name: String): String
fun sayHi(name: String): String
}
class HelloTarget : Hello {
override fun sayHello(name: String): String {
return "Hello $name"
}
override fun sayHi(name: String): String {
return "Hi $name"
}
}
Proxy를 이용하면 Hello Interface의 메서드를 변경하지 않고 부가 기능을 추가 할 수 있다.
HellowUppercase라는 Hello의 구현체를 따로 만들고, HelloTarget의 객체를 선언해준다.
타겟의 기능을 가져와 수행하고 결과를 다시 대문자로 변환한다.
Hello의 메서드를 호출하면 HelloTarget이 위임받아 원래의 기능을 수행하고 uppercase() 메소드를 통해 대문자로 변환하는 기능을 추가한 것이다.
class HelloUpperCase: Hello{
private val helloTarget = HelloTarget()
override fun sayHello(name: String): String {
return helloTarget.sayHello(name).uppercase(Locale.getDefault())
}
override fun sayHi(name: String): String {
return helloTarget.sayHi(name).uppercase(Locale.getDefault())
}
}
정리하면 Proxy를 사용하면 타겟의 코드 수정 없이 기능을 확장하거나 부가 기능을 추가 할 수 있다.
Dynamin Proxy
하지만 위와 같은 Proxy는 Interface를 직접 구현해야하고, 중복이 발생하는 단점이 있다.
두 함수 중 하나만 수정 하고 싶어도, 모든 메서드를 override해야하고, 중복된 코드가 많아진다.
Dynamic Proxy는 일일이 프록시를 생성하지 않고 런타임 시점에 Interface를 구현하는 클래스 또는 Instance를 만들어주는 것을 뜻한다.
그것을 수행하는게 Proxy의 newProxyInstance() 이다.
Proxy.newProxyInstance
Java의 Refelction API에서 제공해주는 Dynamic Proxy 클래스를 만드는 함수
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
Objects.requireNonNull(h);
final Class<?>[] intfs = interfaces.clone(); // java class Type의 Interface를 클론
Class<?> cl = getProxyClass0(loader, intfs); // getProxyClass : Proxy 클래스 return 받음
final Constructor<?> cons = cl.getConstructor(constructorParams); // cl에 대한 생성자 받아오기
final InvocationHandler ih = h; 전달 받은 InvocationHandler 할당
// reflected 된 객체에 접근 가능하게 설정
if (!Modifier.isPublic(cl.getModifiers())) {
cons.setAccessible(true);
}
// 생성자로 위에 정의한 InvocationHandler를 가진 인스턴스 반환
return cons.newInstance(new Object[]{h});
}
넘겨준 파라미터 loader, interfaces, InvocationHandler를 받아서 동적으로 인스턴스 생성
- ClassLoader loader : Proxy를 만들 클래스 로더
- Class<?>[] interfaces : 어떤 Interface의 Proxy를 만들 것인지 명시
- InvocationHandler h : InvocationHandler Interface의 구현체
- return 값 : 동적으로 만든 Profxy 객체
ClassLoader를 참조, 런타임에 해당하는 Interface의 구현체 (Proxy Instance)를 만든다.
인자 InvocationHandler
invoke()라는 메소드 하나만 가지고 있는 인터페이스
동적으로 생성될 Proxy의 메소드가 호출되면 Invoke를 거치게 되고, 여기서 메소드의 기능을 확장할 수 있다.
platform의 method가 defaultMethod라면 invokeDefaultMethod를 반환, 아니라면 loadServiceMethod.invoke를 반환
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);
}
});
Platform
단어 그대로 플랫폼을 지칭
Android Platform 레벨에 접근하기 위한 클래스
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);
}
Default Method
자바 8부터 지원한 public non-abstract한 instance 메소드
인터페이스에 정의되어있는 body가 있는 non-static 메소드
@IgnoreJRERequirement // Only called on API 24+.
boolean isDefaultMethod(Method method) {
return hasJava8Types && method.isDefault();
}
/**
* Returns {@code true} if this method is a default
* method; returns {@code false} otherwise.
*
* A default method is a public non-abstract instance method, that
* is, a non-static method with a body, declared in an interface
* type.
*
* @return true if and only if this method is a default
* method as defined by the Java Language Specification.
* @since 1.8
*/
public boolean isDefault() {
// Android-changed: isDefault() implemented using Executable.
return super.isDefaultMethodInternal();
}
이렇게 해서 위의 platform은 Android가 담기고, 개발자가 선언해준 인터페이스의 함수는 default가 아니기에platform.isDefaultMethod가 false가 나와 아래의 loadServiceMethod가 실행
즉, Dynamic Proxy를 통해 Retrofit을 위해 선언해준 interface에 대해서 동적으로 클래스를 생성해주는 역할을 하는 것이 Dynamic Proxy
Retrofit.loadServiceMethod
serviceMethodCache는 Retrofit 클래스 최상단에 선언되어 있는 Map객체 프로퍼티이다.
Retrofit 객체는 map을 이용해 메소드들을 캐싱 처리하여 loadServiceMethod 호출 시 캐시에 해당 메서드가 있다면 이를 사용하고, 없으면 parseAnnotations로 메소드를 만들어 set해주고 있다.
처음 실행시 비어있기 때문에 null을 반환하여 아래 synchronized 블록을 실행합니다.
그 안에서 값이 저장되지 않은 채로 get을 하기때문에 result는 첫 값으로 null이며, 후에 if문을 실행
즉, 사용할 때 API 첫 번째 호출에는 새 메서드를 생성, 다음 호출부터는 캐싱된 메서드를 가져온다.
public final class Retrofit {
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;
}
ServiceMethod.parseAnnotations
requestFactory를 이용해 (PUT, POST, GET 등)어노테이션을 수집하고, 이를 HttpServiceMethod.parseAnnotations에 넘겨준다.
HttpServiceMethod.parseAnnotations에선 최종적으로 메서드의 Response, Return 타입들을 정의한다.
abstract class ServiceMethod<T> {
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);
}
abstract @Nullable T invoke(Object[] args);
}
RequestFactory.parseAnnotations
여기서 실제로 지정해준 Annotation들을 파싱한다.
private void parseMethodAnnotation(Annotation annotation) {
if (annotation instanceof DELETE) {
parseHttpMethodAndPath("DELETE", ((DELETE) annotation).value(), false);
} else if (annotation instanceof GET) {
parseHttpMethodAndPath("GET", ((GET) annotation).value(), false);
} else if (annotation instanceof HEAD) {
parseHttpMethodAndPath("HEAD", ((HEAD) annotation).value(), false);
} else if (annotation instanceof PATCH) {
parseHttpMethodAndPath("PATCH", ((PATCH) annotation).value(), true);
} else if (annotation instanceof POST) {
parseHttpMethodAndPath("POST", ((POST) annotation).value(), true);
} else if (annotation instanceof PUT) {
parseHttpMethodAndPath("PUT", ((PUT) annotation).value(), true);
} else if (annotation instanceof OPTIONS) {
parseHttpMethodAndPath("OPTIONS", ((OPTIONS) annotation).value(), false);
} else if (annotation instanceof HTTP) {
HTTP http = (HTTP) annotation;
parseHttpMethodAndPath(http.method(), http.path(), http.hasBody());
} else if (annotation instanceof retrofit2.http.Headers) {
String[] headersToParse = ((retrofit2.http.Headers) annotation).value();
if (headersToParse.length == 0) {
throw methodError(method, "@Headers annotation is empty.");
}
headers = parseHeaders(headersToParse);
} else if (annotation instanceof Multipart) {
if (isFormEncoded) {
throw methodError(method, "Only one encoding annotation is allowed.");
}
isMultipart = true;
} else if (annotation instanceof FormUrlEncoded) {
if (isMultipart) {
throw methodError(method, "Only one encoding annotation is allowed.");
}
isFormEncoded = true;
}
}
HttpServiceMethod.parseAnnotations
중요한 변수 isKotlinSuspendFunction, 해당 함수가 suspend function 인지 확인하는 변수
이 suspendFunction 관련 변수들과 처리는 다른 글에서 다룰 것이다.
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;
...
함수 코드 아래에 보면 suspend function이 아니라면 CallAdapted를 생성하여 리턴하고, Response 객체를 리턴값으로 가진다면 SuspendForResponse를, Body가 필요하다면 SuspendForBody를 생성하여 리턴한다.
if (!isKotlinSuspendFunction) {
return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
} else if (continuationWantsResponse) {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForResponse<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
} else {
//noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
return (HttpServiceMethod<ResponseT, ReturnT>)
new SuspendForBody<>(
requestFactory,
callFactory,
responseConverter,
(CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
continuationBodyNullable);
}
이 모든 정보들을 가지고 Retrofit.loadServiceMethod로 돌아가 result에 SuspendForResponse를 저장하고, method 정보와 함께 serviceMethodCache에 저장한다.
즉, serviceMethodCache에는 메소드 정보, 그리고 실행에 대한 함수에 대한 정보(SuspendForResponse, callAdapter, callFactory, requestFactory, responseConverter)를 가지고 있다.
내부 동작 정리
- Retrofit.create API 지정
- Proxy.newProxyInstance 로 동적 인스턴스 생성
- 인자의 InvocationHandler 에서 메소드의 기능을 확장
- 플랫폼 확인
- default 메소드가 아니면 Retrofit.loadServiceMethod 호출
- 인자의 InvocationHandler 에서 메소드의 기능을 확장
- Proxy.newProxyInstance 로 동적 인스턴스 생성
- Retrofit.loadServiceMethod 호출
- map을 이용해 메소드들을 캐싱 처리 (첫 호출에만 새 메서드 생성)
- 메서드 없을시 ServiceMethod.parseAnnotations 호출
- ServiceMethod.parseAnnotations
- RequestFactory.parseAnnotations
- 실제로 Annotion들을 파싱
- HttpServiceMethod.parseAnnotations
- 최종적으로 파싱한 RequestFactory와 메서드로 Response, Return 타입들을 정의
- RequestFactory.parseAnnotations
- ServiceMethod.parseAnnotations
코드에서 Dynamic Proxy 클래스를 생성하고 invocationHandler를 정의하는 모습을 볼 수 있었다.
만약 클래식한 Proxy 패턴이라면 명시적으로 프록시 클래스를 정의해야 한다.
Proxy 패턴을 사용한다면 각각의 API Interface마다 Proxy 클래스를 만들어야 하고, 구현체가 필요하기 때문에 Interface의 함수들을 일일이 정의해준 다음 Proxy 클래스에서 사용해야 한다.
이것은 코드가 길어지며 반복되어 구현과 관리도 힘들어진다.
API Interface에 정의하는 함수들은 웬만해선 모두 API Call을 위한 API Endpoints 함수들이고 각각의 Interface 구현체들을 구현할 필요 없다.
Annotation으로 각 함수들이 할 일을 알려주고 이를 런타임에 Reflection을 통해 얻어와서 비슷한(API 함수들) 함수들을 찍어낸다.
일종의 팩토리를 이용해 모든 API 함수에 대응하는 InvocationHandler의 invoke를 정의하면서, Dynamic Proxy를 만들어내게 된다.
이렇게 하여 개발자는 Interface의 구현체를 만들지 않고 지정만 해주어 사용할 수 있다.
이러한 솔루션이 들어간게 Retrofit.create() 함수이다.
Retrofit Interface
https://rlaudals2374.medium.com/retrofit의-구현체는-어디에-있을까-e04b7952c408
https://ongveloper.tistory.com/674?source=post_page-----964f4b5d0a5d--------------------------------
Proxy
https://live-everyday.tistory.com/216
https://live-everyday.tistory.com/217
'Android > Debugging' 카테고리의 다른 글
Retrofit 과 Coroutine 사용시 enequeue 내부적 구현 (0) | 2024.01.02 |
---|---|
RoomDB warning: Schema export directory is not provided (1) | 2023.11.29 |
Android Studio New LogCat (0) | 2023.02.27 |
Android 13에 따른 compileSdkVersion , targetSdkVersion 타겟 33 구글 정책 변경 (0) | 2022.12.23 |
Android GoogleMap Ensure that the "Google Maps Android API v2" is enabled. 구글맵 키 오류 (0) | 2022.08.01 |