Retrofit Internals - Retrofit In Coroutine

2025. 6. 20. 11:53·Android

이전 글에서는 Retrofit이 동적 프록시와 리플렉션을 활용해 서비스 인터페이스의 구현체를 생성하는 과정을 살펴보았습니다. 이를 통해 Retrofit이 어떻게 인터페이스 메서드 호출을 가로채고, 내부에서 HTTP 요청 처리를 위한 준비를 하는지 이해할 수 있었습니다.

 

이번 글에서는 코루틴을 지원하는 Retrofit의 동작 방식을 자세히 들여다보며, HttpServiceMethod가 어떻게 다양한 호출 방식(suspend 함수, 일반 함수, Response, Call)을 처리하는지, 그리고 네트워크 요청을 어떻게 수행하는지 알아보겠습니다.

# Continuation

Retrofit이 코루틴을 다루는 방법을 이해하기 위해선 먼저 suspend 함수에 대한 지식이 필요합니다. 이를 위해 간단한 예시 코드를 한번 보겠습니다.

fun main() = runBlocking {
    println(printName("peto"))
}

suspend fun printName(name: String): Int {
    delay(1000L)
    return 13
}

suspend 함수는 컴파일 시 마지막 인자로 Continuation <T> 객체를 받는 형태로 변환됩니다. Continuation은 코루틴의 실행 상태를 저장하고 재개할 수 있도록 도와주는 인터페이스입니다. suspend 함수가 일시 중단되면 해당 지점을 기억하고, 이후 resumeWith를 통해 중단된 곳부터 다시 실행을 이어갈 수 있습니다.

suspend와 Continuation에 관한 내용도 자세히 다루게 되면 너무나 방대한 내용이기 때문에 지금은 suspend 함수의 마지막 인자로 Continuation이 전달된다는 것만 기억해 주세요!

HttpServiceMethod.parseAnnotations는 인자로 전달받은 RequestFactory 객체의 isKotlionSuspendFunction라는 플래그 값을 호출합니다. suspend 함수 여부를 확인하는 인자가 requestFactory에서 이루어지는 것을 확인했으니 requestFactory에선 이를 어떻게 구현하고 있는지 먼저 살펴보겠습니다.

#1. ParameterHandler

ParameterHandler는 서비스 인터페이스 메서드의 각 파라미터에 붙은 Retrofit 어노테이션(@Body, @Query, @Path 등)을 분석하고, 이를 HTTP 요청에 어떤 방식으로 반영할지를 정의하는 전략 객체입니다. 예시를 위해 내부 구현 중 일부를 발췌해 왔습니다. 

  • @Query -> ParameterHandler.Query<T> 생성
  • @Path -> ParameterHandler.Path<T> 생성

RequestFactory는 build 메서드에서 호출된 서비스 인터페이스 메서드의 파라미터를 순회하며 parseParameter 메서드를 호출해 ParameterHandler를 생성합니다. 이때, 마지막 suspend 함수가 메서드의 마지막 인자로 continuation을 추가한다는 점을 활용해 마지막 파라미터일 경우 이에 대한 검증을 수행합니다.

final class RequestFactory {
  ...

  final boolean isKotlinSuspendFunction;
  
  RequestFactory build() {
      ...
      
      parameterHandlers = new ParameterHandler<?>[parameterCount];
      for (int p = 0, lastParameter = parameterCount - 1; p < parameterCount; p++) {
        parameterHandlers[p] =
            parseParameter(
            p, 
            parameterTypes[p], 
            parameterAnnotationsArray[p], 
            p == lastParameter // 마지막 파라미터일 경우 Continuation인지 검증 수행
       );
      }
   }
}

#2. RequestFactory.parseParameter

parseParameter 메서드는 parseParameterAnnotation을 호출해 파라미터에 붙은 Retrofit 어노테이션(@Query, @Path 등)을 파싱 하고, 이에 대응하는 ParameterHandler 객체를 생성합니다.

 

어노테이션이 없는 마지막 파라미터이면 해당 파라미터가 Continuation 타입인지 확인하여 suspend 함수인지 판별합니다.

 

Continuation이면 isKotlinSuspendFunction를 true로 하고 HTTP 요청에 사용되지 않기 때문에 ParameterHandler는 null을 반환합니다. 이해하기 위해선 먼저 suspend 함수에 대한 선수 지식이 필요합니다. 이를 위해 간단한 예시 코드를 한번 보겠습니다.

fun main() = runBlocking {
    println(printName("peto"))
}

suspend fun printName(name: String): Int {
    delay(1000L)
    return 13
}

# HttpServiceMethod.parseAnnotations

RequestFactory 내에서 suspend 함수를 판별하는 원리를 이해했으니 다시 돌아와서 HttpServiceMethod가 이를 어떻게 활용하는지 이해해 보겠습니다. 

