-
Retrofit Github
-
Retrofit2
-
Retrofit2를 어떻게 사용해왔는가
-
1. Call 인터페이스
-
2. Response
-
Retrofit은 내부적으로 어떻게 동작할까?
-
Retrofit.create
-
프록시? 프록시 패턴이란
-
Retrofit에서의 프록시 패턴
-
내부 코드 살펴보기
-
Retrofit#loadServiceMethod
-
ServiceMethod#parseAnnotations
-
RequestFactory#parseAnnotations
-
HttpServiceMethod#parseAnnotations
-
코루틴과 함께 Retrofit이 어떻게 동작하는가
-
ServiceMethod#invoke와 코루틴 동작 흐름
-
SuspendForResponse#adapt() - Response를 리턴하는 suspend 함수 처리
-
KotlinExtensions.awaitResponse()의 동작 방식
-
SuspendForBody#adapt() - Body만 리턴하는 suspend 함수 처리
-
await() vs awaitNullable()
-
suspendAndThrow() – 코루틴 예외 강제 전파
-
요약
-
참고자료
참고 링크가 원문이다. 이번 포스팅은 사실상 내가 보기 좀 더 편하게 정리를 한 것 뿐이다.
잘못된 내용은 댓글로 남겨주시면 감사하겠습니다.
Retrofit Github
https://github.com/square/retrofit
GitHub - square/retrofit: A type-safe HTTP client for Android and the JVM
A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.
github.com
Retrofit2
Retrofit2는 서버와 클라이언트(Android) 간 HTTP 통신을 간편하게 처리하기 위해 Square사에서 개발한 네트워크 라이브러리이다. Type-Safe한 방식으로 API를 정의할 수 있으며, 내부적으로 OkHttp 클라이언트와 함께 동작한다.
Retrofit2를 어떻게 사용해왔는가
Retrofit2를 사용할 때는 반드시 API 인터페이스를 정의하여 사용한다. 일반적으로 아래와 같은 3가지 방식으로 호출 방식을 선택할 수 있다.
1. Call 인터페이스
가장 기본적인 사용 방식은 Call<T> 인터페이스를 사용하는 것이다. 이 인터페이스는 Call#execute 또는 Call#enqueue를 통해 요청을 보낼 수 있다.
internal interface ApiService {
// 1)
@GET("boxoffice/searchDailyBoxOfficeList.json")
fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): Call<KobisBoxOfficeResponse>
// 2)
@GET("boxoffice/searchDailyBoxOfficeList.json")
suspend fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): KobisBoxOfficeResponse
// 3)
@GET("boxoffice/searchDailyBoxOfficeList.json")
suspend fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): Response<KobisBoxOfficeResponse>
}
Call#execute
Call#execute는 서버에 동기적으로 요청을 보낸다. 안드로이드는 기본적으로 UI 스레드(메인 스레드)에서 동작하기 때문에, 이 방식으로 요청을 보내면 해당 스레드가 응답을 받을 때까지 블로킹된다. 이는 UI 차단을 발생시키며, NetworkOnMainThreadException 예외가 발생하므로 일반적인 안드로이드 개발에서는 지양해야 한다.
public interface Call<T> extends Cloneable {
/**
* Synchronously send the request and return its response.
*
* @throws IOException if a problem occurred talking to the server.
* @throws RuntimeException (and subclasses) if an unexpected error occurs creating the request or
* decoding the response.
*/
Response<T> execute() throws IOException;
...
}
// RemoteDataSource
fun getDailyBoxOffice(targetDate: String): List<DailyBoxOfficeModel> {
val response = kobisService.getDailyBoxOffice(targetDate = targetDate)
return response.execute().body()?.boxOfficeResponse?.dailyBoxOfficeList?.map {
it.toDataModel()
}.orEmpty()
}
Call#enqueue
Call#enqueue는 서버에 비동기적으로 요청을 보낸다. 비동기 처리이기 때문에 UI 차단이 발생하지 않는다. 콜백 인터페이스인 Callback<T>를 통해 성공 및 실패에 따른 분기 처리가 가능하다. 네트워크 오류나 예외 발생 시도 예외가 아닌 콜백 메서드에서 처리할 수 있다.
다만, 코루틴을 사용하는 환경에서는 Call#enqueue 대신 KotlinExtension.kt에 정의된 await() 확장함수를 사용하는 것이 더 자연스럽다. 콜백 기반 코드보다 직관적이며 순차적인 흐름으로 가독성이 뛰어나기 때문이다.
public interface Call<T> extends Cloneable {
/**
* Asynchronously send the request and notify {@code callback} of its response or if an error
* occurred talking to the server, creating the request, or processing the response.
*/
void enqueue(Callback<T> callback);
...
}
// RemoteDataSource
fun getDailyBoxOffice(targetDate: String): List<DailyBoxOfficeModel> {
val response = kobisService.getDailyBoxOffice(targetDate = targetDate)
response.enqueue(object : Callback<KobisBoxOfficeResponse> {
override fun onResponse(
call: Call<KobisBoxOfficeResponse>,
response: Response<KobisBoxOfficeResponse>,
) {
// 네트워크 연결 성공
}
override fun onFailure(call: Call<KobisBoxOfficeResponse>, t: Throwable) {
// 네트워크 연결 실패
}
})
}
2. Response
Retrofit2는 Response<T>를 통해 응답 결과를 전달한다. 이는 성공 여부를 판단하거나 응답 코드를 확인할 수 있는 메타 정보를 포함한다. 코루틴 환경에서 Response<T>를 사용하는 경우에는 enqueue와 같은 비동기 처리를 내부적으로 포함하고 있어, 더 간결한 방식으로 사용할 수 있다.
Retrofit은 내부적으로 어떻게 동작할까?

