Android

Coroutine과 Retrofit을 같이 사용하면 enqueue를 쓰지 않아도 되는 이유는?

seokzoo 2022. 9. 13. 23:43
반응형

이 포스팅을 작성하는 이유

안드로이드를 처음 접할 때 retrofit을 이용해 서버와 통신할 경우 아래와 같은 코드를 작성하게됩니다.

interface UserApi {
    @GET("api/")
    suspend fun getUserList(
        @Query("page") page: Int,
        @Query("results") results: Int,
        @Query("seed") seed: Int,
    ): Call<UserData>
}

...

NetworkManager.UserApi
    .getUserList(page, MainActivity.USER_COUNT, seed)
    .enqueue(object : Callback<UserData>{
       override fun onResponse(call: Call<UserData>,
                               response: Response<UserData>) {
                    // 통신 성공시에 코드
                }

       override fun onFailure(call: Call<UserData>, t: Throwable) {
                    // 통신 실패시에 코드
                }
            })

그런데 Coroutine을 사용하여 통신할 경우에는 아래와 같이 작성하면 됩니다.

interface UserApi {
    @GET("api/")
    suspend fun getUserList(
        @Query("page") page: Int,
        @Query("results") results: Int,
        @Query("seed") seed: Int,
    ): Response<UserData>
}

...

override suspend fun remoteGetUserList(seed: Int, page: Int): List<UserData.Result> {
        // 서버를 통신해서
        val userListData = NetworkManager.UserApi
        .getUserList(page, MainActivity.USER_COUNT, seed)

        // 받은 데이터를 리턴
        return userListData.body()?.results ?: emptyList()
    }

... 

lifecycleScope.launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            submitList()
        }

Coroutine을 사용하게 되면, enqueue()나 execute()라는 비동기 코드를 "실행"하는 코드가 없어도 스스로 값을 가져오게 됩니다.

저는 이러한 코드가 이해가 되지 않았었고, 이를 통해 내부 코드를 따라가며 공부하는 계기가 되었습니다. 그리고 드디어 오늘에서야 정리가 돼서 블로그에 포스팅하게 됐습니다!

추가적으로 내부코드를 살펴보며 왜 안드로이드에서 서버 통신을 진행할 때 1번과 2번중 1번을 써야하는 이유에 대해서도 이야기 해보겠습니다.

// MainActivity

1. lifecycleScope.launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            submitList()
        }

2. CoroutineScope(Dispatchers.IO).launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            withContext(Dispatchers.Main) {
                submitList()
            }
        }

Retrofit의 내부 동작

먼저 저는 Retrofit을 이해하기 위해서 처음에는 디버깅 없이 코드를 따라가보며 예측을 해봤고, 디버깅하며 한줄씩 따라가보기도 했습니다. 그렇게 정리한 내용들을 아래에 적어보겠습니다. 그중 가장 중요하다고 생각하는 부분은 강조해서 쓰겠습니다.

자 이제 코드를 하나씩 따라가며, 위의 궁금즘을 풀어보겠습니다!

1. Retrofit.create()

현재 retrofit2 패키지 안에 있는 KotlinExtensions.kt를 보면 내부적으로 만들어주기 때문에, 타입을 명시해줄 시 create()안에 클래스 이름을 명시하지 않아도 됩니다.

// KotlinExtensions.kt
@file:JvmName("KotlinExtensions")

package retrofit2

import ...

inline fun <reified T> Retrofit.create(): T = create(T::class.java)

// object NetworkManager
private val retrofit = Retrofit.Builder()
        .baseUrl(BASE_URL)
        .addConverterFactory(GsonConverterFactory.create())
        .build()

val UserApi = retrofit.create(UserApi::class.java) // X
val UserApi: UserApi = retrofit.create()           // O

그렇다고 위의 방식이 틀린 것은 아니니, 사용하셔도 됩니다!

2. create() 메소드 내부

가장 먼저 create() 메소드의 내부를 살펴봅니다.

