사내에서 프로젝트를 인계 받고 성능 개선을 하던 중, 병렬 처리를 위한 방법으로 Blocking Api 호출을 이전 서버들과 동일하게 Coroutine으로 처리하려고 했습니다.
하지만, 해당 서버는 Java 21을 사용하고 있었고, virtual Thread를 통해 처리하면 되지 않을까 생각했습니다.
Virtual Thread를 사용하기 위한 Executor를 생성하고, 이를 통해 처리했는데요.
기존과 다르게 coroutine을 사용하지 않고, Thread를 받아서 처리하였더니 어떤 차이점이 있고, 어느 상황에서는 어떤 기술을 사용하는게 좋은지 궁금하기 시작했습니다.
이 기회에 virtual Thread에 대한 내용을 찾아보고 정리하며, 어떤 장단점이 있는지 자세히 살펴보려고 합니다.
Virtual Threads는 기존의 플랫폼 스레드와 비교했을 때 더욱 가벼운 스레드입니다. 플랫폼 스레드는 운영 체제에 의해 관리되지만, Virtual Threads는 JVM이 자체적으로 관리합니다. 이로 인해 많은 수의 스레드를 생성하고 관리할 때 성능과 자원 사용 면에서 큰 장점을 제공합니다.
Java 21에서는 Thread.ofVirtual() 메서드를 사용하여 Virtual Threads를 쉽게 생성할 수 있습니다. 다음은 기본적인 예제입니다:
Runnable task = () -> {
System.out.println("Hello from virtual thread!");
};
Thread virtualThread = Thread.ofVirtual().start(task);
virtualThread.join(); // 메인 스레드가 virtualThread의 종료를 기다림
다만, 실제로 이렇게 사용하지는 않고 Configuration에 Thread Pool을 선언하여, 필요한 Task는 일급 함수를 선언하듯이 사용하는 방법을 보통 사용할 것 같습니다.
@Configuration
class VirtualThreadConfig {
fun virtualThreadExecutor(): ExecutorService {
// Virtual Thread 기반 ExecutorService 생성
return Executors.newVirtualThreadPerTaskExecutor()
}
}
@Service
class XXXService(
private val executor: ExecutorService
) {
fun xxxFunction() {
val futures = (0 until 5).map { i ->
CompletableFuture.supplyAsync({
// Blocking Logic
}, executor)
}
CompletableFuture.allOf(*futures.toTypedArray()).join()
// Bussiness Logic
val results = futures.map { it.join() }
...
}
}
일반 Thread

virtual Thread

