Language/Kotlin

Coroutine과 Java Virtual Thread 비교

kkang._.h00n 2026. 7. 10. 19:00

 

코루틴이란

Co(협력) + Routine(작업)의 합성어로, 이름 그대로 여러 작업이 협력하여 동시에 진행하는 비동기 처리 프로그래밍 패턴이다. 

 

중단과 재개

fun main() {
    println("START")
    newRoutine()
    println("END")
}

fun newRoutine() {
    val num1 = 1
    val num2 = 2
    println("${num1 + num2}")
}

===========================

START
3
END

 

다음과 같이 2개의 루틴(함수)가 있으며, 굉장히 직관적이고 결과도 예상이 간다.

 

fun main(): Unit = runBlocking {
    //0. main 코루틴 실행, START 출력
    println("START")
    
    //1. newRoutine 코루틴 생성
    launch {
        newRoutine()
    }
    
    yield() //2. main 코루틴 중단 -> newRoutine 코루틴 실행
    println("END") //4. main 코루틴 재개, END 출력
    //5. main 코루틴 종료 후, 아직 끝내지 못한 newRoutine 코루틴 재개
}

//중단이 가능한 suspend 함수
suspend fun newRoutine() {
    val num1 = 1
    val num2 = 2
    
    yield() //3. newRoutine 코루틴 중단 -> main 코루틴으로 복귀
    println("${num1 + num2}") //6. newRoutine 코루틴 재개, 결과 출력
}

===========================

START
END
3

 

일반 루틴을 코루틴으로 변경해보았다. 새로운 키워드들이 많이 생겼고, 출력순서가 변경되었다. 주석만 정리하면 아래와 같다.

  1. main 코루틴 실행, START 출력
  2. newRoutine 코루틴 생성
  3. main 코루틴 중단 -> newRoutine 코루틴 실행
  4. newRoutine 코루틴 중단 -> main 코루틴 복귀
  5. main 코루틴 재개 -> END 출력
  6. main 코루틴 종료 -> newRoutine 코루틴 재개
  7. newRoutine 코루틴 재개 -> 결과 출력

 

코루틴의 가장 큰 특징은 중단과 재개이다. 일반 루틴은 한 번 시작되면 종료될 때까지 멈추지 않지만, 코루틴은 중단되기도 하고 재개되기도 한다.

 

가벼운 리소스

스레드는 프로세스 내의 실행 흐름을 의미하며, 스레드도 작업을 잠시 중단하고 재개할 수는 있다. 코루틴은 스레드보다 더 가벼운 실행단위이다. 그렇기에 context switching 과정에서 차이가 있다. 

 

 

스레드는 프로세스 내에서 독립된 스택영역을 갖는다. 그렇기에 실행이 변경되면 스택 영역을 교체하면 된다. 

스레드는 여러 개의 스레드를 통해 동시성(병렬성)을 확보한다. (병렬성 - 여러 작업이 동시에 실행)

 

출처 : https://wonyong-jang.github.io/kotlin/2021/10/28/Kotlin-coroutine-start.html

 

코루틴은 스레드보다도 더 가벼운 실행단위이다. 코루틴은 중단 후 재개 시 다른 스레드에 배치될 수도 있으며, 하나의 스레드에서 여러 코루틴을 번갈아가며 실행한다. 하나의 스레드 내에서도 동시성을 확보할 수 있다.

코루틴 전환 시 커널을 거치지 않기 때문에, 기존 스레드와 비교했을 때 context switching 비용은 훨씬 가볍다.

 

 

Java Virtual Thread와 차이점

필자는 스레드와 코루틴에 대해 비교하고 정리하면서 한 가지 의문이 더 들었다. 

'이거 완전 Virtual Thread 랑 비슷한 거 같은데?'
'그래서 어느 시점에 어떤 걸 적용해야 하는거야?'

 

참고로 Virtual Thread에 대한 개념이 생소하다면, 필자가 Virtual Thread에 대해서 학습하고 정리한 글도 확인해보면 좋겠다. 

 

차이점 1. 중단 주체

Virtual Thread의 중단 주체는 JVM 런타임이다.

Virtual Thread는 블로킹 상태에 진입할 경우, JVM 레벨에서 carrier thread에서 Virtual Thread를 unmount 시킨다. 이 부분은 코드에서 나타나지 않으며, 평범한 블로킹 코드여도 JVM 레벨의 스케쥴러가 알아서 처리한다.

 

Coroutine은 중단 주체가 컴파일러이다.

함수의 suspend 키워드를 붙이게 되면, 해당 루틴은 중단될 수도 있는 지점을 의미한다. Coroutine은 suspend 키워드가 붙은 함수를 컴파일러가 CPS(Continuation Passing Style) 변환한다.

 

CPS에 대해 간략히 설명하면, 중단 예상 지점인 suspend 함수를 상태 머신으로 변환한다. 상태 머신이 내부적으로 switch문으로 구현되고, 코루틴 상태를 저장하는 Continuation이라는 객체를 파라미터로 넘겨주는 방식이다. 중단 시 Continuation 객체에 코루틴의 현재 상태를 저장하고, 해당 객체를 통해 재개한다.

 

정리하자면, Virtual Thread는 JVM 스케쥴러가 알아서 중단 및 재개를 처리하지만, Coroutine은 개발자가 코드에 중단 지점을 명시해주어야 컴파일러가 CPS 스타일로 변환하여 중단 지점을 만든다. 

 

차이점 2. 블로킹 I/O vs 논블로킹 I/O

Virtual Thread는 블로킹 I/O에서 빛을 발한다. 블로킹 호출을 만나도 JVM 스케쥴러에서 Virtual Thread를 unmount 시켜주어 Carrier Thread가 블로킹되지 않고 다른 작업을 처리할 수 있다.

 

Coroutine은 논블로킹 I/O에서 빛을 발한다. 라이브러리 자체가 논블로킹을 지원해야 한다. 예로 R2DBC, WebClient와 같은 것들이 있다.

이런 논블로킹 API는 I/O가 끝나면 콜백을 불러주는 구조인데, 코루틴은 콜백 자리에 continuation 재개를 넣는다. 그렇기에 I/O를 기다리는 동안 스레드가 블로킹되지 않고 다른 작업을 처리할 수 있는 것이다.

 

그렇다면 Coroutine에서 블로킹 API를 쓸 수는 없는 것인가?

코루틴에 블로킹 API를 호출하게 되면 스레드가 블로킹되기에 Dispatcher를 통해 별도의 스레드 풀에 던지는 수밖에 없다. 이 때, 스레드 풀을 Virtual Thread로 설정하게 된다면, 블로킹 작업도 효율적으로 처리할 수 있을 것이다.