public <T> T create(final Class<T> service) {
    validateServiceInterface(service);
    ...

create() 메소드가 실행되면 validateServiceInterface()라는 메소드로 인터페이스(위의 예제에서는 UserApi)가 전달됩니다.

3. validateServiceInterface(service) 메소드 내부

private void validateServiceInterface(Class<?> service) {
    if (!service.isInterface()) {
      throw new IllegalArgumentException("API declarations must be interfaces.");
    }
    ...

전달 받은 인터페이스가 인터페이스인지 확인하고, 아니라면 IllegalArgumentException을 발생시킵니다.

4. 다시 create() 메소드로

validateServiceInterface() 메소드를 통해 인터페이스인지 체크를 한 뒤, 리턴값으로 리턴해줍니다. 그런데 리턴값에서 Proxy를 이용해 invoke를 수행해주는데, Proxy에 대해서 잠깐 알아보고 가야합니다.

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

Proxy는?

Proxy는 타겟의 기능을 확장하거나, 타겟에 대한 접근을 제어하기 위해 사용하는 클래스를 말합니다. 아래 예시를 봅니다.

interface Hello {
    fun sayHello(name: String): String
    fun sayHi(name: String): String
}

class HelloTarget : Hello {
    override fun sayHello(name: String): String {
        return "Hello $name"
    }

    override fun sayHi(name: String): String {
        return "Hi $name"
    }

}

Hello라는 인터페이스가 있고, 이를 구현한 HelloTarget 클래스가 있습니다. 이때 Proxy를 이용하면 Hello의 메소드를 변경하지 않고 대문자로 변환하는 부가기능을 추가할 수 있습니다.

class HelloUpperCase : Hello {
    private val helloTarget = HelloTarget()

    override fun sayHello(name: String): String {
        return helloTarget.sayHello(name).toUpperCase()
    }

    override fun sayHi(name: String): String {
        return helloTarget.sayHi(name).toUpperCase()
    }
}

HelloUppercase라는 Hello의 구현체를 따로 만들고, HelloTarget의 객체를 선언해줍니다. 그리고 타겟을 가져와 기능을 수행하고, 결과를 다시 대문자로 변환합니다.

클라이언트에서 Hello의 메소드를 호출하면 HelloTarget이 위임받아 원래의 일을 하고, toUpperCase()라는 메소드를 통해 대문자 변환이라는 부가 기능을 추가한 것입니다. 이처럼 Proxy를 사용하면 타겟 코드의 수정 없이 타겟의 기능을 확장하거나 부가기능을 추가해줄 수 있습니다!

Dynamic Proxy

그런데 위와 같은 Prxoy는 인터페이스를 직접 구현해야 하며, 중복이 발생한다는 단점이 있습다.
sayHello()와 sayHi() 중에서 한개만 확장하고 싶어도, 모든 메소드를 override해주어야 하며, 코드를 보면 중복된 코드가 많아 보입니다.

Dynamic Proxy는 위의 단점들을 보완해줍니다. Dynamic Proxy는 일일이 프록시를 생성하는게 아닌, 런타임 시점에 인터페이스를 구현하는 클래스 또는 인스턴스를 만들어주는 것을 뜻합니다.

Proxy.java의 Dynamic Proxy를 생성해주는 newProxyInstance()를 살펴겠습니다.


첫 번째 인자 : 프록시를 만들 클래스 로더
두 번째 인자 : 어떤 인터페이스에 대해 프록시를 만들 것인지 명시해줌
세 번째 인자 : InvocationHandler 인터페이스의 구현체
리턴 값 : 동적으로 만든 프록시 객체

여기서 살펴볼 것은 InvocationHandler입니다.
InvocationHandler는 invoke() 메소드 한개만 가지고 있는 인터페이스로, Dynamic Proxy로 생성될 Proxy에서 메소드가 호출되면 invoke() 메소드가 호출됩니다. 여기서 메소드가 확장될수 있으며, 사용자가 어떤 메소드를 호출했는지에 대한 정보를 인자로 전달해줍니다.

// retrofit dynamic proxy의 invoke() 메소드
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] 
// 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); }

첫 번째 인자 : 프록시 객체
두 번째 인자 : 메소드 객체(클라이언트에서 호출한 메소드)
세 번째 인자 : 메소드의 인자(클라이언트에서 메소드에게 전달한 인자)

Proxy의 기능에 따라 위의 코드를 보자면, method가 Object의 형태라면 메소드를 그대로 실행해줍니다. 아닌 경우 리턴 값을 3항 연산자를 통해 지정하는데, DefaultMethod인 경우 invokeDefaultMethod를 invoke() 해주고, 아닌 경우 loadServiceMethod()의 결과 메소드를 invoke() 해줍니다.

이렇게 해서 위의 platform은 Android가 담기고, platform.isDefaultMethod가 false가 나와 아래의 loadServiceMethod가 실행됩니다. isDefaultMethod는 자바에서 인터페이스 안에 내용이 있는 메소드를 선언할 때 사용되는 default 키워드를 가진 메소드인지를 판별해주는 함수입니다. 우리가 선언해준 인터페이스의 함수는 default가 아니기에 false가 나옵니다!

즉 정리해보자면, Dynamic Proxy를 통해 Retrofit을 위해 선언해준 interface에 대해서 동적으로 클래스를 생성해주는 역할을 하는 것이 Dynamic Proxy입니다.

5. loadServiceMethod(method).invoke(args)

private final Map<Method, ServiceMethod<?>> serviceMethodCache = new ConcurrentHashMap<>();

...

ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

serviceMethodCache는 Retrofit 클래스 최상단에 선언되어 있는 Map객체입니다.
loadServiceMethod() 메소드 에서 serviceMethodCache안에서 메소드에 대한 정보를 얻어옵니다.
처음 실행시 비어있기 때문에 null을 반환하여 아래 synchronized 블록을 실행합니다.
그 안에서 값이 저장되지 않은 채로 get을 하기때문에 result는 첫 값으로 null이며, 후에 if문을 실행합니다.
이제부터 중요한 parseAnnotations() 메소드가 등장합니다.

6. ServiceMethod.parseAnnotations()

메소드의 이름 그대로 우리가 인터페이스에 정의해 주었던 @GET, @POST 와 같은 어노테이션들을 파싱해줍니다.
그런데 이곳에서 해주진 않습니다. RequestFactory.parseAnnotations에서 해줍니다. 코드를 확인해보세요!

abstract class ServiceMethod<T> {
  static <T> ServiceMethod<T> parseAnnotations(Retrofit retrofit, Method method) {
    RequestFactory requestFactory = RequestFactory.parseAnnotations(retrofit, method);

    Type returnType = method.getGenericReturnType();
    if (Utils.hasUnresolvableType(returnType)) {
      throw methodError(
          method,
          "Method return type must not include a type variable or wildcard: %s",
          returnType);
    }
    if (returnType == void.class) {
      throw methodError(method, "Service methods cannot return void.");
    }

    return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);
  }

  abstract @Nullable T invoke(Object[] args);
}

