03. Coroutine 구조화 수정하기

배경

과거 프로젝트에서 Coroutine을 적극 이용하여 다운로드 수를 최적화 시킨 경험이 있다.

해당 경험을 통해 Coroutine의 이해도에 따라 최적화의 수준이 달라진다는 것을 느꼈고,
강의를 듣고 공부하여 예전에 비해 많은 것을 배웠다.

다만 강의를 듣고 문득 생각해보니,
배운대로라면 나의 최적화 코드가 내가 의도한 바와 다르게 동작할 것 같다는 느낌이 들었고,

이를 다시 살펴보는 시간을 가지고자 한다.

결과적으로 내가 의도한 대로 구조화가 되지 않았다.

기존 코드의 양이 방대하다보니, 간단하게 설명하고자 새로운 프로젝트를 만들어 설명하는 점 양해 바란다.

  • 실습 코드

의도

우선 list<데이터<각 데이터마다 다운로드 받아야 하는 것들>> 이런 식으로 데이터가 들어온다.

listOf(listOf())라고 생각하면 된다.

의도한 바는

  1. 이 데이터를 5개씩 쪼갠다
  2. 데이터 안의 파일들을 동시에 다운받는다 (순차X)
  3. 5개의 데이터에 대해서 동시에 2번 작업을 진행한다 (순차 X)
  4. 5개의 데이터에 대해서 모두 작업이 끝난 것이 확인되면 다음 5개의 작업에 대해서 이를 진행한다.

위와 같다.

기존 코드

코드와 결과를 먼저 보고서 설명을 시작하고자 한다.

// file: "Improperly structured Coroutine.kt"
// 병렬 작업하기
fun main() = runBlocking{
    // Example Data
    val list = listOf(
            // 0번 인덱스는 무시하시면 됩니다.
            // 착업은 각 작업당 세부 작업 1~5까지 존재하는 걸로 설정
            listOf("작업 1", "작업 1-1", "작업 1-2", "작업 1-3", "작업 1-4", "작업 1-5"),
            listOf("작업 2", "작업 2-1", "작업 2-2", "작업 2-3", "작업 2-4", "작업 2-5"),
            listOf("작업 3", "작업 3-1", "작업 3-2", "작업 3-3", "작업 3-4", "작업 3-5"),
            listOf("작업 4", "작업 4-1", "작업 4-2", "작업 4-3", "작업 4-4", "작업 4-5"),
            listOf("작업 5", "작업 5-1", "작업 5-2", "작업 5-3", "작업 5-4", "작업 5-5"),
            listOf("작업 6", "작업 6-1", "작업 6-2", "작업 6-3", "작업 6-4", "작업 6-5"),
            listOf("작업 7", "작업 7-1", "작업 7-2", "작업 7-3", "작업 7-4", "작업 7-5"),
            listOf("작업 8", "작업 8-1", "작업 8-2", "작업 8-3", "작업 8-4", "작업 8-5"),
            listOf("작업 9", "작업 9-1", "작업 9-2", "작업 9-3", "작업 9-4", "작업 9-5"),
            listOf("작업 10", "작업 10-1", "작업 10-2", "작업 10-3", "작업 10-4", "작업 10-5"),
            listOf("작업 11", "작업 11-1", "작업 11-2", "작업 11-3", "작업 11-4", "작업 11-5"),
            listOf("작업 12", "작업 12-1", "작업 12-2", "작업 12-3", "작업 12-4", "작업 12-5"),
            listOf("작업 13", "작업 13-1", "작업 13-2", "작업 13-3", "작업 13-4", "작업 13-5"),
            listOf("작업 14", "작업 14-1", "작업 14-2", "작업 14-3", "작업 14-4", "작업 14-5"),
            listOf("작업 15", "작업 15-1", "작업 15-2", "작업 15-3", "작업 15-4", "작업 15-5"),
            listOf("작업 16", "작업 16-1", "작업 16-2", "작업 16-3", "작업 16-4", "작업 16-5"),
            listOf("작업 17", "작업 17-1", "작업 17-2", "작업 17-3", "작업 17-4", "작업 17-5"),
            listOf("작업 18", "작업 18-1", "작업 18-2", "작업 18-3", "작업 18-4", "작업 18-5"),
            listOf("작업 19", "작업 19-1", "작업 19-2", "작업 19-3", "작업 19-4", "작업 19-5"),
            listOf("작업 20", "작업 20-1", "작업 20-2", "작업 20-3", "작업 20-4", "작업 20-5"),
    )

    withContext(Dispatchers.IO){
        // 작업 크기를 5개씩으로 쪼개서 동시처리
        val job = list.chunked(5).map {
            launch {
                // 각 작업들에 대해서 돌아가면서
                it.forEach {
                    withContext(Dispatchers.IO) {
                        // 세부 작업 1~5번까지 각각에 대해서 Coroutine 빌더 함수를 호출하여 동시에 처리
                        val task1 = launch {
                            println(it[1])
                        }
                        val task2 = launch {
                            println(it[2])
                        }
                        val task3 = launch {
                            println(it[3])
                        }
                        val task4 = launch {
                            println(it[4])
                        }
                        val task5 = launch {
                            println(it[5])
                        }
                        joinAll(task1, task2, task3, task4, task5)
                    }
                }
            }
        }
        job.joinAll()
    }
}
  • 결과

    잘못된 결과
    분명 작업을 5개씩 쪼갰지만 11과 같은 작업이 초반부터 바로 나오는 것을 볼 수 있다.

  • 문제점

    결론적으로 핵심은 job을 joinAll()로 처리하는 부분이 문제다.

    기본적으로 join()의 경우 launch 등 코루틴 빌더를 통해 만들어진 Job이 끝날 때까지 기다리게 된다.

    그리고 joinAll은 이러한 Job들이 모두 끝날 때 까지 기다린다.

    그런데 본 코드에서 job.joinAll()을 하게 되면, 5개로 나누어서 처리하기 위해 나오는 모든 Job을 기다리겠다는 의미인데,
    본래 의도한 바는 5개로 나뉜 한 덩어리의 청크가 완료될 때까지 기다렸다가 다음 것을 진행하는 것이 본 의도였으니,
    의도와 부합하지 않게 진행이 되게 된다.

    또한 세부작업을 처리하는 부분에 존재하는 joinAll()은 오히려 각 세부 작업들 간 의존성이 없어 어떤 순서대로 처리되든 상관 없으므로,
    joinAll()을 할 필요가 없다.

  • 해결책

    5개로 나눈 청크에 대해서 나오는 Job List 들을 joinAll로 한꺼번에 묶어서 대기하는 것이 아니라, launch를 통해 생성된 Job 각각에 대해서 join을 진행해 5개에 대한 세부 작업이 다 끝날 때까지 기다리도록 하여,
    map이 다음 인덱스로 넘어가지 않도록 해야 한다.

