Retrofit2와 Coroutines 사용 시 스케줄러는 어떻게 처리할까? - 내부 코드로 알아보자.



개인 광고 영역

Retrofit 사용 시 알아두면 좋은 것이라는 주제로 만든 영상이다.

별건 아니고, Retrofit의 통신 방식 중 coroutines을 활용했을때 알아두면 좋은 부분을 정리했던 영상이다.

이 글에서 텍스트로 한 번 더 정리하고, 이 글에 도움 될 문서는 아래와 같다.


이 글에서 알아볼 내용

  • Corotuines과 Retrofit2을 함께 사용할 때 스케줄러 처리가 궁금하다면 읽어보시길
  • Retrofit2의 KotlinExtensions.kt를 알아본다.
  • suspendCancellableCoroutine에 대해서 알아본다.


Retrofit2의 coroutines 활용 방법

Retrofit2의 Coroutines 활용 방법은 간단한데, 별도의 Adapter의 추가도 필요치 않고, 정의할 때 아래와 같이 interface에 suspend로 시작하는 코드만 적어주면 Coroutines을 사용할 수 있다.

interface FlickrApi {

    @GET("?method=flickr.photos.getInfo&format=json&nojsoncallback=1")
    suspend fun loadPhotoDetail(
        @Query("photo_id") photoId: String,
        @Query("api_key") apiKey: String = BuildConfig.FLICKR_API_KEY
    ): PhotoDetail
}

이 코드에 연결해 사용하는 ViewModel에서 아래와 같이 호출만 해주면 된다.

fun loadDetail(photoId: String) = viewModelScope.launch {
    loadDetailRepository.loadDetail(photoId)
}

사실 Retrofit2의 Coroutines 사용은 매우 간단하다.

별도의 스케쥴러를 지정하지 않아도 동작하고, 알아서 UI에서 사용할 수 있도록 만들어준다. 이 글에서는 그 이유를 조금 더 파악해 보도록 한다.


별도의 스케쥴러를 지정하지 않았지만 동작한다.

간혹 블로그에 작성되어 있는 문서에 보면 아래와 같이 withContext를 직접 지정하는 경우가 있다.

fun loadDetail(photoId: String) = viewModelScope.launch {
    withContext(Dispatchers.IO) {
        loadDetailRepository.loadDetail(photoId)
    }
}

사실 이렇게 할 필요가 없다. 만약 이러한 문서를 보았다면 잘못 알고 사용한 예에 해당한다.

간단하게 설명하면 아래와 같다.

  • loadDetail을 UI에서 호출한다.
  • viewModelScope.launch {}는 UI 스케쥴러를 기본적으로 사용한다.
  • loadDetailRepository.loadDetail 함수를 호출한다. 아직까지는 UI 스케쥴러이다.
  • interface로 작성한 suspend fun loadDetail()은 Retrofit2의 내부 코드에서 IO를 알아서 처리하고, 이를 리턴해주는 코드를 가지고 있다.

결국 Retrofit2를 사용해 coroutines 의로 값을 불러온다면 알아서 새로운 쓰레드를 생성해 내부에서 데이터를 불러오는 작업을 하고, 이를 UI로 바꿔준다.

UI로 바꿔주는 이유는 Retrofit2의 enqueue를 사용해보았다면 알 수 있다.

enqueue를 직접 사용하는 경우 2개의 결과를 리턴해줌을 알 수 있는데,

  • onResponse
  • onFailure

이 두개의 함수를 사용해보았다면 알 수 있을 것이다. UI에서 바로 값을 사용해도 문제가 없다는 것을

제이크 와튼이 만들었던 Retrofit2 Adapter 역시 이를 활용했었다.

Kotlin Coroutine Adapter

지금은 이를 활용할 필요는 없어졌지만…


Retrofit2에 포함된 kotlin 관련 코드를 보기 전에

KotlinExtensions.kt를 살펴보면 이유를 알 수 있다.

하나 더 알아두면 좋은 것이 suspendCancellableCoroutine이다.

Retroift2의 enqueue와 suspendCancellableCoroutine을 합쳐 만들었다.

원리는 간단한데, suspendCancellableCoroutine는 어떠한 응답이 오기 전까지 코루틴을 대기시킨다. 이 방법을 활용하면 RxJava든, 별도의 Thread든 원하는 응답을 코루틴으로 바꿔치기할 수 있다.

과거에 작성했던 글에서도 볼 수 있는데, RxJava와 Coroutine 함께 사용하기라는 삽질?에서 확인할 수 있다.

참고로 이 suspendCancellableCoroutine을 사용하면 필수로 invokeOnCancellation을 작성해야 한다.


Retrofit2의 KotlinExtensions을 살펴보자

KotlinExtensions.kt에는 3개의 await() 관련 코드를 제공한다. Null을 포함하는 제네릭과 Null을 허용하지 않는 제네릭 모두 포함되어 있는데, 방법은 모두 동일하니 이중 하나만 살펴보자.

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)
      }
    })
  }
}

매우 긴 것처럼 보이지만 핵심 코드는 아래와 같다.

  • suspendCancellableCoroutine을 사용하고 있다.
  • Retrofit2의 enqueue를 활용하고 있다.

Retrofit2의 enqueue는 어떠한 네트워크 처리 후 이에 대한 응답을 UI에서 바로 사용할 수 있는 형태로 자동으로 변경해 준다. 결국 위 코드의 onResponse, onFailure으로 들어온 값을 suspendCancellableCoroutine의 응답을 통해 넘겨주는데, 자연스럽게 IO에서 네트워크 처리 후 UI에서 처리할 수 있는 값으로 넘겨준다.

그러니 별도의 withContext를 이용해 Dispatchers.IO와 같은 스케줄러를 지정해 줄 필요성이 없는 것이다.


그럼 언제 withContext(Dispatchers.IO)를 지정할까?

일반적인 경우엔 IO를 지정할 필요성이 없다. IO -> UI -> IO로 돌려주는 과정을 하기 때문에 일반적인 케이스에서는 불필요하다.

하지만 Room을 이용한 백업을 하거나, 데이터 캐싱을 하거나, 처리해야 할 데이터의 양이 많을 경우 UI보다는 IO가 좋다. 이러한 경우라면 IO로 한 번 더 감싸는 건 문제가 없다.

정말 단순하게 사용하는 경우라면 딱히 필요치는 않으니 굳이 IO 스케줄러로 변경할 필요성은 없다.


마무리

이 글은 작년에 촬영한 유튜브에 대한 풀이 글이다.

지금까지 작성한 효과적인 코틀린 활용의 연장선으로 작성한 글이기도 하다.

사실 별건 없지만 알고 쓰면 매우 도움 되기에 정리해두는 글에 해당한다.


이 글만 보기 아쉽죠. Effective Kotlin 더 보기

지금까지 작성한 EffectiveKotlin 관련 글은 태그로 모아 링크를 추가한다.

더 보기 - Effective Kotlin 관련 글



About Taehwan

My name is Taehwan Kwon. I have developed Android for 6 years and blog has been active for eight years.

Comments