RequestFactory.parseAnnotations에서 정규식을 통해 어노테이션을 파싱하고, 그에대한 정보를 담아 변수 requestFactory에 담습니다. 그리고 이전에 전달해 줬던 retrofit 객체, method정보와 함께 리턴 값으로 다시 HttpServiceMethod의 parseAnnotations에 전달합니다.
return HttpServiceMethod.parseAnnotations(retrofit, method, requestFactory);

7. HttpServiceMethod.parseAnnotations()

static <ResponseT, ReturnT> HttpServiceMethod<ResponseT, ReturnT> parseAnnotations(
      Retrofit retrofit, Method method, RequestFactory requestFactory) {
    boolean isKotlinSuspendFunction = requestFactory.isKotlinSuspendFunction;
    boolean continuationWantsResponse = false;
    boolean continuationBodyNullable = false;
    ...

여기 static method에 중요한 변수 하나가 있습니다. 바로 isKotlinSuspendFunction 입니다! 이 변수는 해당 함수가 suspend function 인지 확인하는 변수입니다.

또한 이 parseAnnotations 메소드에서 수행하는 중요한 부분이 있습니다.
가장 아래 부분에 있는 이 부분입니다!

if (!isKotlinSuspendFunction) {
      return new CallAdapted<>(requestFactory, callFactory, responseConverter, callAdapter);
    } else if (continuationWantsResponse) {
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForResponse<>(
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter);
    } else {
      //noinspection unchecked Kotlin compiler guarantees ReturnT to be Object.
      return (HttpServiceMethod<ResponseT, ReturnT>)
          new SuspendForBody<>(
              requestFactory,
              callFactory,
              responseConverter,
              (CallAdapter<ResponseT, Call<ResponseT>>) callAdapter,
              continuationBodyNullable);
    }

만약 suspend function이 아니라면 CallAdapted를 생성하여 리턴하고, Response 객체를 리턴값으로 가진다면 SuspendForResponse를, Body가 필요하다면 SuspendForBody를 생성하여 리턴해줍니다.

ServiceMethod<?> loadServiceMethod(Method method) {
    ServiceMethod<?> result = serviceMethodCache.get(method);
    if (result != null) return result;

    synchronized (serviceMethodCache) {
      result = serviceMethodCache.get(method);
      if (result == null) {
        result = ServiceMethod.parseAnnotations(this, method);
        serviceMethodCache.put(method, result);
      }
    }
    return result;
  }

후에 이 모든 정보들을 가지고 loadServiceMethod로 돌아가 result에 SuspendForResponse를 저장하고, method 정보와 함께 serviceMethodCache에 저장합니다. 즉, serviceMethodCache에는 메소드 정보, 그리고 실행에 대한 함수에 대한 정보(SuspendForResponse, callAdapter, callFactory, requestFactory, responseConverter)를 가지고 있습니다.

8. loadServiceMethod(method).invoke(args)

다시 create() 메소드의 리턴 값 프록시 생성으로 돌아가 invoke를 수행합니다.

return platform.isDefaultMethod(method)
                    ? platform.invokeDefaultMethod(method, service, proxy, args)
                    : loadServiceMethod(method).invoke(args);
              }
            });

