Contents
see ListVirtual Threads란 무엇인가?
Java 21에서 정식 출시된 Virtual Threads(가상 스레드)는 Project Loom의 핵심 결과물이다. 기존 플랫폼 스레드(Platform Thread)와 달리 JVM이 직접 관리하는 경량 스레드로, 수백만 개를 동시에 생성해도 메모리와 CPU 자원을 과도하게 소모하지 않는다. 20년 이상의 개발 역사를 가진 프로젝트의 결실로, Java 동시성 프로그래밍의 패러다임을 근본적으로 바꾸고 있다.
플랫폼 스레드 vs 가상 스레드
플랫폼 스레드(OS 스레드)는 생성 시 약 1MB의 스택 메모리를 예약하고, OS 컨텍스트 스위칭 비용이 크다. 일반적으로 JVM 프로세스 하나에서 수천 개가 한계다. 반면 가상 스레드는 JVM 힙에서 관리되며, 필요할 때만 메모리를 확장하고 GC로 정리된다. 수백만 개 생성이 가능하다.
실제 사례: Netflix의 가상 스레드 도입으로 동시 요청 50,000+건 처리 시 응답 시간이 8.2초에서 180ms로 단축되었다는 보고가 있다.
Virtual Thread 생성 방법
// 방법 1: Thread.ofVirtual() 사용
Thread virtualThread = Thread.ofVirtual()
.name("my-virtual-thread")
.start(() -> {
System.out.println("가상 스레드 실행 중: " + Thread.currentThread());
});
// 방법 2: Thread.startVirtualThread() 간편 메서드
Thread.startVirtualThread(() -> {
// 작업 수행
processRequest();
});
// 방법 3: ExecutorService 사용 (권장)
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 100_000; i++) {
int taskId = i;
executor.submit(() -> {
// I/O 바운드 작업
String result = fetchDataFromAPI(taskId);
processResult(result);
});
}
} // try-with-resources로 자동 shutdownI/O 바운드 작업에서의 동작 원리
가상 스레드의 핵심 메커니즘은 블로킹 I/O 처리 방식이다. 가상 스레드가 블로킹 작업(파일 읽기, 네트워크 요청, DB 쿼리 등)을 만나면:
- JVM이 현재 실행 상태(continuation)를 캡처하여 힙에 저장
- 가상 스레드를 캐리어 스레드(carrier thread)에서 언마운트
- 캐리어 스레드는 다른 가상 스레드 실행
- I/O 완료 시 가상 스레드를 다시 마운트하여 재개
// 기존 비동기 코드 - 복잡한 CompletableFuture 체이닝
public CompletableFuture processOrderAsync(Order order) {
return validateOrderAsync(order)
.thenCompose(validated -> fetchInventoryAsync(validated))
.thenCompose(inventoried -> chargePaymentAsync(inventoried))
.thenCompose(charged -> sendConfirmationAsync(charged))
.exceptionally(ex -> handleError(ex));
}
// 가상 스레드로 동기 스타일 코드 - 훨씬 단순!
public OrderResult processOrder(Order order) {
try {
var validated = validateOrder(order); // 블로킹 OK
var inventoried = fetchInventory(validated); // 블로킹 OK
var charged = chargePayment(inventoried); // 블로킹 OK
return sendConfirmation(charged); // 블로킹 OK
} catch (Exception ex) {
return handleError(ex);
}
} Spring Boot + Virtual Threads 통합
Spring Boot 3.2부터 가상 스레드를 공식 지원한다. application.properties 한 줄로 활성화할 수 있다.
# application.properties
spring.threads.virtual.enabled=true// Spring MVC 컨트롤러 - 가상 스레드에서 자동 실행됨
@RestController
@RequestMapping("/api")
public class UserController {
@Autowired
private UserRepository userRepository; // JPA/JDBC 블로킹 OK
@GetMapping("/users/{id}")
public User getUser(@PathVariable Long id) {
// 이 메서드는 가상 스레드에서 실행됨
// 블로킹 DB 쿼리가 스레드를 낭비하지 않음
return userRepository.findById(id).orElseThrow();
}
@GetMapping("/users")
public List getUsers() {
// 수천 개 동시 요청도 처리 가능
return userRepository.findAll();
}
} Structured Concurrency (구조적 동시성)
Java 21에서 Preview로 포함된 Structured Concurrency는 여러 가상 스레드를 구조적으로 관리하는 API다. 부모 작업과 자식 작업의 생명주기를 명확하게 연결한다.
import java.util.concurrent.StructuredTaskScope;
public UserProfile getUserProfile(Long userId) throws Exception {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 여러 작업을 병렬 실행
var userFuture = scope.fork(() -> userService.findById(userId));
var ordersFuture = scope.fork(() -> orderService.findByUserId(userId));
var pointsFuture = scope.fork(() -> pointService.findByUserId(userId));
scope.join(); // 모든 작업 완료 대기
scope.throwIfFailed(); // 하나라도 실패하면 예외
// 결과 수집
return new UserProfile(
userFuture.get(),
ordersFuture.get(),
pointsFuture.get()
);
}
// scope 종료 시 모든 자식 스레드 자동 취소
}주의사항: 가상 스레드 핀닝 문제
가상 스레드가 캐리어 스레드에 고정(pinning)되어 블로킹이 발생하는 경우가 있다. 주요 원인은 synchronized 블록/메서드 사용과 native method 호출이다.
// 문제: synchronized 사용 시 핀닝 발생
public synchronized void synchronizedMethod() {
// 이 안에서 블로킹 I/O 발생 시 캐리어 스레드도 블로킹됨!
Thread.sleep(Duration.ofSeconds(1));
}
// 해결: ReentrantLock 사용
private final ReentrantLock lock = new ReentrantLock();
public void betterMethod() {
lock.lock();
try {
// 가상 스레드가 올바르게 언마운트됨
Thread.sleep(Duration.ofSeconds(1));
} finally {
lock.unlock();
}
}
// JVM 플래그로 핀닝 감지 (-Djdk.tracePinnedThreads=full)
// Java Flight Recorder(JFR)로 모니터링 권장성능 모니터링
// JFR을 활용한 가상 스레드 모니터링
// jcmd JFR.start filename=recording.jfr
// jcmd JFR.stop
// Micrometer를 통한 메트릭 수집
@Configuration
public class ThreadMonitorConfig {
@Bean
public MeterBinder virtualThreadMetrics() {
return registry -> {
// 활성 가상 스레드 수 측정
ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
Gauge.builder("jvm.virtual.threads.active", threadMXBean,
bean -> countVirtualThreads(bean))
.description("활성 가상 스레드 수")
.register(registry);
};
}
} 언제 가상 스레드를 사용해야 하는가?
가상 스레드는 I/O 바운드 작업(HTTP 요청, DB 쿼리, 파일 I/O)에서 극적인 효과를 발휘한다. CPU 바운드 작업(수학 연산, 이미지 처리)에서는 효과가 제한적이며, 기존 ForkJoinPool이나 고정 크기 스레드 풀이 더 적합하다. 마이그레이션 전략으로는 I/O 바운드 코드에서 먼저 도입하고, synchronized를 ReentrantLock으로 점진적으로 교체하는 접근이 권장된다.