정정된 코드

// file: "Properly structured Coroutine.kt"
// 병렬 작업하기
fun main() = runBlocking{
    // Example Data
    val list = listOf(
            // 0번 인덱스는 무시하시면 됩니다.
            // 착업은 각 작업당 세부 작업 1~5까지 존재하는 걸로 설정
            listOf("작업 1", "작업 1-1", "작업 1-2", "작업 1-3", "작업 1-4", "작업 1-5"),
            listOf("작업 2", "작업 2-1", "작업 2-2", "작업 2-3", "작업 2-4", "작업 2-5"),
            listOf("작업 3", "작업 3-1", "작업 3-2", "작업 3-3", "작업 3-4", "작업 3-5"),
            listOf("작업 4", "작업 4-1", "작업 4-2", "작업 4-3", "작업 4-4", "작업 4-5"),
            listOf("작업 5", "작업 5-1", "작업 5-2", "작업 5-3", "작업 5-4", "작업 5-5"),
            listOf("작업 6", "작업 6-1", "작업 6-2", "작업 6-3", "작업 6-4", "작업 6-5"),
            listOf("작업 7", "작업 7-1", "작업 7-2", "작업 7-3", "작업 7-4", "작업 7-5"),
            listOf("작업 8", "작업 8-1", "작업 8-2", "작업 8-3", "작업 8-4", "작업 8-5"),
            listOf("작업 9", "작업 9-1", "작업 9-2", "작업 9-3", "작업 9-4", "작업 9-5"),
            listOf("작업 10", "작업 10-1", "작업 10-2", "작업 10-3", "작업 10-4", "작업 10-5"),
            listOf("작업 11", "작업 11-1", "작업 11-2", "작업 11-3", "작업 11-4", "작업 11-5"),
            listOf("작업 12", "작업 12-1", "작업 12-2", "작업 12-3", "작업 12-4", "작업 12-5"),
            listOf("작업 13", "작업 13-1", "작업 13-2", "작업 13-3", "작업 13-4", "작업 13-5"),
            listOf("작업 14", "작업 14-1", "작업 14-2", "작업 14-3", "작업 14-4", "작업 14-5"),
            listOf("작업 15", "작업 15-1", "작업 15-2", "작업 15-3", "작업 15-4", "작업 15-5"),
            listOf("작업 16", "작업 16-1", "작업 16-2", "작업 16-3", "작업 16-4", "작업 16-5"),
            listOf("작업 17", "작업 17-1", "작업 17-2", "작업 17-3", "작업 17-4", "작업 17-5"),
            listOf("작업 18", "작업 18-1", "작업 18-2", "작업 18-3", "작업 18-4", "작업 18-5"),
            listOf("작업 19", "작업 19-1", "작업 19-2", "작업 19-3", "작업 19-4", "작업 19-5"),
            listOf("작업 20", "작업 20-1", "작업 20-2", "작업 20-3", "작업 20-4", "작업 20-5"),
    )
    withContext(Dispatchers.IO){
        list.chunked(5).map {
            // 다섯개의 데이터가 하나로 묶인 청크를 대상으로 작업을 진행하는 코루틴 생성
            val task = launch {
                it.forEach {
                    launch {
                        launch {
                            println(it[1])
                        }
                        launch {
                            println(it[2])
                        }
                        launch {
                            println(it[3])
                        }
                        launch {
                            println(it[4])
                        }
                        launch {
                            println(it[5])
                        }
                        // 오히려 아래의 joinAll 작업은 다른 데이터들과 세부 작업 간에 의존성이 없으므로,
                        // 불필요한 기다림이다.
                        // 이게 있으면 n번 데이터의 세부 작업이 모두 끝날 때까지 n+1번의 forEach문으로 넘어가지 못하며,
                        // 동시성이 떨어진다.
                        //joinAll(task1, task2, task3, task4, task5)
                    }
                }
            }
            // 이렇게 해서 5개의 데이터에 대해 각 세부작업이 모두 완료될 때까지,
            // map이 다음 인덱스로 넘어가지 않도록 한다.
            task.join()
        }
    }
}
  • 결과

    수정된 결과
    16,17,18,19,20번 데이터 내부에서 작업의 세부 사항의 순서와 관계 없이 작업을 진행하는 것을 알 수 있다.


© 2025. Na2te All rights reserved.