티스토리 뷰

Java 21에서는 Virtual Threads가 정식 기능으로 포함되었습니다. 이는 기존 Thread-per-request 모델의 구조적 한계를 완화하고, 대규모 동시 요청 처리 환경에서 서버 리소스를 보다 효율적으로 사용하는 것을 목표로 합니다.

 

이 글에서는 Java 공식 문서(JEP 444, Java SE Documentation)를 기준으로 Virtual Threads의 동작 원리와 성능 특성을 정리하고, 실무 환경에서 도입이 적합한 상황과 주의해야 할 한계를 명확히 설명합니다.

 


 

Virtual Threads란 무엇입니까

 

Java 공식 문서에 따르면, Virtual Thread는 경량 스레드(lightweight thread)로 JVM이 직접 관리하는 사용자 수준 스레드입니다.

기존 Java 애플리케이션에서 사용하는 Thread는 OS 스레드와 1:1로 매핑되는 Platform Thread였습니다.

 

이 구조에서는 다음과 같은 제약이 존재합니다.

  • 스레드 생성 비용이 큽니다.
  • 스레드 수 증가 시 메모리 사용량이 급격히 증가합니다.
  • I/O 대기 중인 스레드가 OS 리소스를 점유합니다.

Virtual Threads는 이러한 제약을 완화하기 위해 도입되었습니다. Java 21부터는 별도의 실험 옵션 없이 정식 API로 사용할 수 있습니다.

 


 

Platform Thread와 Virtual Thread의 차이

구분 Platform Thread Virtual Thread
관리 주체 운영체제 JVM
생성 비용 높습니다 매우 낮습니다
메모리 사용 큽니다 작습니다
동시 생성 가능 수 제한적입니다 매우 많습니다
I/O 대기 시 OS 스레드를 점유합니다 JVM이 언마운트합니다

 

Java 공식 문서에서 강조하는 핵심은 Virtual Thread가 대량의 동시 작업을 처리하기 위해 설계되었다는 점입니다.

 


 

Virtual Threads의 동작 원리

Carrier Thread 개념

Virtual Thread는 내부적으로 Carrier Thread라고 불리는 Platform Thread 위에서 실행됩니다.

JVM은 다수의 Virtual Thread를 소수의 Carrier Thread에 스케줄링합니다.

  • Virtual Thread 실행 시 Carrier Thread에 마운트됩니다.
  • Blocking I/O가 발생하면 Virtual Thread는 언마운트됩니다.
  • Carrier Thread는 즉시 다른 Virtual Thread를 실행합니다.

이 구조를 통해 I/O 대기 중인 작업이 OS 스레드를 점유하지 않습니다.

 

 

Blocking I/O 처리 방식의 변화

기존 Thread-per-request 모델에서는 다음과 같은 문제가 발생했습니다.

  • JDBC, HTTP 호출과 같은 Blocking I/O 발생
  • 스레드가 대기 상태로 고정됨
  • 동시 요청 증가 시 스레드 풀 고갈

 

Blocking I/O 동작은 JVM이 인식하는 범위에서만 Carrier Thread 언마운트가 발생합니다. 예를 들어 네이티브 메서드 호출 등 JVM이 모니터링할 수 없는 Blocking은 Carrier Thread를 점유할 수 있으며, 이 경우 Virtual Thread의 장점이 제한될 수 있습니다.

 

Virtual Threads 환경에서는 Blocking I/O 발생 시 해당 Virtual Thread가 Carrier Thread에서 분리되며, 다른 작업이 즉시 실행됩니다. 이 동작은 Java 표준 라이브러리의 Blocking I/O에 한해 JVM이 인식하여 처리합니다. 따라서 별도의 비동기 API로 코드를 재작성할 필요가 없습니다.

 

 


 

성능 관점에서의 의미

I/O-bound 워크로드

Java 공식 문서에서는 Virtual Threads의 주요 대상 워크로드로 I/O-bound 서버 애플리케이션을 명시합니다.