loadServiceMethod() 메소드의 결과로 SuspendForResponse객체를 가지고, invoke를 수행하게 됩니다. 이 SuspendForResponse 클래스는 HttpServiceMethod안에 있으며,
이때의 invoke는 HttpServiceMethod.java 안의 SuspendForBody클래스 내부의 invoke() 메소드가 수행됩니다.

// HttpServiceMethod

@Override
  final @Nullable ReturnT invoke(Object[] args) {
    Call<ResponseT> call = new OkHttpCall<>(requestFactory, args, callFactory, responseConverter);
    return adapt(call, args);
  }

이때 adapt(call, args)에서 call은 이전의 정보들을 가지고 수행하게 됩니다.
requestFactory에서 수행했던 어노테이션 파싱, 메소드 정보, callFactory, responseConverter 등..

9. adapt

// HttpServiceMethod.java 내부의 class SuspendForBody 안의 adapt

protected Object adapt(Call<ResponseT> call, Object[] args) {
      call = callAdapter.adapt(call);

      //noinspection unchecked Checked by reflection inside RequestFactory.
      Continuation<Response<ResponseT>> continuation =
          (Continuation<Response<ResponseT>>) args[args.length - 1];

      // See SuspendForBody for explanation about this try/catch.
      try {
        return KotlinExtensions.awaitResponse(call, continuation);
      } catch (Exception e) {
        return KotlinExtensions.suspendAndThrow(e, continuation);
      }
    }

위의 adapt(call, args)를 통해 위의 adapt가 실행되는데, callAdapter의 adapt(call)을 실행합니다. 여기 생성된 call에는 다양한 정보가 있습니다.


이 call 객체 안에 requestFactory 안에 "GET"이라는 정보, BASE_URL 등 중요 정보가 담겨있습니다.

그리고 아래 아주 중요한 코드가 있습니다.

10. awaitResponse, suspendAndThrow

//noinspection unchecked Checked by reflection inside RequestFactory.
      Continuation<Response<ResponseT>> continuation =
          (Continuation<Response<ResponseT>>) args[args.length - 1];

      // See SuspendForBody for explanation about this try/catch.
      try {
        return KotlinExtensions.awaitResponse(call, continuation);
      } catch (Exception e) {
        return KotlinExtensions.suspendAndThrow(e, continuation);
      }
    }

여기서 continuation에는 우리가 코루틴을 실행할 코드가 있습니다. (멈춘 곳)


위와 같이 코루틴을 진행하며 멈춘 곳(11번째 줄)에 대한 정보가 continuation에 들어있습니다.
클래스 이름(RemoteDataSource), 멈춘 위치(11번 째 줄 ).

그리고 그 정보와 함께 awaitResponse 또는 Exception 발생 시 suspendAndThrow 가 실행됩니다.

이 함수들은 모두 이전에 create() 메소드가 들어있던 KotlinExtensions.kt에 위치해있습니다.

