(Android) Retrofit2는 어떻게 동작하는가 — 3. OkHttp3의 스레드 관리

Retrofit2 Deep Dive #3

Jaesung Lee
jaesung dev
8 min readNov 9, 2023

--

Photo by Michael Dziedzic on Unsplash

[목차 보기]

지금까지의 Retrofit2 동작 원리 시리즈를 통해 다음 내용들을 확인할 수 있었습니다.

  • Retrofit2는 OkHttp3에 기반하여 동작한다.
  • Retrofit2와 coroutines를 함께 활용할 경우 Call#enqueue를 수행하지 않아도 내부적으로 비동기 처리를 알아서 한다.
  • Call#enqueue를 수행하고 UI 업데이트 시 runOnUiThread를 하지 않아도 되는 이유는 Callback을 MainThreadExecutor에서 실행시켜주기 때문이다.

Callback을 MainThreadExecutor를 통해 UI 스레드에서 수행할 수 있게끔 하는 것은 맞지만, 네트워크 요청을 하고 응답을 받아오는 비동기 로직이 UI 스레드에서 이루어진다고 생각하면 안됩니다. 네트워크 작업 자체를 UI 스레드에서 단독으로 수행하게 되면 NetworkOnMainThreadException 이 발생하기 때문입니다.

그렇다면 별도의 Worker Thread에서 이 비동기 로직을 수행한다고 이해할 수 있는데 어떻게 수행할 수 있을까요?

이번 글에서는 실제 비동기 로직이 어떻게 별도의 스레드 및 스레드 풀을 통해 수행되는지 살펴보겠습니다.

OkHttp3 Dispatcher

Retrofit에서 비동기 작업을 수행하기 위한 스레드 및 스레드 풀 관리는 OkHttp3의 Dispatcher에서 이루어집니다. 아래 코드를 살펴보겠습니다.

지난 글에서, ExecutorCallbackCall#enqueue에서 Delegate된 OkHttp 인스턴스를 사용하며 여기서 RealCall 인스턴스를 초기화해 다시 RealCall#enqueue를 수행하게 되는 것을 확인했습니다. 해당 메서드에서는 OkHttpClient의 Dispatcher를 통해 다시 enqueue를 수행하는 것을 확인할 수 있고, 실제 콜백을 수행하는 AsyncCall 객체를 파라미터로 전달하게 됩니다.

AsyncCall은 콜백을 비동기하게 실행시킬 수 있도록 구성된 RealCall 클래스의 inner 클래스 입니다. 또한, Call 인터페이스를 구현하지 않고, Runnable을 상속받고 있습니다. AsyncCall은 잠시 후에 다시 살펴보도록 하겠습니다.

OkHttp3 Dispatcher 주요 멤버 변수

OkHttp3의 Dispatcher는 비동기 요청이 실행되는 시점에 적절하게 수행될 구성요소들이 정의되어 있습니다. 정의되어있는 요소들 중 대표적인 요소들을 살펴보겠습니다. 해당 요소들은 OkHttp.Builder#dispatcher를 통해 직접 Dispatcher를 커스텀하여 적절하게 추가 및 변경할 수 있습니다.

1. maxRequests

maxRequests는 OkHttp 클라이언트의 요청이 동시에 실행될 수 있는 최대 요청 수를 의미합니다. 기본적으로 64개의 요청을 허용하고 있습니다.

만약 정의된 최대 요청 수보다 더 많은 요청이 이루어질 경우, readyAsyncCalls로 정의된 ArrayDeque를 통해 메모리에 대기하게 됩니다. 또한, 해당 요청들은 runningAsyncCalls로 정의된 ArrayDeque를 통해 관리됩니다.

2. maxRequestsPerHost

maxRequestsPerHost는 말 그대로 각 호스트가 동시에 실행할 수 있는 최대 요청 수를 의미합니다. 여기서 호스트는 URL의 host name에 따라 구분됩니다. 즉, 특정 BaseURL에 대해 동시에 요청이 필요할 경우 최대 5번의 요청이 동시에 처리될 수 있으며, 이보다 초과된 요청들은 곧바로 처리되지는 않고 전송된 상태로 유지됩니다.

3. executorService

executorService는 실제 요청의 처리가 이루어지는 스레드 풀을 생성 및 관리합니다.

