개발언어/Kotlin

코루틴은 정말 멀티 스레드 보다 가벼울까?

eodevelop 2025. 4. 9. 16:34

코루틴의 경량성을 실험하게 된 계기

코틀린을 써보기도 전에 많이 듣던 얘기는 코틀린의 코루틴이 좋다는 이야기였습니다. 그런 이야기를 늘으니 문득 궁금증이 생겼습니다. 얼마나 좋길래 이렇게 유명하지?!

아무래도 궁금하다면 직접 테스트해보는 게 가장 좋지 않을까란 생각에 직접 멀티스레드와 비교해보면서 테스트 해보려 합니다.

코루틴이 코틀린 말고도 다른 언어에서도 지원하는 기술자체의 명칭이란 건 나중에 알게 되었습니다;; 물론 코틀린은 라이브러리나 프레임워크가 아닌 언어 차원에서 지원한다는 차이점이 존재하긴 합니다.


멀티스레드 VS 코루틴

비교 조건은 아래와 같습니다.

  1. 코루틴과 멀티 스레드 모두 사용 가능한 모든 스레드를 사용한다.
  2. 멀티스레드와 코루틴 모두 1만 개의 작업을 생성한다.
  3. 하나의 작업당 0.01초의 Sleep을 준다.
  4. 시작 시 메모리와 작업 종료 직후 메모리를 비교해서 메모리를 측정한다.

멀티스레드 코드

fun threadTask() {
    Thread.sleep(10)
}

fun measureThreadPerformance() {
    val threadCount = 10000
    val executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())

    val startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()

    for (i in 0 until threadCount) {
        executor.submit {
            threadTask()
        }
    }

    executor.shutdown()
    executor.awaitTermination(1, TimeUnit.MINUTES)

    val endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
    println("Threads - Memory used: ${(endMemory - startMemory) / 1024} KB")
}

fun main() {
    measureThreadPerformance()
}

결과 (5회 평균):

Threads - Memory used: 6588 KB
Threads - Memory used: 6138 KB
Threads - Memory used: 6588 KB
Threads - Memory used: 6145 KB
Threads - Memory used: 5238 KB

평균: 6139.4 KB


코루틴 코드

suspend fun coroutineTask() {
    delay(10)
}

fun measureCoroutinePerformance() = runBlocking {
    val coroutineCount = 10000

    val startMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()

    val jobs = List(coroutineCount) {
        launch {
            coroutineTask()
        }
    }

    jobs.forEach { it.join() }

    val endMemory = Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory()
    println("Coroutines - Memory used: ${(endMemory - startMemory) / 1024} KB")
}

fun main() {
    measureCoroutinePerformance()
}

결과 (5회 평균):

Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 5224 KB
Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 4772 KB
Coroutines - Memory used: 4772 KB

평균: 4862.4 KB

코루틴이 약 20% 정도 더 적은 메모리를 사용하는 것을 확인할 수 있었습니다.


의문점

  1. 코루틴은 아무리 반복해도 5224KB 혹은 4772KB 두 가지 값만 확인됨.
  2. 실행 속도도 코루틴이 체감상 훨씬 빠름.

얼마나 더 빠른가?

시간 측정을 추가한 결과:

멀티스레드

Threads - Time taken: 13282 ms
Threads - Time taken: 14081 ms
Threads - Time taken: 13770 ms
Threads - Time taken: 13503 ms
Threads - Time taken: 13347 ms

평균: 13596.6 ms

코루틴

Coroutines - Time taken: 153 ms
Coroutines - Time taken: 153 ms
Coroutines - Time taken: 154 ms
Coroutines - Time taken: 141 ms
Coroutines - Time taken: 131 ms

평균: 146.4 ms

약 93배 가까운 속도 차이 발생


이게 말이 되나?

코루틴은 운영체제 스레드가 아닌 사용자 수준 스레드로 관리되며, 스레드 스위칭 오버헤드가 적음. 하지만 아무리 그래도 차이가 너무 크다는 느낌.

그 이유는 바로 Thread.sleep(10)delay(10)동작 방식 차이 때문!

  • Thread.sleep(10)스레드를 차단(blocking)
  • delay(10)스레드를 차단하지 않고 비동기로 기다림 (suspending)

결과적으로 코루틴은 스레드를 차단하지 않기 때문에 훨씬 더 빠르게 동작함.


CPU를 많이 사용하는 작업 시엔 어떻게 될까?

변경된 Task 코드:

fun task() {
    var result = 0L
    for (i in 1..1_000_000) {
        result += i
    }
}

결과

멀티스레드

Threads - Time taken: 427 ms
Threads - Time taken: 474 ms
Threads - Time taken: 421 ms
Threads - Time taken: 420 ms
Threads - Time taken: 602 ms

평균: 468.8 ms

코루틴

Coroutines - Time taken: 3452 ms
Coroutines - Time taken: 3579 ms
Coroutines - Time taken: 3657 ms
Coroutines - Time taken: 3567 ms
Coroutines - Time taken: 3620 ms

평균: 3575.0 ms

CPU 연산이 많은 작업에서는 멀티스레드가 약 8배 더 빠름


코루틴은 왜 메모리 사용량이 일정한가?

  • 멀티스레드는 각각의 스레드가 자체 스택 메모리를 보유하고 있으며, JVM의 GC와 메모리 변동성에 따라 사용량이 들쭉날쭉함.
  • 코루틴힙에 메모리를 할당하여 보다 일정하고 효율적인 메모리 사용이 가능.

정확한 원인이라기보단, 여러 글을 읽고 내린 개인적인 결론입니다. 더 정확한 정보를 아시는 분은 댓글로 알려주시면 감사하겠습니다!


결론

  • 코루틴은 멀티스레드에 비해 더 작은 메모리를 사용함
  • 일반적인 작업에서는 코루틴이 훨씬 빠르지만, CPU를 많이 사용하는 작업에서는 오히려 멀티스레드가 유리할 수도 있음

이상입니다. 긴 글 읽어주신 분들께 감사합니다! 🙏