대표적인 예시는 다음과 같습니다.

  • Web API 서버
  • JDBC 기반 데이터베이스 접근
  • 외부 API 호출이 많은 서비스

이러한 환경에서는 다음과 같은 효과를 기대할 수 있습니다.

  • 동시 요청 처리 수 증가
  • 스레드 풀 튜닝 부담 감소
  • 메모리 사용량 감소

 

CPU-bound 워크로드

Virtual Threads는 CPU 성능을 직접적으로 향상시키지 않습니다. CPU-bound 작업에서는 다음과 같은 특성이 유지됩니다.

  • 실행 시간은 CPU 코어 수에 의해 제한됩니다.
  • Virtual Thread 수를 늘려도 처리량은 증가하지 않습니다.

따라서 계산 집약적 작업에서는 기존 Platform Thread와 성능 차이가 크지 않습니다.

 

 


 

Spring Boot 환경에서의 실무 적용

적용 배경

Spring MVC 기반 애플리케이션은 전통적으로 Thread-per-request 모델을 사용합니다. Virtual Threads의 가장 큰 장점은 기존 MVC 구조와 Blocking I/O 방식을 유지한 채 성능 개선이 가능하다는 점입니다.

 

 

Spring Boot 3.x 설정 예시

Spring Boot 공식 문서에 따르면 Virtual Thread는 다음과 같이 활성화할 수 있습니다.

@Bean
Executor taskExecutor() {
    return Executors.newVirtualThreadPerTaskExecutor();
}

Tomcat 또는 Servlet 컨테이너에서도 Virtual Thread Executor를 사용하도록 설정할 수 있습니다.

이 방식은 기존 동기 코드 구조를 변경하지 않습니다.

 

 


 

Virtual Threads 도입이 적합한 경우

적합한 경우

  • 동시 요청 수가 많은 서비스입니다.
  • JDBC, REST 호출 등 Blocking I/O 비중이 큽니다.
  • Thread Pool 튜닝이 반복적으로 필요합니다.
  • MVC 기반 구조를 유지해야 합니다.

부적합한 경우

  • CPU-bound 작업이 대부분입니다.
  • synchronized 블록이나 native call 사용이 많습니다.
  • ThreadLocal 의존도가 높은 코드입니다.

 


 

주의사항과 한계

Pinned Thread 문제

Java 공식 문서에 따르면 다음과 같은 경우 Virtual Thread가 Carrier Thread에 고정될 수 있습니다.

  • synchronized 블록 사용
  • native method 호출

이 경우 Virtual Thread의 장점이 감소합니다. 가능하면 java.util.concurrent 기반 동기화 도구 사용이 권장됩니다.

 


 

ThreadLocal 사용 시 고려사항

Virtual Thread는 생성 비용이 낮아 대량 생성이 일반적입니다. ThreadLocal에 큰 객체를 저장하면 메모리 사용량이 증가할 수 있으므로 주의해야 합니다.

 

 

Reactive Stack과의 관계

Virtual Threads는 Reactive 모델을 대체하지 않습니다. Java 공식 문서에서도 두 접근 방식은 서로 다른 문제를 해결한다고 명시합니다.

  • Reactive: 비동기 스트림 기반 설계입니다.
  • Virtual Thread: 동기 코드 유지와 확장성 개선에 초점을 둡니다.

 


 

기존 Thread Pool 기반 서버와 Virtual Thread 기반 서버의 성능 비교

기존 Thread Pool 기반 서버의 특성

전통적인 Java 서버 애플리케이션은 Thread Pool 기반 구조를 사용합니다. 대표적으로 Spring MVC + Tomcat 환경에서는 요청마다 Platform Thread 하나를 할당하는 Thread-per-request 모델이 사용됩니다.

 

이 구조의 특징은 다음과 같습니다. 

  • 요청 수에 비례하여 스레드가 필요합니다.
  • 스레드 수는 미리 정의된 풀 크기에 의해 제한됩니다.
  • Blocking I/O가 발생하면 해당 스레드는 대기 상태로 고정됩니다.