이 필드들은 HttpServiceMethod가 네트워크 요청을 완료 후 어떤 타입을 반환할지 결정합니다.

  • continuationWantsResponse : Response<T>
  • continuationBodyNullable : Nullable 한 반환타입 
  • continuationIsUnit : Unit

parseAnnotations은 네트워크 요청을 수행하기 위해 크게 3가지 과정을 거칩니다.

  • 리턴 타입을 확인해서 adapterType 생성
  • 네트워크 호출 결과를 어떻게 감싸서 리턴할지 결정해서 CallAdapter 생성
  • 응답을 어떻게 변환(역직렬화) 할지 정해서 responseConverter 생성

먼저 46번째 라인에서 suspend 함수 여부에 따라 분기합니다. 만약 suspend 함수일 경우 리플렉션을 사용해 메서드의 제네릭 타입 파라미터를 가져옵니다.

 

이 과정을 예시 코드로 보면 ProductResponse 이거나 그 상위 타입을 받아들일 수 있는 코루틴 Continuation<? super ProductResponse>으로 반환됩니다.

그 후 Retrofit의 Utils 패키지에 있는 getParameterLowerBound 메서드를 통해 이 Continuation 타입의 제네릭 인자에서 실제 반환 타입인 ProductResponse를 추출합니다.

 

예를 들어, Continuation<? super ProductResponse>에서 ? super ProductResponse를 추출하면, ProductResponse가 실제 반환 타입으로 결정됩니다.

final class Utils {
  ...
 
 static Type getParameterLowerBound(int index, ParameterizedType type) {
    Type paramType = type.getActualTypeArguments()[index];
    if (paramType instanceof WildcardType) {
      return ((WildcardType) paramType).getLowerBounds()[0];
    }
    return paramType;
  }
}

이 과정도 예시 코드로 검증해보면 ProductResponse가 반환되는 것을 확인할 수 있습니다. 이때, ParameterizedType은 실제로 제네릭을 사용하고 있는지를 검증합니다.

지금까지의 과정을 정리하면 47번과 50번 라인은 suspend 함수의 리턴 타입을 꺼내는 과정입니다. 코틀린에서 suspend 함수는 내부적으로 Continuation으로 감싸지기 때문에, 그 안에 들어 있는 실제 리턴 타입만 꺼내서 사용하기 위한 과정입니다.

다음은 반환 타입이 Retrofit2.Response 타입인 경우 제네릭 타입 T만 따로 추출해서 내부 변환에 사용하고, 외부에는 Response<T> 전체를 그대로 넘겨줄 수 있도록 continuationWantsResponse = true로 설정합니다.

그리고 코루틴은 이미 비동기로 실행되기 때문에 Call<T>를 반환하는 것은 잘못된 사용이므로 예외를 throw 하고 만약 반환 타입이 Unit이라면 Unit을 반환하도록 continuationIsUnit = true으로 설정합니다.

그리고 Utils.ParameterizedTypeImpl을 사용해 responseType을 타입 인자로 갖는 Call<responseType> 객체를 동적으로 생성합니다. 예를 들어 Call<ProductResponse>와 같은 제네릭 타입 정보를 런타임에 객체 형태로 직접 생성하게 됩니다. 이는 Retrofit 내부 로직이 모든 네트워크 응답을 Call<T> 형태로 처리하도록 설계되어 있기 때문입니다.

 

그러므로 suspend 함수를 사용할 경우, Retrofit이 내부적으로 이미 Call 객체를 만들어 처리하므로 서비스 인터페이스에서 Call<T>를 반환하면 중복되어 예외가 발생하게 됩니다.

 

반면 일반 함수 (ex, fun getProduct(): Call<ProductResponse>)의 경우 사용자가 직접 enqueue() 또는 execute()로 네트워크 요청을 수행하기 때문에, 서비스 메서드의 반환 타입을 그대로 사용해도 문제 되지 않습니다.

마지막으로, 메서드의 선언 방식에 따라 알맞은 HttpServiceMethod 구현체가 선택됩니다. 일반 함수인 경우에는 CallAdapted가 사용되며, suspend 함수인 경우 반환 타입에 따라 SuspendForResponse 또는 SuspendForBody 중 하나가 선택됩니다.

이 세 클래스는 모두 HttpServiceMethod를 상속하고 있으며 내부에서 정의된 추상 메서드 adapt()를 각 방식에 맞게 오버라이드하여 실제 네트워크 호출 결과를 어떻게 처리할지 구현합니다.

 

adapt()는 Retrofit이 생성한 Call<T> 객체를 사용자가 선언한 리턴 타입(ReturnT)으로 변환하는 메서드로 실제 네트워크 요청을 수행하고, 그 결과를 적절한 방식으로 사용자에게 전달하는 최종 어댑터 역할을 합니다. 

 

