Retrofit2 사용할 때 혹은 코루틴과 함께 사용할 때 기본으로 WorkerThread 에서 동작한다는 이야기 들어보신적 있으시나요?
항상 WorkerThread 에서 동작한다고 기대하면 안되는 케이스와 기대해도 되는 케이스에 대해 알아보려해요~!
대략 Retrofit2 의 기본 처리, 코루틴의 기본처리, jakewharton 이 제공하는 CoroutineCallAdapterFactory 처리, RxJava 의 기본처리 방식에 대해 알아볼꺼에요.
Retrofit2 로 요청을 보내는 로직에 Thread.currentThread().name 을 출력해보면, "OkHttp ${RequstUrl}" 형식으로 출력되고 있을꺼에요. 이는 OkHttp 에서 가지고 있는 WorkerThread 로 동작하고 있다고 볼 수 있어요.
그리고 위의 WorkThread 는 OkHttp3 의 Dispatcher 클래스에서 동작하고 있어요.
Dispatcher 클래스를 설명하기 전에 대략 Retrofit2 에서 어떤식으로 활용되는지 본다면 OkHttpClient 를 만들때 생성되는 변수에요.
그리고 실제 네트워크 콜이 실행되는 RealCall 클래스에서 enqueue, execute 할 때 dispatcher 가 활용되어요.
네트워크 작업에 관여하는 객체라는 것을 알 수 있어요. (client 는 OkHttpClient 를 의미해요)
위에서 이야기한 WorkerThread 에서 작업이 처리된다는 의미는 Dispatcher 에 미리 정의된 ThreadPoolExcutor 을 통해 executorService 를 사용하는 방식을 의미해요.
이제 Dispatcher 의 굵직한 로직들만 훓어 볼게요.
Dispatcher 는 enqueue 함수가 정의되어 있어요. 먼저 readyAsyncCalls 에 요청을 추가해둬요. 그리고 promoteAndExecute 함수를 실행해요. (Async 키워드가 들어가 있어서 벌써부터 무언가 정답 근처에 온 느낌이 들죠?!)
promoteAndExecute 함수내부에서는 readyAsyncCalls 를 interator 돌면서 하나하나 처리해요.
이때 굉장히 중요한 부분이 있어요. 아까 위에 정의되어있는 executorService(WorkerThread) 가 있었죠?!
asyncCall.executeOn(executorService) 로직을 보면 실행할 때 executorService 에서 실행되도록 파라미터로 넘기고 있네요.
아참 참고로 AsyncCall 객체는 Runnable 을 상속하고 있는 Runnable 객체에요.
AsyncCall 의 executeOn 로직을 살펴보면 파라미터로 넘겨받은 executorService 에 Runnable 객체인 자신을 실행시켜요. (이후에는 run 메서드가 호출되겠네요) 이때 WorkerThread 에서 동작할 것이라 보장할 수 있어요
AsyncCall 의 run 메서드를 보면 threadName 에 "OkHttp ${redactedUrl()}" 처리를 볼 수 있네요. 위에서 Retrofit2 사용하는 부분에 Thread.currentThread().name 를 출력하면 "OkHttp ${RequestUrl}" 이 나온다고 했는데 아래의 threadName 처리 때문에 그랬다는 것을 알 수 있네요.
여기서 알 수 있는 중요한 부분이 등장해요.
Dispatcher 의 enqueue 에서 실행되어야 WorkerThread 에서 동작할 것이라는 것을 보장할 수 있어요
따라서 execute 방식을 사용한다면 따로 WorkerThread 처리를 하지 않는다면 NetworkOnMainThreadException 이 발생해요.
엥 ?! 그냥 간단히 execute 을 사용하지 않으면 된다고 이야기 하면 되는거 아니야 !!! 라고 생각할 수 있어요.
맞는 말이에요 ㅎ..ㅎ
하지만 코루틴, RxJava 를 활용하면 우리가 명시적으로 enqueue 하거나 execute 를 호출하지는 않았을꺼에요. 왜냐면 라이브러리내부에서 처리해주고 있기 때문이에요.
해당 처리를 도와주는 클래스는 CallAdapter.Factory 클래스에요. get 함수를 통해 처리 방식을 핸들링 할 수 있어요.
이제 마법이 하나하나 드러나요.
첫번째 Retrofit2 의 기본 처리에요
Retrofit2 에서는 기본으로 Platform 클래스에서 DefaultCallAdapterFactory 를 제공해요
Retrofit interface 에 SkipCallbackExecutor 어노테이션을 사용하지 않고 있다면 callbackExecutor 를 호출하고 결과적으로 adapt 함수내에서 ExecutorCallbackCall 클래스를 생성하고 있네요,
(SkipCallbackExecutor 을 사용하면 어떤 처리 없이 원래의 call 을 사용한다는 사실도 알 수 있네요!)
ExecutorCallbackCall 클래스는 내부에서 delegate 객체를 이용해서 enqueue 를 구현하고 있네요.
결과적으로 retrofit2 를 기본 방법으로 활용하면 DefaultCallAdapterFactory 를 통해 WorkerThread 처리를 해준다는 것을 알 수 있어요. 마음 놓고 사용해도 괜찮겠네요
두번째 코루틴의 기본 처리에요.
HttpServiceMethod 클래스에서 아까 이야기 했던 callAdapter 를 통해 call 객체를 전달 받고, Continuation 객체를 와서 awaitNullable 혹은 await 함수에 넘기고 있네요. (isNullable 함수는 반환값이 Nullable(NoContent) 한지 체크하기 위한 용도에요)
혹시나 Retrofit2 에서 suspend 함수임을 아는 방법이 궁금하다면 김준비님의 코루틴 딥다이브 영상을 보시면 됩니다
await, awaitNullable 함수는 Body 비교해서 에러를 던져주는 정도 외에는 큰 차이가 없어서 await 메서드만 볼게요.
앗 await 내부에서 enqueue 처리를 해주고 있군요 결과적으로 WorkerThread 에서 동작한다고 볼 수 있어요!
코루틴도 편하게 사용해도 괜찮겠어요 ^^ (물론 awaitNullable 함수도 enqueue 를 구현하고 있어요!)
세번쨰 jakewharton 이 제공하는 CoroutineCallAdapterFactory 는 어떨까요 ?
Response 클래스를 사용하면 ResponseCallAdapter, 사용하지 않는다면 BodyAdapter 클래스로 처리하고 있네요.
짠~! 조금 길긴 하지만 핵심은 enqueue 를 모두 활용하고 있네요. 이것으로 기본으로 WorkerThread 에서 처리될 것이라는 것을 알 수 있습니다. 다만 완료된 상태를 boolean 으로 제공하는 CompletableDeferred 을 사용하고 있는데 코루틴에서 기본 처리가 되기 때문에 따로 사용하지 않아도 될 것 같네요. 또한 Retrofit2 에 CompletableFutureCallAdapterFactory 라는 이름으로 추가 되었기 때문에 더욱 사용할 필요가 없어진 것 같아요
마지막으로 RxJava 는 어떨까요 ?
create 와 createAsync 함수를 제공하고 있네요. 내부 구현이 무엇이 다른지 보죠.
isAsync 가 true 이면 CallEnqueueObservable 아니라면 CallExecuteObervable 를 만들어서 처리하고 있네요.
CallEnqueueObservable 클래스를 먼저보면, enqueue 처리를 하고 있네요!
결과적으로 WorkerThread 에서 실행될 것이라 보장할 수 있어요.
CallExecuteObervable 은 어떨까요? (이미 다들 눈치채셨겠지만)
execute 를 실행하고 있네요 결과적으로 WorkerThread 를 따로 처리해줘야합니다. create() 방식을 사용한다면 꼭 subscribeOn(Schedulers.io()) 처리가 필요할 것으로 보이네요.
여기까지 WorkerThread 처리가 기본으로 되는 이유에 대해 알아봤어요.
그럼 저는 20000
출근 준비하러 가볼게요 :)
'Android > Development Tips' 카테고리의 다른 글
Tips. Compose 성능 최적화 요약 (0) | 2024.06.29 |
---|---|
Compose. StateFlow + List 를 활용하여 Recomposition 할 때 Tips (0) | 2023.06.06 |
Summary MVI 직접 작성 해야/하지 않아야 하는 이유 (0) | 2023.01.05 |
확대, 축소, 이동, 회전 가능한 컴포즈 뷰 (0) | 2022.12.30 |
간단 Tips. FrameLayout 위에 Fragment 를 올리는 것보다 FragmentContainerView 에 Fragment 를 올리는 것이 안전한 이유 (0) | 2022.11.30 |