출처 : https://findstar.pe.kr/2023/04/17/java-virtual-threads-1/
관련 글을 여러 개 찾아보던 중 동작을 가장 잘 보여주는 그림이 있어서 참고했습니다.
즉, 기존에는 Thread Pool을 생성하여 N개의 쓰레드를 만들어놓고, 필요에 따라 Platform Thread를 가져가서 실행하는 방식이었습니다. 이 때, N개 이상의 요청이 들어오는 경우 Queue에서 대기하고, Pool에서 할당받기 전까지 무제한으로 대기하는 상황이었습니다.
Virtual Thread에서는 이를 해결하기 위해, Scheduling을 하고 있습니다.
Coroutine의 내부 동작과 비슷한 방식으로 Blocking한 호출 (ex, I/O 요청)을 하면 Scheduling을 해서 Carrier Thread가 그 다음 작업을 처리합니다.
이로 인해, Blocking 호출이 많은 서버에서는 Reactive와 동일하게 처리하는 것처럼 좋은 성능을 발휘할 수 있습니다.
Java virtual Thread는 경량 쓰레드를 활용한다는 점에서 Coroutine과 비슷해 보였습니다.
Java 21 에서 기본적으로 Virtual Thread를 지원하고 있기 때문에, 내가 원하는 Blocking 작업을 알아서 Scheduling 한다는 관점에서 굳이 Coroutine을 도입할 필요가 없다고 생각했습니다.
그럼 Coroutine과 큰 차이점은 무엇이 있을지 궁금하여 찾아보게 되었습니다.
Coroutine은 suspend 함수를 통해 중단 지점을 설정할 수 있음. 즉, 한번에 실행 되면 좋은 최적화 지점에서는 명시적으로 suspend 함수를 사용하지 않고 일반적으로 처리할 수 있음.
Java virtual Thread는 기존 코드를 그대로 사용. 단, 내부적으로 I/O 같은 Blocking 작업이 발생하면 자동적으로 Scheduling 되기 때문에, 어디서 Thread가 변경될 지 추측할 수 없음
Coroutine은 공식 문서를 보면 "구조적 동시성 - Structured Concurrency" 를 강조하고 있음. 즉, 여러개의 병렬 처리가 하나로 처리되는 것을 원할 경우 쉽게 구현할 수 있음. (자식의 코루틴 생명 주기 관리 / supervisor Scope를 통해 예외적으로 전파받지 않을 수 있음)
Java Virtual Thread는 경량 쓰레드에 집중. 개발자는 동일한 코드를 작성하지만 내부적으로 알아서 Blocking 되는 작업은 스케쥴링 되면서 빠르게 작업될 수 있도록 함.
Coroutine은 전통적인 방식인 Thread Pool에서 관리되는 Platform Thread를 사용해서 처리함. 즉, 내부 동작을 잘 모르는 상태에서 코드를 작성하여 Thread.sleep() 같은 Thread를 직접 Blocking 해버리면, Coroutine은 내부적으로 Continuation하게 동작하지 않고, 성능이 나오지 않음.
즉, 코루틴이 지원하는 delay()나 java reactive 프로그램을 지원하는 awaitSingle()을 사용하지 않는다면, 원하는 성능이 나오지 않을 수 있음.
Java Virtual Thread는 JVM 내에서 자체적으로 제공하고 있기 때문에, Thread가 Block 되는 거의 모든 작업이 스케쥴링 되어 최적화가 가능함.
Coroutine의 경우 일반 Scope에서는 예외가 전파되어 다른 scope에도 영향을 미침. 또한, CoroutineExceptionHandler을 등록하여 따로 처리할 수 있게 만들어야 함. 또한, try catch를 사용해도 launch 같은 특수한 함수는 에러가 잡히지 않을 수 있습니다
참고문서 : https://kotlinlang.org/docs/exception-handling.html#exception-propagation
코루틴의 철학에 맞춰 자식 scope에서 발생한 에러는 부모로 던지게 되어 있습니다.
java virtual Thread는 전통적으로 처리하는 try catch 등의 에러 처리로도 가능합니다.
여기까지 정리하면 굳이 어려운 Coroutine 대신 Virtual Thread를 사용하면 만사 해결이 아닌가 생각이 듭니다.
하지만, 무조건 정답은 없으니 조심해야 하는 경우가 어떤 것이 있을지 확인해보았습니다.
위의 내용들을 보면 대체로 우리가 제어 가능한 부분이지만, Lock을 획득하는 과정에서 synchronized가 있는 내부 라이브러리에서 Pinning 문제가 발생할 수 있다고 합니다.
java 21 버전의 virtual thread를 지원하기 위해 synchronized 대신 ReentrantLock으로 처리를 변경해야 한다고 하지만, 이는 java 24에서 synchronized의 JVM 동작을 변경하여 해결하였다고 합니다.
이제는 native 메서드를 호출하여 Thread가 blocking되는 상황을 제외하고 범용적으로 사용하기에 부담이 없을 것으로 확인됩니다.
서버의 병렬 처리를 하기 위해 이전 사용하던 방식인 Coroutine을 바로 도입하기 보다는, java에서 새로 제공하고 있는 java virtual Thread를 사용하면 어떨까 하다가 찾아보게 되었습니다.
내부적인 동작과 Coroutine의 철학 및 virtual Thread의 장단점 및 주의 사항을 좀 더 알아볼 수 있는 좋은 시간이었습니다.
이상으로 긴 글 읽어주셔서 감사합니다. 궁금한 부분이 있다면 질문 남겨주시면 감사하겠습니다.