요근래 Retrofit에서 ThreadPool 관리를 어떻게 하는가에 대한 질문 받았었습니다.
하지만 그 당시에는 대답을 잘 하지 못해가지고😭
해당 부분에 대한 내용을 다시 공부도 할겸 이렇게 게시글을 정리하게 되었습니다.
먼저 결론부터 말하자면!
Retrofit 내에서 ThreadPool을 직접 만들어 관리하는 것으로 확인하였습니다. 👍🏻
결국 Dispatcher.IO도 IO만을 위한 ThreadPool을 사용하고 있는 방식이기 때문에 굳이 withContext를 이용해 IO Thread로 바꿔주지 않아도 되는듯해요.🔥
아직 안드로이드 경력 0년인 초보자인 관계로 혹시 틀린 점이 있다면
꼭! 댓글로 말씀 부탁드립니다! 🙏
지금까지 분석해보았던 라이브러리들 중에서 가장 어려운 분석 난이도가 아니었을까 해요.ㅎㅎ
1. API Service 구현 클래스 생성
Retrofit과 Hilt를 같이 사용하다보면 Api Service를 모듈로 관리할 때가 많았는데,
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideImageApiService(
retrofit: Retrofit
): ImageApiService = retrofit.create(ImageApiService::class.java)
}
저는 위와 같이 선언하였습니다.
메서드 이름 그대로 Retrofit의 create 메서드들 통해 ImageApiService 구현 클래스를 생성하는 코드입니다.
📌 (Retrofit).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);
}
});
}
으아. 한눈에 봐도 뭔가 긴 코드가 호출이 되는데, 이곳에서 중요한 부분은 바로 저 InvocationHandler입니다.
이 Handler는 우리가 Api Service에 선언해둔 메서드를 호출할 때 실행이 되는 부분이라고 생각하시면 됩니다. 🤗
👇🏻 요런 느낌?
이제 하나의 API 메서드를 사용하였을 때 어느 코드가 호출되었는지 알게 되었습니다.
그리고 이 코드를 타고타고 들어가다보면 실질적으로 Network 요청을 처리하는 enque 메서드와 만나실 수 있습니다. 😌
2. enque
이 enque 메서드에서는 client의 dispatcher를 사용하게 되는데,
🌹 바로 이 dispatcher가 Retrofit Thread 관리의 핵심적인 인스턴스입니다.
여기서 Client는 OkHttpClient입니다.
처음에는 이름이 Dispatcher이길래 kotlin껀줄 알았는데 OkHttp의 Dispatcher 클래스였다는..
3. Dispatcher
OkHttp3 package 에 있는 Dispatcher 코드를 보시면
@get:Synchronized
@get:JvmName("executorService") val executorService: ExecutorService
get() {
if (executorServiceOrNull == null) {
executorServiceOrNull = ThreadPoolExecutor(0, Int.MAX_VALUE, 60, TimeUnit.SECONDS,
SynchronousQueue(), threadFactory("$okHttpName Dispatcher", false))
}
return executorServiceOrNull!!
}
요렇게 ExecutorService 프로퍼티를 만들어 사용하고 있는 것을 알 수 있습니다.
이 executorService는 실질적으로 각 네트워크 요청을 threadpool에서 동작시키는데 사용됩니다.
추가적으로 executorService는 OkHttpClient를 정의할 때 Custom하게 변경할 수도 있습니다. 🤗
@Module
@InstallIn(SingletonComponent::class)
object HttpClientModule {
private const val TIMEOUT = 3L
@Singleton
@Provides
fun provideHttpClient(): OkHttpClient {
val interceptor = HttpLoggingInterceptor()
interceptor.level = if (BuildConfig.DEBUG) {
HttpLoggingInterceptor.Level.BODY
} else {
HttpLoggingInterceptor.Level.NONE
}
return OkHttpClient.Builder()
.dispatcher(
// Custom ExecutorService로 설정
Dispatcher(
ThreadPoolExecutor(0,10,60,
TimeUnit.SECONDS,
SynchronousQueue(),
threadFactory("이건 테스트 Dispatcher", false)
)
)
)
.connectTimeout(TIMEOUT, TimeUnit.SECONDS)
.build()
}
}
4. 정리
이전에는 코드리뷰를 통해 Retrofit 자체에서 Dispatcher.IO를 지원해준다고만 들어 withContext를 사용하지 않기만 하다가
이런 질문을 듣게 되어 직접 한번 분석해보는 경험을 가지게 되었습니다. 😭
사실 이 질문을 받게된 계기가 Hilt를 사용할 때 ApiService에는 @Singleton을 사용하고 Retrofit에는 @Singleton을 사용하지 않았던 부분에 질문을 받은 것이 시작이었습니다.ㅎㅎ 왜 그 때는 대답을 잘 못했는지ㅠㅠ
4.1 결론
- Api Service의 메서드를 호출할 때마다 새로운 HttpConnection이 연결된다.(물론 3 Hand-Shake도)
- Api Service의 메서드가 호출될 때마다 하나의 thread를 점유해서 작업을 수행하며, 이는 Api Service 메서드가 suspend로 정의되어있어도 동일한 듯 합니다. 즉, Api Service 메서드를 호출하는 Thread만 코루틴으로 동작해서 block 되지 않고, 실질적으로 네트워크 연결이 수행되는 쪽은 하나의 thread 자체를 사용한다고 생각합니다.
- Api Service 메서드의 반환값이 Response여도 내부에서는 결국 Call을 사용하는 것이며, onResponse / onFailure는 Api Service를 호출한 thread에서 수행됩니다.
덧붙이자면
결국은 Api Service 인스턴스 자체가 아닌 Api Service의 메서드를 호출할 때마다 특정 작업이 이루어지므로
Api Service를 생성하는 쪽도 Retrofit을 생성하는 쪽도 Singleton을 붙여주었습니다. 🪴
@Module
@InstallIn(SingletonComponent::class)
object NetworkModule {
@Singleton
@Provides
fun provideImageApiService(
retrofit: Retrofit
): ImageApiService = retrofit.create(ImageApiService::class.java)
}
@Module
@InstallIn(SingletonComponent::class)
object RetrofitModule {
@Singleton
@Provides
fun provideRetrofit(
okHttpClient: OkHttpClient,
converter: Converter.Factory
): Retrofit {
return Retrofit.Builder()
.client(okHttpClient)
.addConverterFactory(converter)
.baseUrl("https://picsum.photos")
.build()
}
}
감사합니다. 🌹
'Android > Android' 카테고리의 다른 글
[Android] Room을 이용해 만든 Database와 dao는 어떻게 생겼을까? (0) | 2023.04.01 |
---|---|
[Android/Compose] 언제 Recomposition이 발생하는 것일까? (0) | 2023.03.26 |
[Android/Compose] Compose에서 Paging3 적용기 (0) | 2023.03.03 |
[공식문서 열어보기] AlarmManger로 정확한 시간에 알림 띄워주자 (0) | 2023.02.13 |
[Hilt] Component와 Scope 무엇이 다른지 디버깅으로 다 찍어보자! (0) | 2023.02.11 |