Java 21 가상 스레드(Virtual Threads) 도입과 성능 최적화 가이드
최근 백엔드 개발 환경에서 동시성 처리는 시스템의 전체 성능과 직결되는 매우 중요한 요소입니다. 기존의 Java는 운영체제(OS)의 스레드와 1:1로 매핑되는 플랫폼 스레드 모델을 사용해 왔으나, 이는 메모리 점유율과 컨텍스트 스위칭 비용 측면에서 한계가 있었습니다. 특히 수만 개의 동시 연결을 처리해야 하는 현대적인 웹 애플리케이션에서는 이러한 한계가 병목 현상으로 작용하곤 합니다.
이번 포스팅에서는 Java 21에서 정식 도입된 가상 스레드(Virtual Threads)가 무엇인지, 그리고 이를 통해 어떻게 애플리케이션의 처리량을 획기적으로 개선할 수 있는지 상세히 살펴보겠습니다.
1. 가상 스레드(Virtual Threads)란 무엇인가?
가상 스레드는 JDK 21(Project Loom)에서 도입된 경량 스레드 모델입니다. 기존 플랫폼 스레드와 달리 가상 스레드는 OS 스레드와 직접 매핑되지 않고, JVM 내부의 스케줄러를 통해 소수의 플랫폼 스레드 위에서 수백만 개의 가상 스레드가 동작할 수 있도록 설계되었습니다.
가상 스레드의 가장 큰 장점은 ‘Thread-per-Request’ 모델을 유지하면서도 리소스를 매우 적게 사용한다는 점입니다. 이는 기존의 비동기 리액티브 프로그래밍(Reactive Programming)이 주는 복잡성을 피하면서도 그에 상응하는 높은 처리량을 제공합니다.
플랫폼 스레드와 가상 스레드 비교
| 구분 | 플랫폼 스레드 (Platform Thread) | 가상 스레드 (Virtual Thread) |
|---|---|---|
| 메모리 점유 | 약 1MB (Stack 영역) | 수 KB 내외 |
| 생성 비용 | 비쌈 (OS 시스템 콜 필요) | 매우 저렴 (객체 생성 수준) |
| 컨텍스트 스위칭 | OS 커널 개입 (무거움) | JVM 내부 스케줄링 (가벼움) |
| 동시성 한계 | 수천 개 수준 | 수백만 개 가능 |
2. 가상 스레드 생성 및 사용 방법
Java 21부터는 매우 간단한 API를 통해 가상 스레드를 생성할 수 있습니다. 기존의 Thread 클래스에 가상 스레드 생성을 위한 정적 메서드가 추가되었습니다.
기본 생성 예제
// 단일 가상 스레드 생성 및 시작
Thread.ofVirtual().start(() -> {
System.out.println("가상 스레드에서 동작 중: " + Thread.currentThread());
});
// ExecutorService를 이용한 관리
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 비즈니스 로직 수행
return "Success";
});
}
위의 newVirtualThreadPerTaskExecutor()는 각 작업마다 새로운 가상 스레드를 할당합니다. 가상 스레드는 생성 비용이 매우 낮기 때문에 기존처럼 스레드 풀(Thread Pool)을 만들어 관리할 필요가 없다는 것이 핵심입니다.
3. Spring Boot에서 가상 스레드 활성화하기
Spring Boot 3.2 버전부터는 설정 한 줄로 내장 톰캣(Tomcat)과 작업 스케줄러에서 가상 스레드를 사용하도록 지정할 수 있습니다.
application.yml 설정
spring:
threads:
virtual:
enabled: true
이 설정을 활성화하면 Spring MVC의 요청 처리 스레드 모델이 가상 스레드로 전환됩니다. 블로킹 I/O(예: DB 조회, 외부 API 호출)가 발생하는 구간에서 가상 스레드가 자동으로 양보(yield)되어 다른 작업을 처리할 수 있게 하므로, 전체적인 시스템 처리량이 대폭 향상됩니다.
4. 가상 스레드 도입 시 주의사항 (Pinning 현상)
가상 스레드가 만능은 아닙니다. 가장 주의해야 할 점은 ‘Pinning’ 현상입니다. 가상 스레드 내에서 synchronized 블록을 사용하거나 네이티브 메서드를 호출할 경우, 해당 가상 스레드가 실행 중인 플랫폼 스레드(Carrier Thread)에 고정되어 다른 가상 스레드로 전환되지 못하는 문제가 발생할 수 있습니다.
이를 방지하기 위해서는 synchronized 대신 java.util.concurrent.locks.ReentrantLock을 사용하는 것이 권장됩니다.
// 개선 전: Pinning 발생 가능
public synchronized void updateData() {
// I/O 작업 포함 시 위험
}
// 개선 후: 가상 스레드 친화적 설계
private final ReentrantLock lock = new ReentrantLock();
public void updateData() {
lock.lock();
try {
// 비즈니스 로직
} finally {
lock.unlock();
}
}
결론
Java 21의 가상 스레드는 높은 동시성이 요구되는 서버 환경에서 게임 체인저가 될 수 있는 기술입니다. 코드의 복잡성을 낮추면서도 리소스를 효율적으로 사용할 수 있게 해주어, 인프라 비용 절감과 성능 향상을 동시에 꾀할 수 있습니다.
가상 스레드를 도입할 때는 무조건적인 적용보다는 synchronized 사용 여부를 점검하고, 실제 I/O 집약적인 작업에서 성능 테스트를 거친 후 점진적으로 전환하는 것을 추천합니다. 차세대 Java 생태계의 중심이 될 가상 스레드를 지금 바로 프로젝트에 적용해 보시기 바랍니다.