MySQL, PostgreSQL과 같은 RDBMS 접근이나 외부 HTTP 호출이 많은 서비스에서는 다음과 같은 병목이 발생합니다. 

  • 동시 요청 증가 시 스레드 풀이 빠르게 소진됩니다.
  • 대기 중인 스레드가 OS 리소스를 점유합니다.
  • 스레드 풀 크기 튜닝이 필수적입니다.

Java 공식 문서에서도 Platform Thread는 생성 비용과 메모리 비용이 크기 때문에 대량 동시성 처리에 한계가 있다고 명시합니다.

 

 

Virtual Thread 기반 서버의 특성

Virtual Thread 기반 서버에서는 요청 단위로 Virtual Thread를 생성합니다. 이 스레드는 JVM이 관리하며, 소수의 Carrier Thread 위에서 실행됩니다. 핵심적인 차이점은 다음과 같습니다. 

  • Virtual Thread는 생성 비용이 매우 낮습니다.
  • Blocking I/O 발생 시 Carrier Thread에서 언마운트됩니다.
  • OS 스레드를 점유하지 않습니다.

Java 공식 문서(JEP 444)에 따르면, Virtual Thread는 I/O 대기 시간이 긴 서버 애플리케이션을 위해 설계되었습니다.

 

성능 비교 관점 정리

비교 항목 Thread Pool 기반 Virtual Thread 기반
동시 요청 처리 수 스레드 풀 크기에 의존 매우 큼
Blocking I/O 영향 스레드 점유 스레드 언마운트
메모리 사용량 높음 낮음
튜닝 난이도 높음 낮음
코드 변경 필요성 없음 거의 없음

중요한 점은 Virtual Thread가 CPU 성능을 향상시키는 것이 아니라, 동시성 처리 효율을 개선한다는 점입니다.

CPU-bound 작업에서는 두 모델 간 성능 차이가 크지 않습니다.

 

언제 성능 차이가 명확해지는가

다음과 같은 조건에서 성능 차이가 명확해집니다.

  • 요청당 JDBC 호출이 포함된 경우
  • 외부 API 호출이 빈번한 경우
  • 트래픽 변동 폭이 큰 경우
  • Peak 트래픽에서 Thread Pool 포화가 발생하는 경우

이러한 환경에서는 Virtual Thread 기반 서버가 더 많은 요청을 안정적으로 처리할 수 있습니다.

 

 


 

Virtual Thread + JDBC 기반 실무 아키텍처 예시

전통적인 JDBC 아키텍처의 한계

JDBC는 기본적으로 Blocking I/O 방식입니다. 쿼리 실행 중 스레드는 데이터베이스 응답을 기다리며 대기 상태가 됩니다. Thread Pool 기반 환경에서는 다음 문제가 발생합니다.

  • JDBC 대기 시간이 길어질수록 스레드 점유 시간이 증가합니다.
  • DB 커넥션 풀과 스레드 풀이 동시에 병목이 됩니다.
  • 트래픽 증가 시 요청 대기 시간이 급격히 증가합니다.

 

Virtual Thread 환경에서의 JDBC 처리 방식

Virtual Thread 환경에서도 JDBC는 여전히 Blocking I/O입니다. 그러나 동작 결과는 다릅니다.

  • JDBC 호출 시 Virtual Thread가 Carrier Thread에서 언마운트됩니다.
  • Carrier Thread는 즉시 다른 Virtual Thread를 실행합니다.
  • 대기 중인 JDBC 작업은 OS 스레드를 점유하지 않습니다. 

Java 공식 문서에서는 이를 Blocking-friendly concurrency model로 설명합니다.

 

 

실무 아키텍처 구성 예시

[Client]
   ↓
[Spring MVC Controller]
   ↓ (Virtual Thread)
[Service Layer]
   ↓ (JDBC Blocking Call)
[RDBMS]

 

이 구조의 특징은 다음과 같습니다.

  • 기존 MVC 구조를 그대로 유지합니다.
  • 비동기 API나 Reactive Stack으로 전환할 필요가 없습니다.
  • Virtual Thread가 JDBC 대기 시간을 흡수합니다.

 