이중에서 awaitResponse() 메소드만 살펴보겠습니다.

11. awaitResponse

// KotlinExtensions.kt

suspend fun <T> Call<T>.awaitResponse(): Response<T> {
  return suspendCancellableCoroutine { continuation ->
    continuation.invokeOnCancellation {
      cancel()
    }
    enqueue(object : Callback<T> {
      override fun onResponse(call: Call<T>, response: Response<T>) {
        continuation.resume(response)
      }

      override fun onFailure(call: Call<T>, t: Throwable) {
        continuation.resumeWithException(t)
      }
    })
  }
}

자 첫 번째 의문점이 여기서 풀립니다. enqueue해주지 않은 코드들이 어떻게 실행되는 것일까?
답은 내부적으로 enqueue를 이미 진행해주고 있다! 입니다.
resume(), resumeWithException() 메소드를 통해 내부적으로 resumeWith() 메소드를 실행하고, 결과값을 받고 Result 클래스를 이용해 값을 전달합니다.

public inline fun <T> Continuation<T>.resume(value: T): Unit =
    resumeWith(Result.success(value))

public inline fun <T> Continuation<T>.resumeWithException(exception: Throwable): Unit =
    resumeWith(Result.failure(exception))

(Result 클래스에 대해서는 앞으로 공부해야할 것 같습니다)

이렇게 Coroutine과 Retrofit을 함께 사용할 때, 내부적인 동작을 알아봤습니다.

왜 1번이 맞을까?

// MainActivity

1. lifecycleScope.launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            submitList()
        }

2. CoroutineScope(Dispatchers.IO).launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            withContext(Dispatchers.Main) {
                submitList()
            }
        }

위의 코드에서 왜 첫 번째 코드가 맞는지 설명할 수 있으신가요??
결론 부터 말하자면, Retrofit Call의 메소드로 자체적 비동기 호출을 하고, 결과에 따라 콜백을 처리해줍니다. 즉, 우리가 Dispatchers를 지정해주어도 코루틴의 흐름을 타지 않고, 자바 쓰레드 풀에서 돌게 됩니다.

retrofit은 아시다 싶이 okHttp 위에서 돌아갑니다. 그래서 retrofit의 enqueue를 보면 okHttp의 enqueue를 호출합니다. 이를 내부 코드를 통해 확인해봅니다!

@Override
  public void enqueue(final Callback<T> callback) {
    Objects.requireNonNull(callback, "callback == null");

    okhttp3.Call call;
    Throwable failure;

    synchronized (this) {
      if (executed) throw new IllegalStateException("Already executed.");
      executed = true;

      call = rawCall;
      failure = creationFailure;
      if (call == null && failure == null) {
        try {
          call = rawCall = createRawCall();
        } catch (Throwable t) {
          throwIfFatal(t);
          failure = creationFailure = t;
        }
      }
    }

    if (failure != null) {
      callback.onFailure(this, failure);
      return;
    }

    if (canceled) {
      call.cancel();
    }

    call.enqueue(
        new okhttp3.Callback() {
          @Override
          public void onResponse(okhttp3.Call call, okhttp3.Response rawResponse) {
            Response<T> response;
            try {
              response = parseResponse(rawResponse);
            } catch (Throwable e) {
              throwIfFatal(e);
              callFailure(e);
              return;
            }

            try {
              callback.onResponse(OkHttpCall.this, response);
            } catch (Throwable t) {
              throwIfFatal(t);
              t.printStackTrace(); // TODO this is not great
            }
          }

          @Override
          public void onFailure(okhttp3.Call call, IOException e) {
            callFailure(e);
          }

          private void callFailure(Throwable e) {
            try {
              callback.onFailure(OkHttpCall.this, e);
            } catch (Throwable t) {
              throwIfFatal(t);
              t.printStackTrace(); // TODO this is not great
            }
          }
        });
  }

조금 길지만, okhttp의 call 객체를 생성하여, 그 call 객체를 이용해 enqueue를 실행합니다. 즉 retrofit의 enqueue는 okhttp의 enqueue를 실행하는겁니다!

okHttp의 enqueue

okHttp에서의 enqueue는 RealCall 클래스에 있습니다!