스레드 풀은 ThreadPoolExecutor를 통해 구성되는데 전달할 수 있는 파라미터들을 하나씩 살펴보겠습니다.

  • corePoolSize: Int = 0
    스레드 풀에 최소한으로 유지할 스레드의 갯수를 의미합니다.
    최초에는 corePoolSize만큼의 스레드 수를 가지고 시작하고, 더 많은 요청이 들어올 경우 maximumPoolSize에 정의된 수 만큼 생성하게 됩니다. 또한, 생성된 스레드의 작업이 없다면 allowCoreThreadTimeOut에 의해 corePoolSize에 설정한 수 만큼 스레드 풀에서 제거됩니다.
    네트워크 요청 작업이 주기적으로 있는 것은 아니고 요청할때마다 이루어지기 때문에, 스레드 풀의 유지비용 보다 생성 비용이 낫다고 판단한 것으로 파악됩니다.
  • maximumPoolSize: Int = Int.MAX_VALUE
    스레드 풀이 최대로 가질 수 있는 스레드의 개수를 의미합니다.
  • keepAliveTime: Long = 60 / unit: TimeUnit = TimeUnit.SECONDS
    스레드의 유휴시간을 의미합니다.
    네트워크 요청 작업이 60초 이내에 다시 일어난다면 스레드 풀에서 제거하지 않고 재사용하게 됩니다.
  • workQueue: BlockingQueue<Runnable> = SynchrounousQueue()
    스레드 풀의 작업 큐를 의미합니다.
    요청된 작업을 실행시켜주기 위해서는 당연히 큐가 필요할 것입니다. JavaDocs에 의하면 SynchronousQueue는 불공정한 액세스 정책을 갖는 동기화 큐를 의미한다고 합니다.
  • threadFactory: ThreadFactory
    스레드 풀에 스레드를 만드는 방식을 정의하는 팩토리를 의미합니다.
    OkHttp3는 “OkHttp3 Dispatcher” 라고 하는 네이밍을 갖는 스레드를 스레드 풀에 생성하게 됩니다.

Dispatcher#enqueue

Dispatcher#enqueue가 실행되면 readyAsyncCalls에 해당 비동기 작업을 추가하게 됩니다. 또한, 해당 작업의 호스트에 따라 이미 진행중인 작업이 있는지 확인하고, 재사용 가능한지 판단하는 과정도 이루어집니다.

이 후, 새롭게 추가된 작업에 대해 Dispatcher#promoteAndExecute를 수행하게 됩니다.

Dispatcher#promoteAndExecute

Dispatcher#promoteAndExecute에서 하는 역할은 크게 3가지입니다.

readyAsyncCalls에 저장된 비동기 작업들을 executableCalls와 runningAsyncCalls에 넣어 실행 시키기 위한 작업을 진행합니다. 여기서, executableCalls와 runningAsyncCalls 모두 현재 실행 시킬 비동기 작업들을 의미하지만, runningAsyncCalls에 다시 넣는 이유는 앞서 설명한 진행중인 작업인지에 대한 검증 때문입니다.

또한, 정의한 maxRequests와 maxRequestsPerHost에 따라 해당 작업을 건너뛸지 유지할지 판단합니다.

이후, executableCalls에 저장된 비동기 작업들을 순차적으로 실행시킵니다. 여기서 중요한 점은, 해당 작업을 실행하기 위해 AsyncCall#executeOn을 수행하는데 executorService를 파라미터로 전달하고 있습니다. 이 executorService는 앞서 살펴봤던 ThreadPoolExecutor에 의해 생성된 스레드 풀을 의미하며 즉, 해당 비동기 작업은 스레드 풀에 생성된 특정 스레드에 의해 실행된다는 것을 의미합니다.

RealCall.AsyncCall#executeOn

지금까지 비동기 작업은 OkHttp3의 Dispatcher를 통해 구성된 스레드 풀의 특정 스레드에서 동작한다는 것을 살펴봤습니다. 또한, 이러한 비동기 작업은 AsyncCall#executeOn에 의해 실행되는 것까지 알게되었는데 해당 함수는 어떤 역할을 하는지 살펴보겠습니다.

AsyncCall#executeOn을 실행하게 되면 try-catch 블록을 통해 인자로 전달받은 executorService를 통해 해당 비동기 작업을 실행하게 됩니다. 여기서 발생한 예외는 최초에 RealCall#enqueue에 전달한 AsyncCall의 responseCallback을 통해 onFailure로 전달되게 됩니다.

또한, 정상적으로 비동기 작업의 실행이 완료 된다면 Dispatcher#finished를 호출해 runningAsyncCalls에서 완료된 작업을 제거합니다.

정리

이번 글을 통해 네트워크 요청 작업은 OkHttp3의 Dispatcher에서 생성된 스레드 풀에 의해 실행되는 것을 알 수 있었습니다. 또한, 이러한 스레드 풀은 개발자가 직접 제어할 수 있는 것도 알게 되었습니다.

요구사항에 맞게 비동기 작업이 실행될 스레드 풀을 직접 제어해 효율적인 메모리 관리 방법까지 생각해볼 수 있으면 좋을 것 같습니다.

--

--