Spring Boot + Virtual Thread + JDBC 설정 예시

@Configuration
public class VirtualThreadConfig {

    @Bean
    Executor taskExecutor() {
        return Executors.newVirtualThreadPerTaskExecutor();
    }
}

 

이 설정만으로도 Controller, Service, Repository 계층의 동기 코드는 그대로 유지됩니다.

Spring 공식 문서에서도 Virtual Thread는 기존 동기 프로그래밍 모델과의 호환성을 중요한 장점으로 설명합니다.

 

 

실무 적용 시 주의사항

다음 사항은 반드시 고려해야 합니다.

  • DB 커넥션 풀 크기는 여전히 중요합니다.
  • Virtual Thread가 무한정 DB 커넥션을 생성하지는 않습니다.
  • 커넥션 풀 크기는 DB 처리량 기준으로 설정해야 합니다.
  • JDBC 호출이 너무 길어지는 경우 쿼리 최적화가 우선입니다.

Virtual Thread는 DB 성능 문제를 해결하지 않습니다. DB 병목은 인덱스, 쿼리 튜닝, 스키마 설계로 해결해야 합니다.

 


 

 

Virtual Thread 도입 전/후 아키텍처 체크리스트

Virtual Thread는 기존 서버 아키텍처를 크게 변경하지 않고도 동시성 처리 효율을 개선할 수 있습니다. 그러나 무조건적인 도입은 기대한 효과를 얻지 못할 수 있으므로, 도입 전·후로 명확한 체크 포인트를 점검해야 합니다.

 

Virtual Thread 도입 전 체크리스트

1) 애플리케이션의 병목 유형 확인

Virtual Thread는 I/O 대기 시간이 긴 애플리케이션에 적합합니다. 다음 조건을 만족하는지 먼저 확인해야 합니다.

  • JDBC 호출이 요청 처리 경로에 포함되어 있는가
  • 외부 API 호출이 빈번한가
  • 트래픽 증가 시 Thread Pool 포화가 발생하는가

CPU-bound 연산이 대부분인 서비스라면 Virtual Thread 도입 효과는 제한적입니다.

 

2) 현재 Thread Pool 사용 현황 점검

다음 항목을 점검합니다.

  • Web Server(Thread Pool) 최대 스레드 수
  • Async Executor 사용 여부
  • 비동기 작업과 동기 작업의 혼합 여부

Thread Pool 튜닝만으로도 문제가 해결될 수 있다면, Virtual Thread 도입은 후순위가 됩니다.

 

3) Blocking I/O 사용 여부 확인

Virtual Thread는 Blocking I/O와 함께 사용할 때 효과가 큽니다. 다음 라이브러리 사용 여부를 점검합니다.

  • JDBC (MySQL, PostgreSQL 등)
  • RestTemplate, WebClient(blocking mode)
  • 외부 SDK 기반 HTTP 호출 

Java 공식 문서에서는 Virtual Thread를 Blocking I/O 친화적 동시성 모델로 설명합니다.

 

 

4) DB 및 외부 시스템 처리량 확인

Virtual Thread는 요청 수를 늘릴 수는 있지만, 하위 시스템의 처리량을 증가시키지는 않습니다. 

  • DB 최대 커넥션 수
  • DB 평균 쿼리 응답 시간
  • 외부 API Rate Limit

이 부분이 병목이라면 Virtual Thread 도입만으로는 효과가 없습니다.

 

 

 

Virtual Thread 도입 시 체크리스트

1) Executor 설정 방식 확인

Virtual Thread는 명시적으로 Executor를 설정해야 합니다.

Executors.newVirtualThreadPerTaskExecutor();

Spring 환경에서는 Web 요청 처리와 내부 비동기 작업이 동일한 Executor를 사용하는지 확인해야 합니다.

 

2) ThreadLocal 사용 여부 점검

Virtual Thread는 ThreadLocal을 지원하지만, 다음 사항을 주의해야 합니다.

  • 요청 스코프 데이터 관리 방식
  • MDC(Log Context) 사용 여부