@Override public void enqueue(Callback responseCallback) {
    synchronized (this) {
      if (executed) throw new IllegalStateException("Already Executed");
      executed = true;
    }
    transmitter.callStart();
    client.dispatcher().enqueue(new AsyncCall(responseCallback));
  }

위의 코드를 보면 client.dispatcher().enqueue()를 실행합니다.
해당 enqueue를 들어가보면 아래와 같습니다.

void enqueue(AsyncCall call) {
    synchronized (this) {
      readyAsyncCalls.add(call);

      // Mutate the AsyncCall so that it shares the AtomicInteger of an existing running call to
      // the same host.
      if (!call.get().forWebSocket) {
        AsyncCall existingCall = findExistingCallWithHost(call.host());
        if (existingCall != null) call.reuseCallsPerHostFrom(existingCall);
      }
    }
    promoteAndExecute();
  }

synchronized 블록을 통해 AsyncCall을 처리해줍니다. 더 자세한 구현은 보지 않았지만, Deque를 이용해 runningAsyncCalls과 readyAsyncCalls를 구분해서 순차적으로 통신을 진행하는 것 같습니다.


결국 AsyncCall은 Runnable을 상속받아, 비동기 실행을 돕습니다.

AsyncCall 클래스 내부에 promoteAndExecute 메소드는 이름 그대로, readyAsyncCall을 runningAsyncCall로 승격시켜주고 executeOn메소드를 통해 실행시켜줍니다.

executeOn() 메소드에서는 executorService를 통해 인자로 runnable로 받아 쓰레드를 이용해 실행시켜줍니다.

Executor 구현체는 전달받은 작업을 큐에 넣은 후, 사용할 수 있는 스레드가 존재하면, 해당 스레드에 작업을 실행 하도록 합니다.

Executor의 내부 동작은 여기를 확인하시면 됩니다!

그래서

// MainActivity

1. lifecycleScope.launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            submitList()
        }

2. CoroutineScope(Dispatchers.IO).launch {
            userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
            withContext(Dispatchers.Main) {
                submitList()
            }
        }

그래서 2번의 코드처럼 Dispatchers를 바꿔주며 코루틴을 사용해도 사실은 아무런 영향을 미치지 못합니다. 안드로이드에서는 앞으로 lifecycle에 따라 코루틴을 자동으로 취소해주는 기능까지 추가된, lifecycleScope를 이용하도록 합니다! (lifecycleScope는 내부적으로 Dispathchers.Main.immediate를 사용하고 있습니다.


/**
 * [CoroutineScope] tied to this [LifecycleOwner]'s [Lifecycle].
 *
 * This scope will be cancelled when the [Lifecycle] is destroyed.
 *
 * This scope is bound to
 * [Dispatchers.Main.immediate][kotlinx.coroutines.MainCoroutineDispatcher.immediate].
 */
public val LifecycleOwner.lifecycleScope: LifecycleCoroutineScope
    get() = lifecycle.coroutineScope

실제로

실제 제 프로젝트 코드에 적용을 해본 결과, Dispatchers.Main.immediate 쓰레드로 presenter.loadDataList라는 네트워크 통신을 실행했지만, 무리없이 네트워크 통신이 됐습니다!(Retrofit 내부적으로 비동기 쓰레드를 이용해 통신해주었기 때문!)

CoroutineScope(Dispatchers.Main.immediate).launch {
                        userItemList.addAll(presenter.loadDataList(seed, FIRST_PAGE))
                        submitList()
                        isRefreshing = false
                        recyclerView.smoothScrollToPosition(0)
                    }

정리

Coroutine을 이용할 경우 내부적으로 enqueue를 진행해주고 있으며, enqueue는 retrofit에서 내부적으로 비동기 처리를 해주고 있으니 Dispatchers.IO를 사용하지 않아도 됩니다!

참고 및 출처

Proxy 참고 :
https://live-everyday.tistory.com/216
https://live-everyday.tistory.com/217
Retrofit suspend 컴파일 코드 :
https://rlaudals2374.medium.com/retrofit%EC%9D%98-%EA%B5%AC%ED%98%84%EC%B2%B4%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%9E%88%EC%9D%84%EA%B9%8C-e04b7952c408
Retrofit + Coroutine은 디스패처가 필요 없다:
http://dalinaum.github.io/android/2021/01/28/retrofit-does-not-need-dispachers-io.html

반응형