회사에서 JDK 17 버전에서 21로 변경하며 Virtual Thread를 도입하기 위해 학습한 내용을 기록하였습니다.
Virtual Thread 개념 정리
Java 19에서 프리뷰로 도입되고 Java 21에서 정식 출시된 Virtual Thread(가상 스레드)에 대해 학습하였습니다. 기존 Platform Thread와 비교하면 상당한 차이가 있었습니다.
기존 Platform Thread의 한계점
- 스레드당 약 2MB의 메모리 사용
- 생성 및 삭제 비용이 높음
- 일반적으로 수십~수백 개 정도만 생성 가능
Virtual Thread의 개선점
- 스레드당 약 10KB로 매우 가벼움
- 수십만~수백만 개까지 생성 가능
- I/O 대기 시간을 효율적으로 활용
동작 원리 학습
Virtual Thread는 Platform Thread 위에서 동작하며, 핵심은 Mount/Unmount 메커니즘입니다.
동작 흐름:
1. Virtual Thread A가 Platform Thread 1에서 실행 (Mount 상태)
2. DB 조회 등 I/O 작업 시작
3. Virtual Thread A가 Platform Thread에서 분리 (Unmount)
4. Platform Thread 1이 다른 Virtual Thread B를 실행
5. I/O 완료 시 Virtual Thread A가 다시 Platform Thread에 연결되어 실행 재개
핵심 용어
- 마운트(Mount): Virtual Thread가 Platform Thread에서 실행되는 상태
- 언마운트(Unmount): I/O 블로킹 시 Platform Thread에서 분리되어 대기하는 상태
- Carrier Thread: Virtual Thread를 실제로 실행하는 Platform Thread
I/O 바운드 작업에서의 효과
Virtual Thread는 I/O 집약적인 작업에서 뛰어난 성능을 보입니다.
public void processUserData() {
// 1. DB 사용자 조회 - I/O 대기 시 언마운트 발생
User user = userRepository.findById(userId);
// 2. 외부 API 호출 - I/O 대기 시 언마운트 발생
ApiResponse response = externalApiClient.call(user.getId());
// 3. 파일 저장 - I/O 대기 시 언마운트 발생
fileService.saveUserData(user, response);
}
위와 같은 코드에서는 각 I/O 작업마다 Virtual Thread가 언마운트되어 Platform Thread가 다른 Virtual Thread를 실행할 수 있게 됩니다.
참고로 Thread.sleep()도 Virtual Thread에서는 언마운트됩니다.
주의해야 할 제약사항
synchronized 블록 사용 시 문제점
학습 중 가장 중요하다고 판단한 부분입니다.
// Virtual Thread 효과가 제한되는 경우
public void inefficientMethod() {
synchronized (lock) {
// Virtual Thread가 Platform Thread를 계속 점유
// 언마운트되지 않아 비효율적
expensiveOperation();
}
}
// Virtual Thread에 적합한 방식
private final ReentrantLock lock = new ReentrantLock();
public void efficientMethod() {
lock.lock();
try {
// Virtual Thread 언마운트 가능
expensiveOperation();
} finally {
lock.unlock();
}
}
synchronized는 JVM 모니터 락으로 OS 레벨에서 스레드를 블로킹하기 때문에 언마운트가 불가능합니다.
CPU 바운드 작업의 한계
public void cpuIntensiveTask() {
for (int i = 0; i < 1000000; i++) {
// CPU 연산 중심 작업에서는 언마운트 포인트가 없음
Math.sin(i) * Math.cos(i);
}
}
CPU 집약적인 작업은 Virtual Thread를 많이 생성해도 CPU 코어 수의 제약을 받습니다.
Spring Boot 환경에서의 적용
설정은 매우 간단합니다.
# application.yml
spring:
threads:
virtual:
enabled: true
이 설정만으로 Spring의 요청 처리 스레드가 Virtual Thread로 변경됩니다.
실제 적용 예시
@RestController
public class UserController {
@GetMapping("/users/{id}")
public UserDetailResponse getUserDetail(@PathVariable Long id) {
// 전체 메서드가 Virtual Thread에서 실행
// 사용자 정보 조회 - 언마운트 발생
User user = userService.findById(id);
// 권한 정보 조회 - 언마운트 발생
List<Permission> permissions = permissionService.findByUserId(id);
// 외부 프로필 이미지 조회 - 언마운트 발생
String profileImageUrl = profileService.getImageUrl(user.getEmail());
return UserDetailResponse.of(user, permissions, profileImageUrl);
}
}
CPU 작업 혼합 시 최적화 방안
보고서 생성과 같이 CPU 계산이 포함된 경우, CPU 작업만 별도 Thread Pool로 분리하는 것이 효과적입니다.
@Configuration
@EnableAsync
public class ThreadPoolConfig {
@Bean("cpuExecutor")
public ExecutorService cpuExecutor() {
return Executors.newFixedThreadPool(
Runtime.getRuntime().availableProcessors()
);
}
}
@Service
public class ReportService {
@Autowired
@Qualifier("cpuExecutor")
private ExecutorService cpuExecutor;
public CompletableFuture<ReportResponse> generateReport(ReportRequest request) {
return CompletableFuture
// I/O 작업: 데이터 수집 (Virtual Thread)
.supplyAsync(() -> dataService.collectData(request))
// CPU 작업: 복잡한 계산 (Platform Thread Pool)
.thenApplyAsync(data -> processCalculation(data), cpuExecutor)
// I/O 작업: 결과 저장 (Virtual Thread)
.thenApply(result -> dataService.save(result));
}
}
도입 시 고려사항
DB Connection Pool 용량 검토
Virtual Thread로 동시 처리량이 증가하면 DB 커넥션 부족 현상이 발생할 수 있습니다. 기존 스레드 풀 크기에 맞춰 설정된 커넥션 풀을 재검토해야 합니다.
외부 API Rate Limiting 대비
동시 호출량 급증으로 외부 서비스에서 Rate Limit에 걸릴 가능성이 있습니다. Circuit Breaker 패턴이나 Rate Limiter 도입을 검토해야 합니다.
로그 시스템 영향도 분석
처리량 증가에 따른 로그 급증으로 다음과 같은 문제가 발생할 수 있습니다:
- 로그 파일 크기 급증
- 로그 I/O 병목 현상
- 디스크 용량 부족
메모리 사용 패턴 변화
- Direct Memory 사용량 증가 가능성
- Young Generation GC 빈도 변화
- 기존 JVM 튜닝 파라미터 재검토 필요
도입 적합성 판단 기준
적극 도입 권장 환경
- 웹 서버, REST API 서비스
- 마이크로서비스 아키텍처
- I/O 집약적 배치 처리
- 실시간 통신 서버
신중한 검토 필요 환경
- CPU 집약적 애플리케이션
- synchronized 사용이 많은 레거시 코드
- 엄격한 성능 요구사항이 있는 시스템
도입 전 체크포인트
- 현재 병목 지점이 I/O 중심인지 확인
- DB Connection Pool 여유분 검토
- 외부 API 의존성 및 제약사항 파악
- 기존 코드의 synchronized 사용량 분석
- 모니터링 체계의 Virtual Thread 대응 가능성 확인
학습 결론
Virtual Thread는 I/O 바운드 작업이 많은 웹 애플리케이션에서 상당한 성능 향상을 기대할 수 있는 기술입니다.
특히 설정이 간단하고 기존 코드 변경이 최소화되어 도입 부담이 적습니다.
다만 synchronized 블록의 제약사항이나 의존 시스템에 미치는 영향을 충분히 검토해야 합니다.