Spring 및 Java 공식 문서에서는 ThreadLocal 과도한 사용을 지양할 것을 권장합니다.

 

3) 동기 코드 유지 여부 확인

Virtual Thread 도입 시 다음을 확인합니다.

  • 기존 동기 코드 유지 가능 여부
  • CompletableFuture, Reactive 코드 혼용 여부

Virtual Thread는 기존 동기 코드를 유지하면서 동시성을 확장하는 것이 목적입니다.

 

Virtual Thread 도입 후 체크리스트

1) DB 커넥션 풀 재설계 여부

Virtual Thread는 DB 커넥션 풀을 무제한으로 확장하지 않습니다. 도입 후 반드시 확인해야 합니다.

  • HikariCP maxPoolSize
  • 평균 커넥션 대기 시간
  • 커넥션 풀 포화 여부

DB 커넥션 풀은 DB 처리량 기준으로 유지해야 합니다.

 

시스템 리소스 사용량 모니터링

다음 지표를 비교합니다.

  • CPU 사용률
  • Heap 사용량
  • OS Thread 수

Java 공식 문서에 따르면, Virtual Thread는 Platform Thread 대비 메모리 사용량이 훨씬 낮습니다.

 

Latency 분포 변화 확인

평균 응답 시간뿐 아니라 다음을 확인합니다.

  • P95, P99 Latency
  • Peak 트래픽 시 응답 시간 안정성

Virtual Thread의 장점은 최악 응답 시간의 안정화에 있습니다.

 

장애 시 영향 범위 점검

동시 요청 수가 증가하면 장애 발생 시 영향 범위도 커질 수 있습니다. 

  • DB 장애 시 요청 폭증 여부
  • 외부 API 장애 시 요청 적체 여부

Circuit Breaker, Timeout 설정은 필수적으로 병행해야 합니다.

 

 

도입 여부 판단을 위한 정리 기준

다음 질문에 “예”가 많을수록 Virtual Thread 도입 효과가 큽니다.

  • JDBC 및 외부 API 호출 비중이 높은가
  • Thread Pool 포화가 자주 발생하는가
  • Reactive 전환이 부담스러운가
  • 기존 MVC 구조를 유지하고 싶은가

반대로 CPU 연산 위주의 서비스라면, Virtual Thread는 큰 효과를 기대하기 어렵습니다.

 

 

체크리스트 요약

  • Virtual Thread는 동시성 확장 도구입니다.
  • I/O 병목을 흡수하지만, 하위 시스템 병목은 해결하지 않습니다.
  • DB 커넥션 풀과 쿼리 성능은 별도로 관리해야 합니다.
  • 도입 전·후 모니터링과 장애 대응 전략이 필수입니다.

Virtual Thread는 무조건적인 선택지가 아니라,

Thread Pool 기반 서버의 구조적 한계를 보완하는 현실적인 대안으로 평가해야 합니다.

 

 


 

정리

Virtual Thread는 Thread Pool 기반 서버의 구조적 한계를 완화합니다. 특히 JDBC와 같은 Blocking I/O 중심의 서버 애플리케이션에서 효과가 명확합니다. 핵심 요약은 다음과 같습니다.

  • Thread Pool 기반 모델은 동시성 확장에 한계가 있습니다.
  • 모든 상황에서 성능을 향상시키는 만능 해법은 아닙니다.
  • 도입 전 워크로드 특성 분석이 필수입니다.
  • Virtual Thread는 I/O 대기 비용을 JVM 레벨에서 흡수합니다.
  • 기존 MVC + JDBC 구조를 유지할 수 있습니다.
  • DB 성능 문제 자체를 해결하는 도구는 아닙니다.

Virtual Threads는 새로운 패러다임이라기보다 기존 Java 서버 모델을 확장하는 현실적인 선택지로 이해하는 것이 적절합니다.

Virtual Thread는 Reactive 전환이 부담스러운 환경에서 현실적인 성능 개선 선택지로 평가할 수 있습니다.