Retrofit.create
Retrofit의 핵심은 Retrofit#create 메서드를 통해 API 인터페이스의 구현체를 생성하는 부분이다. 이 구현체는 프록시 객체이며, 해당 프록시를 호출하면 내부적으로 InvocationHandler#invoke가 호출된다. 이 과정에서 메서드가 DefaultMethod인지 여부에 따라 분기된다.
@Module
@InstallIn(SingletonComponent::class)
internal class ServiceModule {
@Provides
@Singleton
fun provideRetrofit(
@DefaultOkHttpClient okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
): Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(converterFactory)
.build()
@Provides
@Singleton
fun provideApiService(
retrofit: Retrofit
): ApiService = retrofit.create()
// retrofit.create(ApiService::class.java)
}
inline fun <reified T> Retrofit.create(): T = create(T::class.java)
프록시? 프록시 패턴이란
프록시(Proxy) 패턴이란 어떤 객체에 대한 접근을 제어하거나 기능을 추가하기 위해 그 객체를 대신하는 대리 객체(Proxy 객체)를 두는 디자인 패턴이다. 즉, 원래 객체(Real Subject) 앞에 대리 객체(Proxy) 를 두고, 이 대리 객체가 클라이언트의 요청을 받아 내부적으로 실제 객체에 위임하거나 중간에서 가공하는 식으로 동작한다.
Retrofit에서의 프록시 패턴
Retrofit에서는 Retrofit.create(ApiService::class.java)를 호출하면 우리가 정의한 ApiService 인터페이스를 직접 구현한 클래스가 생성되지 않는다. 대신, 프록시 객체가 생성되어 내부적으로 InvocationHandler#invoke()를 통해 모든 메서드 호출이 처리된다.
- 프록시 객체는 Proxy.newProxyInstance()로 동적으로 생성된다.
- 이 프록시 객체는 API 메서드 호출을 가로채어 내부 로직(HTTP 요청 처리 등)을 수행한다.
- 즉, 우리가 정의한 인터페이스가 곧 요청 로직으로 연결되는 구조가 프록시 패턴을 통해 구현된다.
다시 돌아가서
내부 코드 살펴보기
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);
}
});
}
private void validateServiceInterface(Class<?> service) {
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
...
}
1. validateServiceInterface 확인
가장 먼저 validateServiceInterface 메서드를 통해 전달된 클래스가 인터페이스인지 검증한다. API 선언은 반드시 인터페이스 형태로만 가능하므로, 만약 클래스나 추상 클래스일 경우 IllegalArgumentException이 발생한다. 이 검증은 개발자의 선언 실수를 방지하기 위한 사전 처리이다.
2. 프록시 객체 생성과 프록시 패턴
Proxy.newProxyInstance를 통해 API 인터페이스의 프록시 인스턴스를 생성한다. 이 프록시는 동적으로 메서드를 구현하여 실제 네트워크 호출을 처리한다. 해당 방식은 프록시 패턴에 기반하며, 원본 객체 대신 중간 대리 객체를 통해 로직을 확장하거나 위임하는 데 사용된다.
Retrofit은 이 프록시를 이용해 API 인터페이스를 직접 구현하지 않아도 네트워크 요청이 가능하도록 구성한다. InvocationHandler를 구현한 익명 객체는 모든 메서드 호출을 invoke()로 가로채어 처리한다.
3. InvocationHandler#invoke 동작 방식
invoke()는 다음과 같은 흐름으로 동작한다:
- 호출된 메서드가 Object 클래스에서 정의된 기본 메서드(toString, equals, hashCode)일 경우, 원래 로직을 그대로 수행한다.
- 그렇지 않은 경우, 전달된 Method가 Default Method인지 여부를 platform.isDefaultMethod()를 통해 판단한다.
- Default Method라면 Java 8 이후 인터페이스에 정의된 기본 구현 메서드이므로, 해당 구현을 직접 실행한다.
- Default Method가 아닐 경우에는 loadServiceMethod()를 호출하여 Retrofit 내부 요청 로직을 수행한다.
4. Default Method란 무엇인가?
JavaDoc에 따르면 Default Method는 다음과 같은 조건을 가진다:
인터페이스에 선언된 public, non-abstract, non-static, instance 메서드로서, 구현부가 존재하는 메서드를 의미한다.
예를 들어, 다음과 같은 메서드는 Default Method이다:
interface Api {
default String hello() {
return "hi";
}
}
반면, 일반적인 Retrofit API 인터페이스는 대부분 다음과 같이 선언된다:
@GET("movie")
suspend fun getMovies(): Response<MovieResponse>
위처럼 구현부가 없는 메서드는 Default Method가 아니므로 isDefaultMethod()는 대부분 false를 반환하게 된다.
5. platform이란?
Platform 클래스는 Android, Java 등 환경에 따라 다르게 동작해야 하는 기능을 분리한 클래스이다. 내부 구현을 확인해보면 현재 플랫폼이 Android인지, Java 8인지 등을 판별하여 맞춤 로직을 실행하도록 설계되어 있다.
Retrofit에서는 Platform.get()을 통해 현재 플랫폼을 판별하고, Default Method를 지원하는지 등을 판단하는 데 사용된다.
6. 핵심은 loadServiceMethod().invoke(args)에서 시작된다
결과적으로 대부분의 Retrofit API 호출은 platform.isDefaultMethod()에서 false가 반환되므로 loadServiceMethod() → ServiceMethod.invoke() → HttpServiceMethod.adapt() → OkHttpCall.enqueue()로 이어지는 네트워크 호출의 전체 흐름이 이곳에서 시작된다.
Retrofit#loadServiceMethod
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는 API 인터페이스의 각 메서드를 파싱하여 ServiceMethod로 변환하는 역할을 수행한다. 이 메서드는 내부적으로 serviceMethodCache라는 캐시를 활용하여, 메서드 정보를 재사용한다.
캐시된 정보가 없다면 ServiceMethod#parseAnnotations를 호출하여 메서드의 어노테이션과 반환 타입 등을 파싱한 후 캐시에 저장한다.
ServiceMethod#parseAnnotations
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);
}
ServiceMethod#parseAnnotations는 API 메서드에 선언된 어노테이션(@GET, @POST 등)과 반환 타입을 검사한다. 유효하지 않은 어노테이션 조합이거나 반환 타입이 명확하지 않다면 예외를 발생시킨다.
이후 HttpServiceMethod#parseAnnotations를 호출하며, 여기서 코루틴 지원 여부(suspend 함수인지)와 반환 타입에 따른 로직 분기가 이루어진다.
RequestFactory#parseAnnotations
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}
RequestFactory(Builder builder) {
method = builder.method;
baseUrl = builder.retrofit.baseUrl;
httpMethod = builder.httpMethod;
relativeUrl = builder.relativeUrl;
headers = builder.headers;
contentType = builder.contentType;
hasBody = builder.hasBody;
isFormEncoded = builder.isFormEncoded;
isMultipart = builder.isMultipart;
parameterHandlers = builder.parameterHandlers;
isKotlinSuspendFunction = builder.isKotlinSuspendFunction;
}
RequestFactory build() {
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
if (httpMethod == null) {
throw methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
...
return new RequestFactory(this);
}
RequestFactory#parseAnnotations는 Retrofit 메서드의 요청 정보를 파싱하는 핵심 메서드이다. 어노테이션, 헤더, 쿼리 파라미터, 바디 타입, 멀티파트 여부 등 다양한 정보가 이곳에서 파싱된다.
빌더 패턴을 통해 구성된 RequestFactory는 이후 실제 HTTP 요청을 만들기 위한 기반 객체로 사용된다.
HttpServiceMethod#parseAnnotations
HttpServiceMethod#parseAnnotations는 Retrofit에서 선언한 API 메서드가 suspend 함수인지 여부와 반환 타입에 따라 어떤 방식으로 호출을 처리할지 분기하는 중요한 메서드이다. 메서드의 책임이 크고 코드 라인 수도 많은 만큼, 책임을 3단계로 나누어 순서대로 살펴본다.
1. suspend 함수 여부 판단 및 관련 변수 구성
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType =
Utils.getParameterLowerBound(
0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
} else {
// TODO figure out if type is nullable or not
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
// Find the entry for method
// Determine if return type is nullable or not
}
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
adapterType = method.getGenericReturnType();
}
...
}
위 코드에서는 isKotlinSuspendFunction 변수를 통해 메서드가 코틀린의 suspend 함수인지 확인한다. 이 값은 RequestFactory에서 Continuation 타입의 파라미터가 존재하는지를 검사해 결정된다.
private @Nullable ParameterHandler<?> parseParameter(
int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
ParameterHandler<?> result = null;
...
if (result == null) {
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
}
return result;
}
Continuation 객체는 코루틴의 실행 상태를 저장하고 재개하는 데 사용되며, suspend 함수의 마지막 파라미터로 추가된다. 따라서 이 객체가 있으면 해당 메서드는 suspend 함수로 간주된다.
이후 Response<T> 형태인지 확인하고, 응답이 Response 타입일 경우 내부 바디 타입을 추출하여 continuationWantsResponse 값을 true로 설정한다. 이렇게 구성된 정보는 이후 분기 처리에서 사용된다.
2. CallAdapter 생성 및 반환 타입 유효성 검사
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();
if (responseType == okhttp3.Response.class) {
throw methodError(
method,
"'"
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
// TODO support Unit for Kotlin?
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}
두 번째 단계에서는 CallAdapter를 생성한다. 이는 Retrofit에서 Call<T>, Deferred<T>, LiveData<T> 등 다양한 반환 타입을 처리할 수 있게 하는 어댑터 역할을 한다. 사용자가 별도로 등록한 CallAdapter.Factory가 없더라도 Retrofit은 내부적으로 기본 팩토리(DefaultCallAdapterFactory)를 사용한다.
이후 callAdapter.responseType()을 통해 실제 응답 타입을 추출하고, 몇 가지 유효성 검사를 수행한다. 예를 들어:
- okhttp3.Response는 사용할 수 없고, ResponseBody를 써야 한다.
- Response는 반드시 제네릭 타입을 포함해야 한다.
- HEAD 메서드는 반드시 Void 타입의 응답을 가져야 한다.
이러한 검사는 런타임 오류를 예방하기 위해 필수적이다.
3. 반환 타입에 따른 처리 분기
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
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);
}
마지막으로는 메서드의 타입에 따라 어떤 HttpServiceMethod 구현체를 사용할지를 결정한다.
- suspend 함수가 아닌 경우 → CallAdapted 클래스가 사용된다. 이는 전통적인 Call<T> 방식이다.
- suspend 함수이며 반환 타입이 Response<T>인 경우 → SuspendForResponse가 사용된다.
- suspend 함수이며 T만 반환할 경우 → SuspendForBody가 사용된다.
suspend 함수인 경우 내부적으로 KotlinExtensions.kt의 await() 계열 함수를 호출하게 되며, enqueue()를 사용하지 않아도 비동기 처리를 수행할 수 있게 된다.
코루틴과 함께 Retrofit이 어떻게 동작하는가
ServiceMethod#invoke와 코루틴 동작 흐름
이제 다시 Retrofit#create() 이후의 흐름을 살펴보면, API 인터페이스의 메서드를 호출하면 내부적으로 loadServiceMethod()가 실행되고, 그 결과로 생성된 ServiceMethod 인스턴스의 invoke()가 호출된다.
// ServiceMethod.java
abstract class ServiceMethod<T> {
...
abstract @Nullable T invoke(Object[] args);
}
// HttpServiceMethod.java
@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);
ServiceMethod#invoke()는 HttpServiceMethod의 구현체에서 오버라이드되며, 내부적으로 OkHttpCall을 생성한 뒤 adapt() 메서드를 호출한다. 이 adapt() 메서드는 추상 메서드로, 실제 네트워크 요청을 수행할 방식에 따라 CallAdapted, SuspendForResponse, SuspendForBody 중 하나에서 구현된다.
여기서는 suspend 가능한 메서드에서 사용되는 SuspendForResponse와 SuspendForBody의 동작 방식을 중점적으로 살펴본다.
SuspendForResponse#adapt() - Response를 리턴하는 suspend 함수 처리
static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
SuspendForResponse(...) {
...
}
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
try {
return KotlinExtensions.awaitResponse(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
SuspendForResponse는 Response<T> 전체를 반환하는 suspend 함수에 대한 처리 클래스이다. 내부적으로 awaitResponse()를 호출하여 네트워크 요청을 수행하고 결과를 Continuation에 전달한다.
KotlinExtensions.awaitResponse()의 동작 방식
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
이 함수는 suspendCancellableCoroutine을 사용하여 Retrofit의 Call을 코루틴으로 감싼다. 응답이 성공하면 resume(response)를 호출하고, 실패하면 resumeWithException()을 통해 예외를 Continuation으로 전달한다. 이 Continuation에는 API를 호출한 호출 지점의 컨텍스트 정보가 담겨 있으며, 이 지점이 Suspension Point가 된다.
즉, 예외가 발생하면 해당 지점으로 예외가 전달되어 자연스럽게 try-catch로 처리할 수 있게 된다.
SuspendForBody#adapt() - Body만 리턴하는 suspend 함수 처리
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
private final boolean isNullable;
SuspendForBody(...) {
...
}
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
이 클래스는 API 메서드가 T 형식의 바디만 반환하는 경우를 처리한다. 반환 타입이 Nullable인지 여부에 따라 await() 또는 awaitNullable()을 호출한다.
await() vs awaitNullable()
// Non-nullable body 처리
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name + '.' + method.name +
" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
Non-nullable 타입의 바디는 null이면 예외를 발생시킨다. 이는 명시적으로 API 반환 타입을 T로 지정했기 때문이다.
// Nullable body 처리
@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T?> {
override fun onResponse(call: Call<T?>, response: Response<T?>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T?>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
Nullable한 타입은 body가 null이더라도 예외가 발생하지 않는다. 반환 타입이 T?로 선언되었기 때문에 null 자체를 유효한 결과로 간주한다.
suspendAndThrow() – 코루틴 예외 강제 전파
internal suspend fun Exception.suspendAndThrow(): Nothing {
suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
Dispatchers.Default.dispatch(continuation.context, Runnable {
continuation.intercepted().resumeWithException(this@suspendAndThrow)
})
COROUTINE_SUSPENDED
}
}
suspendAndThrow()는 예외가 발생했을 때, 코루틴의 실행을 일시 중단(suspend)한 후 예외를 Suspension Point로 전달하는 역할을 한다. Java의 Proxy는 체크 예외를 직접 throw할 수 없기 때문에, 예외가 발생해도 바로 던지지 않고 이 방식으로 코루틴 컨텍스트로 넘긴다.
이 방식 덕분에 Retrofit은 suspend 메서드 내부에서 예외를 안전하게 throw할 수 있으며, 호출 측에서는 try-catch로 이를 자연스럽게 처리할 수 있게 된다.
요약
- ServiceMethod#invoke()는 Retrofit API 호출의 핵심 진입점이며, 내부적으로 OkHttpCall을 생성하고 adapt() 메서드로 요청을 처리한다.
- suspend 메서드에서는 SuspendForResponse 또는 SuspendForBody를 통해 적절한 방식으로 enqueue()를 호출한다.
- KotlinExtensions.kt의 await, awaitResponse, awaitNullable은 모두 suspendCancellableCoroutine을 사용하여 코루틴과 Retrofit을 자연스럽게 연결한다.
- 예외 처리 시에는 suspendAndThrow()를 통해 Suspension Point로 예외를 안전하게 전파한다.
참고자료
(Android) Retrofit2는 어떻게 동작하는가 — 1. 내부 코드 분석
Retrofit2 Deep Dive #1
medium.com
(Android) Retrofit2는 어떻게 동작하는가 — 2. IO Dispatcher는 필요한가
Retrofit2 Deep Dive #2
medium.com
(Android) Retrofit2는 어떻게 동작하는가 — 3. OkHttp3의스레드 관리
Retrofit2 Deep Dive #3
medium.com
'Android > Study' 카테고리의 다른 글
[Android] Gson vs Moshi vs kotlinx.serialization Deep Dive (0) | 2025.03.26 |
---|---|
[Android] 코루틴 정리 feat. suspend 키워드까지 (0) | 2025.03.19 |
[Android] ViewModel이란 무엇이고 그리고 LifeCycle 까지 (0) | 2025.03.05 |
[Android] 인텐트(Intent) 및 인텐트 필터(Intent Filter) (0) | 2025.02.20 |
[Android] Jetpack Compose의 Recomposition 정리 (0) | 2025.02.20 |
참고 링크가 원문이다. 이번 포스팅은 사실상 내가 보기 좀 더 편하게 정리를 한 것 뿐이다.
잘못된 내용은 댓글로 남겨주시면 감사하겠습니다.
Retrofit Github
https://github.com/square/retrofit
GitHub - square/retrofit: A type-safe HTTP client for Android and the JVM
A type-safe HTTP client for Android and the JVM. Contribute to square/retrofit development by creating an account on GitHub.
github.com
Retrofit2
Retrofit2는 서버와 클라이언트(Android) 간 HTTP 통신을 간편하게 처리하기 위해 Square사에서 개발한 네트워크 라이브러리이다. Type-Safe한 방식으로 API를 정의할 수 있으며, 내부적으로 OkHttp 클라이언트와 함께 동작한다.
Retrofit2를 어떻게 사용해왔는가
Retrofit2를 사용할 때는 반드시 API 인터페이스를 정의하여 사용한다. 일반적으로 아래와 같은 3가지 방식으로 호출 방식을 선택할 수 있다.
1. Call 인터페이스
가장 기본적인 사용 방식은 Call<T> 인터페이스를 사용하는 것이다. 이 인터페이스는 Call#execute 또는 Call#enqueue를 통해 요청을 보낼 수 있다.
internal interface ApiService {
// 1)
@GET("boxoffice/searchDailyBoxOfficeList.json")
fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): Call<KobisBoxOfficeResponse>
// 2)
@GET("boxoffice/searchDailyBoxOfficeList.json")
suspend fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): KobisBoxOfficeResponse
// 3)
@GET("boxoffice/searchDailyBoxOfficeList.json")
suspend fun getDailyBoxOffice(
@Query("key") apiKey: String = BuildConfig.KOBIS_API_KEY,
@Query("targetDt") targetDate: String,
): Response<KobisBoxOfficeResponse>
}
Call#execute
Call#execute는 서버에 동기적으로 요청을 보낸다. 안드로이드는 기본적으로 UI 스레드(메인 스레드)에서 동작하기 때문에, 이 방식으로 요청을 보내면 해당 스레드가 응답을 받을 때까지 블로킹된다. 이는 UI 차단을 발생시키며, NetworkOnMainThreadException 예외가 발생하므로 일반적인 안드로이드 개발에서는 지양해야 한다.
public interface Call<T> extends Cloneable {
/**
* Synchronously send the request and return its response.
*
* @throws IOException if a problem occurred talking to the server.
* @throws RuntimeException (and subclasses) if an unexpected error occurs creating the request or
* decoding the response.
*/
Response<T> execute() throws IOException;
...
}
// RemoteDataSource
fun getDailyBoxOffice(targetDate: String): List<DailyBoxOfficeModel> {
val response = kobisService.getDailyBoxOffice(targetDate = targetDate)
return response.execute().body()?.boxOfficeResponse?.dailyBoxOfficeList?.map {
it.toDataModel()
}.orEmpty()
}
Call#enqueue
Call#enqueue는 서버에 비동기적으로 요청을 보낸다. 비동기 처리이기 때문에 UI 차단이 발생하지 않는다. 콜백 인터페이스인 Callback<T>를 통해 성공 및 실패에 따른 분기 처리가 가능하다. 네트워크 오류나 예외 발생 시도 예외가 아닌 콜백 메서드에서 처리할 수 있다.
다만, 코루틴을 사용하는 환경에서는 Call#enqueue 대신 KotlinExtension.kt에 정의된 await() 확장함수를 사용하는 것이 더 자연스럽다. 콜백 기반 코드보다 직관적이며 순차적인 흐름으로 가독성이 뛰어나기 때문이다.
public interface Call<T> extends Cloneable {
/**
* Asynchronously send the request and notify {@code callback} of its response or if an error
* occurred talking to the server, creating the request, or processing the response.
*/
void enqueue(Callback<T> callback);
...
}
// RemoteDataSource
fun getDailyBoxOffice(targetDate: String): List<DailyBoxOfficeModel> {
val response = kobisService.getDailyBoxOffice(targetDate = targetDate)
response.enqueue(object : Callback<KobisBoxOfficeResponse> {
override fun onResponse(
call: Call<KobisBoxOfficeResponse>,
response: Response<KobisBoxOfficeResponse>,
) {
// 네트워크 연결 성공
}
override fun onFailure(call: Call<KobisBoxOfficeResponse>, t: Throwable) {
// 네트워크 연결 실패
}
})
}
2. Response
Retrofit2는 Response<T>를 통해 응답 결과를 전달한다. 이는 성공 여부를 판단하거나 응답 코드를 확인할 수 있는 메타 정보를 포함한다. 코루틴 환경에서 Response<T>를 사용하는 경우에는 enqueue와 같은 비동기 처리를 내부적으로 포함하고 있어, 더 간결한 방식으로 사용할 수 있다.
Retrofit은 내부적으로 어떻게 동작할까?

Retrofit.create
Retrofit의 핵심은 Retrofit#create 메서드를 통해 API 인터페이스의 구현체를 생성하는 부분이다. 이 구현체는 프록시 객체이며, 해당 프록시를 호출하면 내부적으로 InvocationHandler#invoke가 호출된다. 이 과정에서 메서드가 DefaultMethod인지 여부에 따라 분기된다.
@Module
@InstallIn(SingletonComponent::class)
internal class ServiceModule {
@Provides
@Singleton
fun provideRetrofit(
@DefaultOkHttpClient okHttpClient: OkHttpClient,
converterFactory: Converter.Factory,
): Retrofit = Retrofit.Builder()
.baseUrl(BASE_URL)
.client(okHttpClient)
.addConverterFactory(converterFactory)
.build()
@Provides
@Singleton
fun provideApiService(
retrofit: Retrofit
): ApiService = retrofit.create()
// retrofit.create(ApiService::class.java)
}
inline fun <reified T> Retrofit.create(): T = create(T::class.java)
프록시? 프록시 패턴이란
프록시(Proxy) 패턴이란 어떤 객체에 대한 접근을 제어하거나 기능을 추가하기 위해 그 객체를 대신하는 대리 객체(Proxy 객체)를 두는 디자인 패턴이다. 즉, 원래 객체(Real Subject) 앞에 대리 객체(Proxy) 를 두고, 이 대리 객체가 클라이언트의 요청을 받아 내부적으로 실제 객체에 위임하거나 중간에서 가공하는 식으로 동작한다.
Retrofit에서의 프록시 패턴
Retrofit에서는 Retrofit.create(ApiService::class.java)를 호출하면 우리가 정의한 ApiService 인터페이스를 직접 구현한 클래스가 생성되지 않는다. 대신, 프록시 객체가 생성되어 내부적으로 InvocationHandler#invoke()를 통해 모든 메서드 호출이 처리된다.
- 프록시 객체는 Proxy.newProxyInstance()로 동적으로 생성된다.
- 이 프록시 객체는 API 메서드 호출을 가로채어 내부 로직(HTTP 요청 처리 등)을 수행한다.
- 즉, 우리가 정의한 인터페이스가 곧 요청 로직으로 연결되는 구조가 프록시 패턴을 통해 구현된다.
다시 돌아가서
내부 코드 살펴보기
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);
}
});
}
private void validateServiceInterface(Class<?> service) {
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
...
}
1. validateServiceInterface 확인
가장 먼저 validateServiceInterface 메서드를 통해 전달된 클래스가 인터페이스인지 검증한다. API 선언은 반드시 인터페이스 형태로만 가능하므로, 만약 클래스나 추상 클래스일 경우 IllegalArgumentException이 발생한다. 이 검증은 개발자의 선언 실수를 방지하기 위한 사전 처리이다.
2. 프록시 객체 생성과 프록시 패턴
Proxy.newProxyInstance를 통해 API 인터페이스의 프록시 인스턴스를 생성한다. 이 프록시는 동적으로 메서드를 구현하여 실제 네트워크 호출을 처리한다. 해당 방식은 프록시 패턴에 기반하며, 원본 객체 대신 중간 대리 객체를 통해 로직을 확장하거나 위임하는 데 사용된다.
Retrofit은 이 프록시를 이용해 API 인터페이스를 직접 구현하지 않아도 네트워크 요청이 가능하도록 구성한다. InvocationHandler를 구현한 익명 객체는 모든 메서드 호출을 invoke()로 가로채어 처리한다.
3. InvocationHandler#invoke 동작 방식
invoke()는 다음과 같은 흐름으로 동작한다:
- 호출된 메서드가 Object 클래스에서 정의된 기본 메서드(toString, equals, hashCode)일 경우, 원래 로직을 그대로 수행한다.
- 그렇지 않은 경우, 전달된 Method가 Default Method인지 여부를 platform.isDefaultMethod()를 통해 판단한다.
- Default Method라면 Java 8 이후 인터페이스에 정의된 기본 구현 메서드이므로, 해당 구현을 직접 실행한다.
- Default Method가 아닐 경우에는 loadServiceMethod()를 호출하여 Retrofit 내부 요청 로직을 수행한다.
4. Default Method란 무엇인가?
JavaDoc에 따르면 Default Method는 다음과 같은 조건을 가진다:
인터페이스에 선언된 public, non-abstract, non-static, instance 메서드로서, 구현부가 존재하는 메서드를 의미한다.
예를 들어, 다음과 같은 메서드는 Default Method이다:
interface Api {
default String hello() {
return "hi";
}
}
반면, 일반적인 Retrofit API 인터페이스는 대부분 다음과 같이 선언된다:
@GET("movie")
suspend fun getMovies(): Response<MovieResponse>
위처럼 구현부가 없는 메서드는 Default Method가 아니므로 isDefaultMethod()는 대부분 false를 반환하게 된다.
5. platform이란?
Platform 클래스는 Android, Java 등 환경에 따라 다르게 동작해야 하는 기능을 분리한 클래스이다. 내부 구현을 확인해보면 현재 플랫폼이 Android인지, Java 8인지 등을 판별하여 맞춤 로직을 실행하도록 설계되어 있다.
Retrofit에서는 Platform.get()을 통해 현재 플랫폼을 판별하고, Default Method를 지원하는지 등을 판단하는 데 사용된다.
6. 핵심은 loadServiceMethod().invoke(args)에서 시작된다
결과적으로 대부분의 Retrofit API 호출은 platform.isDefaultMethod()에서 false가 반환되므로 loadServiceMethod() → ServiceMethod.invoke() → HttpServiceMethod.adapt() → OkHttpCall.enqueue()로 이어지는 네트워크 호출의 전체 흐름이 이곳에서 시작된다.
Retrofit#loadServiceMethod
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는 API 인터페이스의 각 메서드를 파싱하여 ServiceMethod로 변환하는 역할을 수행한다. 이 메서드는 내부적으로 serviceMethodCache라는 캐시를 활용하여, 메서드 정보를 재사용한다.
캐시된 정보가 없다면 ServiceMethod#parseAnnotations를 호출하여 메서드의 어노테이션과 반환 타입 등을 파싱한 후 캐시에 저장한다.
ServiceMethod#parseAnnotations
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);
}
ServiceMethod#parseAnnotations는 API 메서드에 선언된 어노테이션(@GET, @POST 등)과 반환 타입을 검사한다. 유효하지 않은 어노테이션 조합이거나 반환 타입이 명확하지 않다면 예외를 발생시킨다.
이후 HttpServiceMethod#parseAnnotations를 호출하며, 여기서 코루틴 지원 여부(suspend 함수인지)와 반환 타입에 따른 로직 분기가 이루어진다.
RequestFactory#parseAnnotations
static RequestFactory parseAnnotations(Retrofit retrofit, Method method) {
return new Builder(retrofit, method).build();
}
RequestFactory(Builder builder) {
method = builder.method;
baseUrl = builder.retrofit.baseUrl;
httpMethod = builder.httpMethod;
relativeUrl = builder.relativeUrl;
headers = builder.headers;
contentType = builder.contentType;
hasBody = builder.hasBody;
isFormEncoded = builder.isFormEncoded;
isMultipart = builder.isMultipart;
parameterHandlers = builder.parameterHandlers;
isKotlinSuspendFunction = builder.isKotlinSuspendFunction;
}
RequestFactory build() {
for (Annotation annotation : methodAnnotations) {
parseMethodAnnotation(annotation);
}
if (httpMethod == null) {
throw methodError(method, "HTTP method annotation is required (e.g., @GET, @POST, etc.).");
}
...
return new RequestFactory(this);
}
RequestFactory#parseAnnotations는 Retrofit 메서드의 요청 정보를 파싱하는 핵심 메서드이다. 어노테이션, 헤더, 쿼리 파라미터, 바디 타입, 멀티파트 여부 등 다양한 정보가 이곳에서 파싱된다.
빌더 패턴을 통해 구성된 RequestFactory는 이후 실제 HTTP 요청을 만들기 위한 기반 객체로 사용된다.
HttpServiceMethod#parseAnnotations
HttpServiceMethod#parseAnnotations는 Retrofit에서 선언한 API 메서드가 suspend 함수인지 여부와 반환 타입에 따라 어떤 방식으로 호출을 처리할지 분기하는 중요한 메서드이다. 메서드의 책임이 크고 코드 라인 수도 많은 만큼, 책임을 3단계로 나누어 순서대로 살펴본다.
1. suspend 함수 여부 판단 및 관련 변수 구성
static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
Retrofit retrofit, Method method, RequestFactory requestFactory) {
boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
boolean continuationWantsResponse = false;
boolean continuationBodyNullable = false;
Annotation[] annotations = method.getAnnotations();
Type adapterType;
if (isKotlinSuspendFunction) {
Type[] parameterTypes = method.getGenericParameterTypes();
Type responseType =
Utils.getParameterLowerBound(
0, (ParameterizedType) parameterTypes[parameterTypes.length - 1]);
if (getRawType(responseType) == Response.class && responseType instanceof ParameterizedType) {
// Unwrap the actual body type from Response<T>.
responseType = Utils.getParameterUpperBound(0, (ParameterizedType) responseType);
continuationWantsResponse = true;
} else {
// TODO figure out if type is nullable or not
// Metadata metadata = method.getDeclaringClass().getAnnotation(Metadata.class)
// Find the entry for method
// Determine if return type is nullable or not
}
adapterType = new Utils.ParameterizedTypeImpl(null, Call.class, responseType);
annotations = SkipCallbackExecutorImpl.ensurePresent(annotations);
} else {
adapterType = method.getGenericReturnType();
}
...
}
위 코드에서는 isKotlinSuspendFunction 변수를 통해 메서드가 코틀린의 suspend 함수인지 확인한다. 이 값은 RequestFactory에서 Continuation 타입의 파라미터가 존재하는지를 검사해 결정된다.
private @Nullable ParameterHandler<?> parseParameter(
int p, Type parameterType, @Nullable Annotation[] annotations, boolean allowContinuation) {
ParameterHandler<?> result = null;
...
if (result == null) {
if (allowContinuation) {
try {
if (Utils.getRawType(parameterType) == Continuation.class) {
isKotlinSuspendFunction = true;
return null;
}
} catch (NoClassDefFoundError ignored) {
}
}
throw parameterError(method, p, "No Retrofit annotation found.");
}
return result;
}
Continuation 객체는 코루틴의 실행 상태를 저장하고 재개하는 데 사용되며, suspend 함수의 마지막 파라미터로 추가된다. 따라서 이 객체가 있으면 해당 메서드는 suspend 함수로 간주된다.
이후 Response<T> 형태인지 확인하고, 응답이 Response 타입일 경우 내부 바디 타입을 추출하여 continuationWantsResponse 값을 true로 설정한다. 이렇게 구성된 정보는 이후 분기 처리에서 사용된다.
2. CallAdapter 생성 및 반환 타입 유효성 검사
CallAdapter<ResponseT, ReturnT> callAdapter =
createCallAdapter(retrofit, method, adapterType, annotations);
Type responseType = callAdapter.responseType();
if (responseType == okhttp3.Response.class) {
throw methodError(
method,
"'"
+ getRawType(responseType).getName()
+ "' is not a valid response body type. Did you mean ResponseBody?");
}
if (responseType == Response.class) {
throw methodError(method, "Response must include generic type (e.g., Response<String>)");
}
// TODO support Unit for Kotlin?
if (requestFactory.httpMethod.equals("HEAD") && !Void.class.equals(responseType)) {
throw methodError(method, "HEAD method must use Void as response type.");
}
두 번째 단계에서는 CallAdapter를 생성한다. 이는 Retrofit에서 Call<T>, Deferred<T>, LiveData<T> 등 다양한 반환 타입을 처리할 수 있게 하는 어댑터 역할을 한다. 사용자가 별도로 등록한 CallAdapter.Factory가 없더라도 Retrofit은 내부적으로 기본 팩토리(DefaultCallAdapterFactory)를 사용한다.
이후 callAdapter.responseType()을 통해 실제 응답 타입을 추출하고, 몇 가지 유효성 검사를 수행한다. 예를 들어:
- okhttp3.Response는 사용할 수 없고, ResponseBody를 써야 한다.
- Response는 반드시 제네릭 타입을 포함해야 한다.
- HEAD 메서드는 반드시 Void 타입의 응답을 가져야 한다.
이러한 검사는 런타임 오류를 예방하기 위해 필수적이다.
3. 반환 타입에 따른 처리 분기
Converter<ResponseBody, ResponseT> responseConverter =
createResponseConverter(retrofit, method, responseType);
okhttp3.Call.Factory callFactory = retrofit.callFactory;
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);
}
마지막으로는 메서드의 타입에 따라 어떤 HttpServiceMethod 구현체를 사용할지를 결정한다.
- suspend 함수가 아닌 경우 → CallAdapted 클래스가 사용된다. 이는 전통적인 Call<T> 방식이다.
- suspend 함수이며 반환 타입이 Response<T>인 경우 → SuspendForResponse가 사용된다.
- suspend 함수이며 T만 반환할 경우 → SuspendForBody가 사용된다.
suspend 함수인 경우 내부적으로 KotlinExtensions.kt의 await() 계열 함수를 호출하게 되며, enqueue()를 사용하지 않아도 비동기 처리를 수행할 수 있게 된다.
코루틴과 함께 Retrofit이 어떻게 동작하는가
ServiceMethod#invoke와 코루틴 동작 흐름
이제 다시 Retrofit#create() 이후의 흐름을 살펴보면, API 인터페이스의 메서드를 호출하면 내부적으로 loadServiceMethod()가 실행되고, 그 결과로 생성된 ServiceMethod 인스턴스의 invoke()가 호출된다.
// ServiceMethod.java
abstract class ServiceMethod<T> {
...
abstract @Nullable T invoke(Object[] args);
}
// HttpServiceMethod.java
@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);
ServiceMethod#invoke()는 HttpServiceMethod의 구현체에서 오버라이드되며, 내부적으로 OkHttpCall을 생성한 뒤 adapt() 메서드를 호출한다. 이 adapt() 메서드는 추상 메서드로, 실제 네트워크 요청을 수행할 방식에 따라 CallAdapted, SuspendForResponse, SuspendForBody 중 하나에서 구현된다.
여기서는 suspend 가능한 메서드에서 사용되는 SuspendForResponse와 SuspendForBody의 동작 방식을 중점적으로 살펴본다.
SuspendForResponse#adapt() - Response를 리턴하는 suspend 함수 처리
static final class SuspendForResponse<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
SuspendForResponse(...) {
...
}
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
//noinspection unchecked Checked by reflection inside RequestFactory.
Continuation<Response<ResponseT>> continuation =
(Continuation<Response<ResponseT>>) args[args.length - 1];
try {
return KotlinExtensions.awaitResponse(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
SuspendForResponse는 Response<T> 전체를 반환하는 suspend 함수에 대한 처리 클래스이다. 내부적으로 awaitResponse()를 호출하여 네트워크 요청을 수행하고 결과를 Continuation에 전달한다.
KotlinExtensions.awaitResponse()의 동작 방식
suspend fun <T> Call<T>.awaitResponse(): Response<T> {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
continuation.resume(response)
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
이 함수는 suspendCancellableCoroutine을 사용하여 Retrofit의 Call을 코루틴으로 감싼다. 응답이 성공하면 resume(response)를 호출하고, 실패하면 resumeWithException()을 통해 예외를 Continuation으로 전달한다. 이 Continuation에는 API를 호출한 호출 지점의 컨텍스트 정보가 담겨 있으며, 이 지점이 Suspension Point가 된다.
즉, 예외가 발생하면 해당 지점으로 예외가 전달되어 자연스럽게 try-catch로 처리할 수 있게 된다.
SuspendForBody#adapt() - Body만 리턴하는 suspend 함수 처리
static final class SuspendForBody<ResponseT> extends HttpServiceMethod<ResponseT, Object> {
private final CallAdapter<ResponseT, Call<ResponseT>> callAdapter;
private final boolean isNullable;
SuspendForBody(...) {
...
}
@Override
protected Object adapt(Call<ResponseT> call, Object[] args) {
call = callAdapter.adapt(call);
Continuation<ResponseT> continuation = (Continuation<ResponseT>) args[args.length - 1];
try {
return isNullable
? KotlinExtensions.awaitNullable(call, continuation)
: KotlinExtensions.await(call, continuation);
} catch (Exception e) {
return KotlinExtensions.suspendAndThrow(e, continuation);
}
}
}
이 클래스는 API 메서드가 T 형식의 바디만 반환하는 경우를 처리한다. 반환 타입이 Nullable인지 여부에 따라 await() 또는 awaitNullable()을 호출한다.
await() vs awaitNullable()
// Non-nullable body 처리
suspend fun <T : Any> Call<T>.await(): T {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T> {
override fun onResponse(call: Call<T>, response: Response<T>) {
if (response.isSuccessful) {
val body = response.body()
if (body == null) {
val invocation = call.request().tag(Invocation::class.java)!!
val method = invocation.method()
val e = KotlinNullPointerException("Response from " +
method.declaringClass.name + '.' + method.name +
" was null but response body type was declared as non-null")
continuation.resumeWithException(e)
} else {
continuation.resume(body)
}
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
Non-nullable 타입의 바디는 null이면 예외를 발생시킨다. 이는 명시적으로 API 반환 타입을 T로 지정했기 때문이다.
// Nullable body 처리
@JvmName("awaitNullable")
suspend fun <T : Any> Call<T?>.await(): T? {
return suspendCancellableCoroutine { continuation ->
continuation.invokeOnCancellation {
cancel()
}
enqueue(object : Callback<T?> {
override fun onResponse(call: Call<T?>, response: Response<T?>) {
if (response.isSuccessful) {
continuation.resume(response.body())
} else {
continuation.resumeWithException(HttpException(response))
}
}
override fun onFailure(call: Call<T?>, t: Throwable) {
continuation.resumeWithException(t)
}
})
}
}
Nullable한 타입은 body가 null이더라도 예외가 발생하지 않는다. 반환 타입이 T?로 선언되었기 때문에 null 자체를 유효한 결과로 간주한다.
suspendAndThrow() – 코루틴 예외 강제 전파
internal suspend fun Exception.suspendAndThrow(): Nothing {
suspendCoroutineUninterceptedOrReturn<Nothing> { continuation ->
Dispatchers.Default.dispatch(continuation.context, Runnable {
continuation.intercepted().resumeWithException(this@suspendAndThrow)
})
COROUTINE_SUSPENDED
}
}
suspendAndThrow()는 예외가 발생했을 때, 코루틴의 실행을 일시 중단(suspend)한 후 예외를 Suspension Point로 전달하는 역할을 한다. Java의 Proxy는 체크 예외를 직접 throw할 수 없기 때문에, 예외가 발생해도 바로 던지지 않고 이 방식으로 코루틴 컨텍스트로 넘긴다.
이 방식 덕분에 Retrofit은 suspend 메서드 내부에서 예외를 안전하게 throw할 수 있으며, 호출 측에서는 try-catch로 이를 자연스럽게 처리할 수 있게 된다.
요약
- ServiceMethod#invoke()는 Retrofit API 호출의 핵심 진입점이며, 내부적으로 OkHttpCall을 생성하고 adapt() 메서드로 요청을 처리한다.
- suspend 메서드에서는 SuspendForResponse 또는 SuspendForBody를 통해 적절한 방식으로 enqueue()를 호출한다.
- KotlinExtensions.kt의 await, awaitResponse, awaitNullable은 모두 suspendCancellableCoroutine을 사용하여 코루틴과 Retrofit을 자연스럽게 연결한다.
- 예외 처리 시에는 suspendAndThrow()를 통해 Suspension Point로 예외를 안전하게 전파한다.
참고자료
(Android) Retrofit2는 어떻게 동작하는가 — 1. 내부 코드 분석
Retrofit2 Deep Dive #1
medium.com
(Android) Retrofit2는 어떻게 동작하는가 — 2. IO Dispatcher는 필요한가
Retrofit2 Deep Dive #2
medium.com
(Android) Retrofit2는 어떻게 동작하는가 — 3. OkHttp3의스레드 관리
Retrofit2 Deep Dive #3
medium.com
'Android > Study' 카테고리의 다른 글
[Android] Gson vs Moshi vs kotlinx.serialization Deep Dive (0) | 2025.03.26 |
---|---|
[Android] 코루틴 정리 feat. suspend 키워드까지 (0) | 2025.03.19 |
[Android] ViewModel이란 무엇이고 그리고 LifeCycle 까지 (0) | 2025.03.05 |
[Android] 인텐트(Intent) 및 인텐트 필터(Intent Filter) (0) | 2025.02.20 |
[Android] Jetpack Compose의 Recomposition 정리 (0) | 2025.02.20 |