시작이 반

[Kotlin] Coroutine을 사용하여 API 병렬 처리 본문

Programming/Kotlin

[Kotlin] Coroutine을 사용하여 API 병렬 처리

G_Gi 2023. 4. 30. 21:26
SMALL

여러개의 코드를 동시에 실행시키기 위해서는 Coroutine을 알기 전에는 쓰레드라는 개념밖에 생각나지 않았다.
 
쓰레드는 다수의 작업을 동시에 처리할 수 있으며, 일반적으로 쓰레드는 선점형으로 멀티태스킹이 가능하다. 하나의 작업이 한 쓰레드에서 발생하고 여러개의 작업을 동시 실행시키기위해 멀티 쓰레드를 사용한다. 멀티쓰레드 환경에서 CPU가 여러 Task를 바꿔 실행하기 위해 Context Switching을 하게 되고 이때 현재 PCB, 다음 PCB등에 대한 정보를 적재시키기는 등 Overhead가 매우 커질 수 있다.
 
Coroutine 또한 여러 작업을 병렬적으로 처리하는 방법으로 비선점형으로 멀티태스킹이 가능하다. suspend와 resume을 사용하여 서브루틴을 구성하고 하나의 쓰레드에서 여러개의 실행 흐름을 관리하며 Context Switching을 최소화 하기 떄문에 쓰레드에 비해 적은 Overhead을 유발하고 빠르게 처리할 수 있다.
 
 그럼 Coroutine을 사용하여 여러 작업을 동시에 처리해 보자.
 
Coroutine을 사용하기 위해서는 Coroutine Scope에서 사용이 가능하다. 이러한 CoroutineScope내에서 Coroutine을 사용하여 병렬 수행이 가능하다. 
 

launch와 async를 사용해보자.

launch는 단순히 코루틴을 실행하는데 사용되며, 작업 결과를 반환하지 않는다. 따라서 launch를 사용하면 코루틴이 백그라운드에서 실행되며, 해당 작업이 완료될 때까지 기다릴 필요가 없다.
 
반면, async는 작업을 실행하고 해당 작업의 결과를 반환하는 Deferred 객체를 생성한다. Deferred 객체는 작업이 완료될 때까지 대기하고 결과를 반환할 수 있다. 이는 일반적으로 여러 작업을 수행하고 결과를 모아서 반환할 때 유용하다.
 
 

반환이 필요 없는 경우 - lauch

아래와 같은 API 가 있다고 가정해보자.

    suspend fun callApi(): Long {
        var i = (10..1000).random().toLong()
        println(i)
        delay(i)
        return i
    }

API를 병렬적으로 호출하기 위해서는 Collection을 통해서 처리를 해야 한다.
for문을 통해서 처리하게 되면 하나의 작업이 끝날때 까지 기다리게 됨으로 병렬적으로 처리가 되지 않는다.
 
아래 코드는 순차적으로 API를 호출하게 된다.
( join은 launch작업을 기다린다는 뜻이다.)

    @Test
    fun listTest() = runBlocking{
        val time2 = measureTimeMillis{

            for(i in 1..10){
                val job = launch { callApi() }
                job.join()
            }
        }

        println()
        println("time2 : ${time2}")

    }

실행 결과

201
830
675
595
696
540
472
651
525
412

time1 : 5728

Collection을 통해서 API를 동시에 호출할 수 있다.

    @Test
    fun listTest() = runBlocking{

        val time = measureTimeMillis{
            val jobs = List(10) { launch { callApi() } }
            jobs.joinAll()
        }

        println()
        println("time1 : ${time}")
    }

실행 결과

515
28
297
931
431
377
386
152
87
677

time1 : 975

 
 
 
보통 API는 json형태로 response를 주게 되기 때문에 async를 사용하여 결과 값을 가져와보자.
물론 여기서도 for문을 사용하면 병렬 처리가 되지 않기 때문에 Collection을 통해서 동시에 처리를 해준다.
async는 await를 통해서 작업을 기다린다. Collection에 대해서는 awaitAll을 사용할 수 있다.
 
아래와 같은 호출했을때 시간을 응답값으로 주는 api가 있다고 해보자.

    suspend fun callApi2(): Map<String, Long> {
        val response = mutableMapOf<String, Long>()
        var i = (10..1000).random().toLong()
        delay(i)
        response["time"] = i
        return response
    }

 
async awaitAll을 통해서 해당 응답을 요청을 동시에 처리할 수 있다.

    @Test
    fun listTest() = runBlocking{
        val time3 = measureTimeMillis{
            val list = List(10) { async { callApi2() } }
            val awaitAll = list.awaitAll()
            println(awaitAll.toString())
        }

        println()
        println("time1 : ${time3}")

    }

실행 결과

[{time=560}, {time=717}, {time=619}, {time=969}, {time=461}, {time=556}, {time=205}, {time=76}, {time=579}, {time=522}]

time1 : 1024

 
즉, API 를 동시에 처리하고 응답값을 받기 위해서는 Collection에 async를 통해서 Coroutine을 수행시키고 응답값을 다 받을 때까지 awaitAll을 사용한다.

LIST