Coroutine 이란?

처음 코루틴을 접했을 때 코틀린의 '코'를 따서 'Ko'routine 인줄 알았지만, 사실 코루틴의 CoConcurrency라는 의미를 가지고있습니다. 즉, 동시성 프로그래밍 개념을 코틀린에 도입한 것이 코루틴 이라고 합니다.

코루틴은 javascript Promise와 조금 다른 성질을 가지고 있습니다. 코루틴은 스레드 위에서 실행되는데 여러가지 코루틴 1,2,3이 존재한다고 할 때 코루틴1을 실행하던 중 코루틴2가 실행되어도 실행중인 스레드를 정지하면서 컨텍스트 스위칭 개념으로 다른 스레드로 전환하는 것이 아니라 기존 스레드를 유지하며 기존 스레드에서 코루틴2를 실행하게 됩니다. 이후 코루틴1을 다시 실행할 때 저장해둔 코루틴1 상태를 불러와 다시 스레드에서 코루틴1을 실행하게 됩니다. 한마디로 스레드의 멈춤 없이 루틴을 돌릴 수 있게되며 이는 성능 향상을 기대할 수 있고 여러 스레드를 사용하는 것 보다 훨씬 적은 자원을 소모하게 됩니다.

CSP(Communicating Sequential Process) 모델 기반으로 작동하는 Coroutine

하지만 이런 Coroutine은 비동기 프로그래밍을 편리하게 작성하기 위한 강력한 문법이지만 처음 접했을 때 suspend, async, await, runBlocking, launch, coroutineScope, suspendCoroutine 등 생소한 용어들이 많다보니 어떤 상황에 어떤 코드를 작성해야할지 적응하는데 시간이 많이 걸린다. 하지만 비동기 프로그래밍이라는 개념 자체는 언어가 달라지더라도 같은 의미로 사용된다. 때문에 Javascript에서 비동기 프래그래밍을 많이 접해본 사람이라면 다양한 상황에 맞춰 Javascript Promise 예제와 Kotlin Coroutine 예제 코드를 나란히 놓고 비교해서 학습하고자 한다.

 

Promise vs Koroutine

Javascript에서 비동기 함수는 async 함수를 선언하고 Kotlin Coroutine에서는 suspend 함수를 선언해야한다.

Javascript

async function getDataFromServer() {
    const response = await apiClient.get('https://example.com');
    return response.data
}

Kotlin

suspend fun getDataFromServer(): Data {
    val response = apiClient.get("https://example.com/something")
    return response.data
}

interface ApiClient {
    suspend fun get(path: String): Response
}
data class Response(val data: Data)
  • Kotlin은 suspend, Javascript는 async 키워드를 함수 앞에 붙여서 사용합니다.
  • Javascript async 함수 내에서 호출되는 함수는 async의 유무와 상관없이 모든 메소드를 호출할 수 있지만, Kotlin suspend 함수 내에서 호출되는 함수는 suspend 함수만 호출이 가능합니다.
  • Javascript에서 비동기 함수를 호출할 때 await을 붙이지 않는다면, 결과를 기다리지 않고 다음 라인이 실행되는 반면, Kotlin에서 suspend fun 안에서 또다른 suspend fun 을 호출할 경우 자동으로 해당 suspend fun의 결과가 준비될 때 까지 다음 라인이 실행되지 않습니다.

 

여러개를 동시 수행

위에서 살펴본 예제는 비동기 요청을 한개만 수행하는 간단한 코드였지만, 실제 코드를 작성하다 보면 여러개의 비동기 요청을 동시에 수행해야하는 상황에 직면합니다.

단순하게 생각하면 아래 코드 같이 작성할 수 있습니다.

async function getDataFromMutipleServer() {
    const response1 = await apiClient.get('server1');
    const response2 = await apiClient.get('server2');
    return response1.data + response2.data
}

비동기 요청을 여러번 수행하지만 첫번째 요청이 끝나는 것을 기다렸다 두번째 요청을 순차적으로 수행하기 때문에 효율적이라고 볼 수 없습니다. 위 코드를 Kotlin으로 작성하면 아래와 같습니다.

suspend fun getDataFromMutipleServer(): Data {
    val response1 = apiClient.get("server1")
    val response2 = apiClient.get("server2")
    return response1 + response2
}

위 코드 역시 비효율적입니다. Javascript에서는 아래와 같이 이 문제를 해결하였습니다.

async function getDataFromMutipleServer() {
  const [response1, response2] = Promise.all([
    apiClient.get('server1'),
    apiClient.get('server2'),
  ]);
  return response1.data + response2.data;
}

Promise를 이용하여 여러개의 비동기 요청을 더 효율적으로 수행할 수 있게 되었습니다.
위 코드를 Kotlin으로 작성하면 아래와 같습니다.

suspend fun getDataFromMutipleServer(): Data {
    val deferredList = coroutineScope {
        val deferred1 = async { apiClient.get("server1") }
        val deferred2 = async { apiClient.get("server2") }
        listOf(deferred1, deferred2)
    }
    val deferred = deferredList.awaitAll()
    return deferred[0].data + deferred[1].data
}

 

결국 Javascript Promise는 Kotlin Deferred와 기능적으로 비슷하다고 볼 수 있습니다. (Java는 Future)

 

병렬처리 중 callback 처리

Javascript의 경우 async 함수의 결과를 callback 쪽으로 넘겨주기 위해 Promise를 사용하여 아래와 같이 처리할 수 있습니다.

// 일반 function
function getDataFromServer(callback) {
  const response = apiClient
    .get('https://example.com/someting')
    .then((response) => callback(null, response.data));
  return response.data;
}

비슷한 경우 Kotlin에서 아래와 같이 처리합니다.

fun getDataFromServer(callback: (Data) -> Unit) {
    // suspend 키워드가 붙지 않은 함수 내에서 suspend fun인 apiClient.get을 호출할 수 없습니다.
    // 이 경우 suspend fun을 호출할 수 있도록 코루틴을 생헝하고 해당 스코프 안에서 suspend fun을 수행합니다.
    runBlocking {
        val response = apiClient.get("server")
        callback(response.data)
    }
    
    logger.info("apiClient.get 수행 후 runBlocking 내부의 코드가 완전히 수행된 후 이 로그가 실행됩니다.")
}

위 예제에서 suspend 키워드가 붙지 않은 일반적인 함수에서는 suspend fun을 호출할 수 없고, runBlocking 함수를 이용하여 새롭게 코루틴을 생성하고 해당 코루틴 스코프 내에서 비동기 요청을 수행하게 됩니다.

Javascript async 함수 내에서 callback으로 결과를 돌려주는 함수를 기다리려면 보통 다음과 같이 Promise로 callback을 래핑합니다.

async function getDataFromServer() {
  const promise = new Promise((resolve, reject) => {
    apiCallbackClient.get('server', (response) => resolve(response));
  });
  const response = await promise;
  return response.data;
}

동일하게 Kotlin에서 suspend 함수 내부에서 callback 함수를 기다리려면 아래와 같이 suspendCoroutine을 사용합니다.

suspend fun getDataFromServer(): Data {
    val response = suspendCoroutine<Response> { continuation ->
        logger.info("Call 0")
        apiClient.get("server") { response ->
            logger.info("Call 2")
            // 성공적으로 결과를 받아온 경우 결과와 함께 코루틴의 진행을 재개
            continuation.resumeWith(Result.success(response))
        }
        logger.info("Call 1")
    }
    logger.info("Call 3")
    return response.data
}

위 코드가 수행될 떄 로그 메세지는 Call 0, 1, 2, 3 순서로 출력됩니다.