그리고 adapt에 인자로 전달되는 첫 번째 인자가 바로 위에서 알아본, Retrofit이 내부적으로 만든 Call<T> 이며(단, 일반 함수일 경우 Call<T>을 그대로 사용), 두 번째 인자는 호출된 메서드의 인자입니다.

1. CallAdapted

2. SuspendForResponse

SuspendForResponse에선 KotlinExtensions.awaitResponse를 통해 네트워크 요청을 수행합니다.

# Call<T>.awaitResponse

awaitResponse는 Call 타입에 대한 확장 함수로, 내부적으로 enqueue를 사용하여 네트워크 요청을 비동기적으로 수행합니다. 요청이 성공하면 응답 데이터를 continuation.resume()에 담아 반환하고, 실패하면 continuation.resumeWithException()을 호출하여 예외를 전달합니다.

3. SuspendForBody

SuspendForBody 에선 반환 타입에 따라 각각 다른 메서드를 호출합니다.

# Call<T>.await

await()는 내부적으로 enqueue()를 사용해 비동기 요청을 보내고, 응답 결과에 따라 코루틴을 재개하거나 예외를 던집니다. 응답이 성공했지만 본문이 null이면 KotlinNullPointerException을 발생시키며 실패한 HTTP 응답은 HttpException 네트워크 오류는 그대로 예외로 전달됩니다.

# Call<T>.awaitNullable

awaitNullable의 전체적인 구조는 awaitResponse와 매우 유사하며 차이점은 Response <T?>에서 본문만 꺼내서 그대로 nullable 하게 반환한다는 점입니다.

# Call<T>.awaitUnit

awaitUnit은 내부적으로는 Unit을 nullable하게 캐스팅하여 처리하기 때문에 awaitNullable을 호출합니다.

adapt()는 HttpServiceMethod.invoke() 내부에서 호출되며, 이 invoke()는 Retrofit이 프록시를 통해 API 메서드를 실행할 때 실행되는 메서드입니다. adapt()는 Retrofit의 프록시 API 메서드 호출 흐름 중 HttpServiceMethod.invoke() 내부에서 호출되어 실제 네트워크 요청을 수행하거나 반환 타입에 맞게 처리합니다.

References

  • https://docs.oracle.com/javase/8/docs/api/java/lang/reflect/Proxy.html
  • https://github.com/square/retrofit
  • https://medium.com/jaesung-dev/android-retrofit2%EB%8A%94-%EC%96%B4%EB%96%BB%EA%B2%8C-%EB%8F%99%EC%9E%91%ED%95%98%EB%8A%94%EA%B0%80-1-%EB%82%B4%EB%B6%80-%EC%BD%94%EB%93%9C-%EB%B6%84%EC%84%9D-964f4b5d0a5d

'Android' 카테고리의 다른 글

운영체제 메모리  (0) 2025.10.14
안드로이드에서 네트워크 상태에 따라 API를 재호출해보자  (0) 2025.09.27
Retrofit Internals - Retrofit은 어떻게 인터페이스의 구현체를 만들까 ?  (1) 2025.06.16
ViewModel의 One Time Event를 다루는 다양한 솔루션  (0) 2025.02.07
안드로이드 클린아키텍처 에러 핸들링 (부제: 상속을 지양하자)  (2) 2024.07.28
'Android' 카테고리의 다른 글
  • 운영체제 메모리
  • 안드로이드에서 네트워크 상태에 따라 API를 재호출해보자
  • Retrofit Internals - Retrofit은 어떻게 인터페이스의 구현체를 만들까 ?
  • ViewModel의 One Time Event를 다루는 다양한 솔루션
빨주노초잠만보
빨주노초잠만보
  • 빨주노초잠만보
    과거의 나를 통해 미래의 나를 성장시키자
    빨주노초잠만보
  • 전체
    오늘
    어제
    • 분류 전체보기 (108)
      • 우아한테크코스 (6)
      • TEKHIT ANDROID SCHOOL (4)
      • Android Architecture (8)
      • Android (38)
      • PROJECT (11)
      • KOTLIN (10)
        • 코루틴의 정석 (3)
      • BACK END (12)
      • CS (4)
      • 컨퍼런스 (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    2025 우아콘 후기
    android clean architecture
    DI
    retrofit call
    컴포즈 디자인 시스템
    android view lifecylce
    DataSource
    Throttle
    value class
    repository
    Compose Typography
    안드로이드 디자인 시스템
    flow
    코틀린 코루틴의 정석
    sealed class
    callbackflow
    coroutine Context Switching
    MVI
    STATEFLOW
    Repository Pattern
    process Context Switching
    의존성 주입
    view 생명주기
    Room
    android Room
    Two pass process
    thread Context Switching
    ThrottleFirst
    orbit
    Clean Architecture
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
빨주노초잠만보
Retrofit Internals - Retrofit In Coroutine
상단으로

티스토리툴바