티스토리 뷰
<주의> 해당 포스트는 Retrofit 라이브러리의 사용방법이 아닌 내부 코드를 파헤쳐놓은 글입니다.
흔히 Retrofit 라이브러리를 사용하기위해 Retrofit 객체를 초기화할 때 아래와 같이 사용한다.
fun getRetrofit(client: OkHttpClient): Retrofit {
return Retrofit.Builder()
.baseUrl(API_BASE_URL)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJava2CallAdapterFactory.create())
.client(client)
.build()
}
Builder Pattern을 사용하여 Retrofit 객체를 생성하게 되는데 이때 내부에서 어떤 동작들이 일어나는지 살펴보자
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;
}
/** Add converter factory for serialization and deserialization of objects. */
public Builder addConverterFactory(Converter.Factory factory) {
converterFactories.add(Objects.requireNonNull(factory, "factory == null"));
return this;
}
/**
* Add a call adapter factory for supporting service method return types other than {@link
* Call}.
*/
public Builder addCallAdapterFactory(CallAdapter.Factory factory) {
callAdapterFactories.add(Objects.requireNonNull(factory, "factory == null"));
return this;
}
/**
* Specify a custom call factory for creating {@link Call} instances.
*
* <p>Note: Calling {@link #client} automatically sets this value.
*/
public Builder callFactory(okhttp3.Call.Factory factory) {
this.callFactory = Objects.requireNonNull(factory, "factory == null");
return this;
}
코드를 살펴보면 알겠지만 별게없다. 그저 Retrofit 필드에 값을 세팅을 한다.
interface SwanApi {
@POST("api/model")
fun signUpModel(@Body bodyData: RequestModelSignUpData): Single<ResponseModelSignUpData>
}
// return SwanApi
fun create(service: Retrofit): SwanApi = service.create(SwanApi::class.java)
그다음 실제로 우리가 호출할 HTTP method를 가지고있는 Interface를 만들고 Retrofit의 create 함수를 통해 Interface를 생성하게 된다. 이제 우리는 생성된 Interface를 통해 원하는 method를 호출만 하면 간단하게 HTTP 통신을 할 수 있다.
그럼 이제 Retrofit의 create 함수 내부에서 어떻게 HTTP 통신이 이뤄지는지 확인해보도록하자(여기서부터가 핵심이다)
먼저 create 함수 내부를 보면 아래처럼 구현 되어져있다.
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);
}
});
}
프록시(Proxy) 패턴을 사용하여 Proxy 객체를 생성하는 모습을 볼 수있다. 여기서 중요한 부분은 InvocationHnadler Interface가 가지고 있는 Invoke method 구현 부분인데 이 함수는 런타임에서 사용자가 HTTP method를 호출했을때 프록시 객체와 호출한 메서드에 대한 정보, 전달받은 파라미터 3개가 인자 값으로 들어오며 실행이 된다.
실행한 메서드가 Object class 라면 메서드를 바로 실행해주고 아니라면 메서드가 defualt 메서드인지에 따라서 결과가 달라진다.
defualt 메서드로 만들어놓지 않았기 때문에 loadServiceMethod 함수와 invoke 함수를 확인해보자
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;
}
먼저 loadServiceMethod 함수는 ServiceMethod를 리턴해주는데 캐시 데이터가 있는지 확인 후 없으면 parseAnnotations 메서드를 통해 새로 생성 후 캐싱 처리를 한다.
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 메서드 내부에서는 호출한 HTTP method의 리턴 타입을 체크하며 Validation 체크를 하고 다시 ServiceMethod를 상속받은 HttpServiceMethod의 parseAnnotations 메서드를 호출한다.
해당 코드는 길어서 중요한 부분만 확인해보자.
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
해당 부분에서 callAdapter 객체를 생성하게 되는데 내부 코드를 살펴보면 아래처럼 되어있다.
public CallAdapter<?, ?> nextCallAdapter(
@Nullable CallAdapter.Factory skipPast, Type returnType, Annotation[] annotations) {
Objects.requireNonNull(returnType, "returnType == null");
Objects.requireNonNull(annotations, "annotations == null");
int start = callAdapterFactories.indexOf(skipPast) + 1;
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
CallAdapter<?, ?> adapter = callAdapterFactories.get(i).get(returnType, annotations, this);
if (adapter != null) {
return adapter;
}
}
StringBuilder builder =
new StringBuilder("Could not locate call adapter for ").append(returnType).append(".\n");
if (skipPast != null) {
builder.append(" Skipped:");
for (int i = 0; i < start; i++) {
builder.append("\n * ").append(callAdapterFactories.get(i).getClass().getName());
}
builder.append('\n');
}
builder.append(" Tried:");
for (int i = start, count = callAdapterFactories.size(); i < count; i++) {
builder.append("\n * ").append(callAdapterFactories.get(i).getClass().getName());
}
throw new IllegalArgumentException(builder.toString());
}
callAdapterFactories 컬랙션 내부에서 i번째를 가져와 get 메서드를 호출하여 CallAdapter 객체를 리턴한다. callAdapterFactories 에는 Retrofit 객체를 생성할 때 넣었던 RxJava2CallAdapterFactory가 들어있다. 그럼 RxJava2CallAdapterFactory에서 구현한 get 메서드를 살펴보자.
@Override
public @Nullable CallAdapter<?, ?> get(
Type returnType, Annotation[] annotations, Retrofit retrofit) {
Class<?> rawType = getRawType(returnType);
if (rawType == Completable.class) {
// Completable is not parameterized (which is what the rest of this method deals with) so it
// can only be created with a single configuration.
return new RxJava2CallAdapter(
Void.class, scheduler, isAsync, false, true, false, false, false, true);
}
boolean isFlowable = rawType == Flowable.class;
boolean isSingle = rawType == Single.class;
boolean isMaybe = rawType == Maybe.class;
if (rawType != Observable.class && !isFlowable && !isSingle && !isMaybe) {
return null;
}
boolean isResult = false;
boolean isBody = false;
Type responseType;
if (!(returnType instanceof ParameterizedType)) {
String name =
isFlowable ? "Flowable" : isSingle ? "Single" : isMaybe ? "Maybe" : "Observable";
throw new IllegalStateException(
name
+ " return type must be parameterized"
+ " as "
+ name
+ "<Foo> or "
+ name
+ "<? extends Foo>");
}
Type observableType = getParameterUpperBound(0, (ParameterizedType) returnType);
Class<?> rawObservableType = getRawType(observableType);
if (rawObservableType == Response.class) {
if (!(observableType instanceof ParameterizedType)) {
throw new IllegalStateException(
"Response must be parameterized" + " as Response<Foo> or Response<? extends Foo>");
}
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
} else if (rawObservableType == Result.class) {
if (!(observableType instanceof ParameterizedType)) {
throw new IllegalStateException(
"Result must be parameterized" + " as Result<Foo> or Result<? extends Foo>");
}
responseType = getParameterUpperBound(0, (ParameterizedType) observableType);
isResult = true;
} else {
responseType = observableType;
isBody = true;
}
return new RxJava2CallAdapter(
responseType, scheduler, isAsync, isResult, isBody, isFlowable, isSingle, isMaybe, false);
}
RxJava에서 사용하면 알 수 있는 익숙한 단어들이 보인다. Single, Maybe, Completable 등등 실행한 HTTP method 리턴 타입에 따라
RxJava2CallAdapter class의 필드값을 셋팅하고 RxJava2CallAdapter 객체를 리턴한다. 그리고 Rx에서 사용하는 생성자 타입이 아닐 경우에는 null을 리턴하게 되는데 nextCallAdapter의 for문으로 돌아가서 다음번째에 있는 callAdapter를 가져온다.
다시 parseAnnotations 메서드의 다른 부분을 보자.
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
Converter<ResponseBody, ?> converter =
converterFactories.get(i).responseBodyConverter(type, annotations, this);
if (converter != null) {
//noinspection unchecked
return (Converter<ResponseBody, T>) converter;
}
위에 callAdapter를 생성한 부분과 거의 동일하다. 여기도 처음에 설정했던 GsonConverterFactory를 리스트에서 꺼내어 가져오고 있다. 내부를 쭉쭉 들어가 보면 callAdapter에서 했던 것과 동일하게 가져오고 있고 responseBodyConverter 메서드를 통해 생성한 converter를 리턴하고 있다.
여기까지 Retrofit 객체에 설정했던 값들을 가져오고 아래 코드는 해당 값들로 HttpServiceMethod 객체를 다시 생성하는 것을 볼 수 있다.
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);
}
HTTP method가 suspend 함수가 아니면 CallAdapter 객체를 생성하게 된다 이렇게 생성한 후에 invoke 메서드를 호출하고 그 내부에선 구현되지 않은 adapt를 호출하고 있다.
@Override
final @Nullable ReturnT invoke(Object[] args) {
Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
return adapt(call, args);
}
protected abstract @Nullable ReturnT adapt(Call<ResponseT> call, Object[] args);
그럼 다시 CallAdapter에서 구현한 adapt 함수를 보면 아래와 같다.
@Override
protected ReturnT adapt(Call<ResponseT> call, Object[] args) {
return callAdapter.adapt(call);
}
callAdapter의 adapt를 호출하고 있는데 이때 callAdapter는 HttpServiceMethod 객체를 생성할 때 Retrofit 객체에서 가져온 RxJava2CallAdapter가 된다. 그럼 RxJava2 CallAdapter의 adapt 메서드를 살펴보자.
@Override
public Object adapt(Call<R> call) {
Observable<Response<R>> responseObservable =
isAsync ? new CallEnqueueObservable<>(call) : new CallExecuteObservable<>(call);
Observable<?> observable;
if (isResult) {
observable = new ResultObservable<>(responseObservable);
} else if (isBody) {
observable = new BodyObservable<>(responseObservable);
} else {
observable = responseObservable;
}
if (scheduler != null) {
observable = observable.subscribeOn(scheduler);
}
if (isFlowable) {
return observable.toFlowable(BackpressureStrategy.LATEST);
}
if (isSingle) {
return observable.singleOrError();
}
if (isMaybe) {
return observable.singleElement();
}
if (isCompletable) {
return observable.ignoreElements();
}
return RxJavaPlugins.onAssembly(observable);
}
Okhttp를 사용해봤으면 유추가 가능한 CallEnqueueObservable, CallExecuteObservable 두 개를 볼 수 있다. 코드 그대로 Execute는 동기, Enqueue 비동기 실행이다. CallExecuteObservable 내부를 한번 살펴보자.
final class CallExecuteObservable<T> extends Observable<Response<T>> {
private final Call<T> originalCall;
CallExecuteObservable(Call<T> originalCall) {
this.originalCall = originalCall;
}
@Override
protected void subscribeActual(Observer<? super Response<T>> observer) {
// Since Call is a one-shot type, clone it for each new observer.
Call<T> call = originalCall.clone();
CallDisposable disposable = new CallDisposable(call);
observer.onSubscribe(disposable);
if (disposable.isDisposed()) {
return;
}
boolean terminated = false;
try {
Response<T> response = call.execute();
if (!disposable.isDisposed()) {
observer.onNext(response);
}
if (!disposable.isDisposed()) {
terminated = true;
observer.onComplete();
}
} catch (Throwable t) {
Exceptions.throwIfFatal(t);
if (terminated) {
RxJavaPlugins.onError(t);
} else if (!disposable.isDisposed()) {
try {
observer.onError(t);
} catch (Throwable inner) {
Exceptions.throwIfFatal(inner);
RxJavaPlugins.onError(new CompositeException(t, inner));
}
}
}
}
private static final class CallDisposable implements Disposable {
private final Call<?> call;
private volatile boolean disposed;
CallDisposable(Call<?> call) {
this.call = call;
}
@Override
public void dispose() {
disposed = true;
call.cancel();
}
@Override
public boolean isDisposed() {
return disposed;
}
}
}
Observable을 상속받고 있는데 subscribeActual 메서드 내부에서 excute()를 호출하는 것을 볼 수 있다.
이 말은 즉 subscribeActual이 호출되는 시점(viewModel에서 observable을 subscribe 할 때)에 HTTP 통신이 시작된다는 것을 알 수 있다. CallEnqueueObservable 도 이와 비슷한 형태로 되어있다.
Response<T> parseResponse(okhttp3.Response rawResponse) throws IOException {
ResponseBody rawBody = rawResponse.body();
// Remove the body's source (the only stateful object) so we can pass the response along.
rawResponse =
rawResponse
.newBuilder()
.body(new NoContentResponseBody(rawBody.contentType(), rawBody.contentLength()))
.build();
int code = rawResponse.code();
if (code < 200 || code >= 300) {
try {
// Buffer the entire body to avoid future I/O.
ResponseBody bufferedBody = Utils.buffer(rawBody);
return Response.error(bufferedBody, rawResponse);
} finally {
rawBody.close();
}
}
if (code == 204 || code == 205) {
rawBody.close();
return Response.success(null, rawResponse);
}
ExceptionCatchingResponseBody catchingBody = new ExceptionCatchingResponseBody(rawBody);
try {
T body = responseConverter.convert(catchingBody);
return Response.success(body, rawResponse);
} catch (RuntimeException e) {
// If the underlying source threw an exception, propagate that rather than indicating it was
// a runtime exception.
catchingBody.throwIfCaught();
throw e;
}
}
excute() 함수 내부에서는 parseResponse() 함수를 호출하는데 이때 처음 세팅했던 GsonConverter로 인해 서버에서 받은 ResponseBody 데이터를 역직렬화 하여 원하는 타입을 응답 값으로 받을 수 있다.
글이 너무 길어져서 다음 글에 마저 정리를 해야 할 것 같다.
이번 글에서는 RxJava + Retrofit을 사용할 때 어느 시점에 HTTP 통신이 시작되고 어떻게 Single, Maybe 등 Rx 생성자들로 래핑 한 데이터로 응답을 받을 수 있는지, GsonConvert가 언제 사용되는지 등 여러 가지를 알 수 있었다.
'개발노트 > 안드로이드' 카테고리의 다른 글
[안드로이드] 에서 SOLID원칙 지키기 (0) | 2022.05.09 |
---|---|
[안드로이드] TDD란 ? (1) | 2022.01.11 |
[안드로이드] Kotlin Scope Function(apply, with, let, also, run) (1) | 2021.08.09 |
[안드로이드] 이미지 로딩 라이브러리 Glide 사용하기 (0) | 2020.09.11 |
[안드로이드] 안드로이드x 마이그레이션 (0) | 2019.11.04 |
- Total
- Today
- Yesterday
- 안드로이드 TDD
- 중소기업전세자금대출 꿀팁
- 코틀린 let
- 이미지라이브러리
- 청년전세자금대출
- 제주도 커플사진
- 제주도 여행
- 샤이니길
- 제주도 흑돼지고기
- 제주도 맛집
- 중소기업전세자금대출
- 제주도
- 올데이롱 후기
- 중소기업청년전세자금대출
- 화정
- 미아사거리
- 한성컴퓨터 올데이롱
- 전세방
- 제주도 샤이니길
- 한성 올데이롱
- 코틀린 also
- 화정 맛집
- 안드로이드
- 코틀린 apply
- 코틀린 run
- 한성노트북추천
- ScopeFunction
- 코틀린 with
- 맛집
- 제주도 흑돈가
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |