Java 21 Virtual Thread 성능 실험 및 분석

이성혁·2025년 7월 6일
0
post-thumbnail

들어가며

100,000개의 HTTP 요청을 동시에 처리해야 한다면 Traditional Thread Pool을 사용한다면 수천 개의 Thread가 필요하고, 메모리 부족으로 애플리케이션이 다운될 수 있습니다. 하지만 Java 21의 Virtual Thread를 사용하면 단 몇 개의 Platform Thread로도 동일한 작업을 처리할 수 있습니다.

과연 이것이 가능할까요? 실제로 얼마나 성능 차이가 날까요?

이 글에서는 직접 실험을 통해 Virtual Thread의 성능을 측정하고, Traditional Thread와 비교 분석한 결과를 공유합니다.

Java 동시성의 진화

Traditional Thread의 한계

Java의 Traditional Thread는 OS Thread와 1:1 매핑됩니다. 이는 다음과 같은 문제를 야기합니다:

// Traditional Thread Pool 설정
@Configuration
public class ThreadPoolConfig {
    @Bean
    public ThreadPoolTaskExecutor taskExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(50);      // 최소 Thread 수
        executor.setMaxPoolSize(200);      // 최대 Thread 수
        executor.setQueueCapacity(500);    // 대기 큐 크기
        return executor;
    }
}

Traditional Thread는 각각 상당한 메모리를 사용하며, 많은 수의 Thread 생성 시 메모리 부족 문제가 발생할 수 있습니다. 또한 Context Switching 비용이 높아 성능 저하를 야기합니다.

Virtual Thread의 등장

Virtual Thread는 이러한 한계를 해결하기 위해 Java 21에서 도입되었습니다:

// Traditional Thread 방식
public class TraditionalThreadExample {
    private final ExecutorService executor = Executors.newFixedThreadPool(200);
    
    public void handleRequests() {
        for (int i = 0; i < 10000; i++) {
            executor.submit(() -> {
                // I/O 작업 시뮬레이션
                try {
                    Thread.sleep(1000); // 1초 대기
                    System.out.println("작업 완료: " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
    }
}

// Virtual Thread 방식
public class VirtualThreadExample {
    public void handleRequests() {
        for (int i = 0; i < 10000; i++) {
            Thread.startVirtualThread(() -> {
                // 동일한 I/O 작업
                try {
                    Thread.sleep(1000); // 1초 대기
                    System.out.println("작업 완료: " + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            });
        }
    }
}

// Spring Boot에서 Virtual Thread 활성화
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        // JVM 옵션: --enable-preview (Java 19-20에서)
        // Java 21부터는 정식 기능
        System.setProperty("jdk.virtualThreadScheduler.parallelism", "10");
        SpringApplication.run(Application.class, args);
    }
    
    @Bean
    public TomcatProtocolHandlerCustomizer<?> protocolHandlerVirtualThreadExecutorCustomizer() {
        return protocolHandler -> {
            protocolHandler.setExecutor(Executors.newVirtualThreadPerTaskExecutor());
        };
    }
}

핵심 차이점:

  • Traditional Thread: 200개의 Platform Thread로 제한, 10,000개 작업 처리 시 대기 발생
  • Virtual Thread: 10,000개의 Virtual Thread를 즉시 생성, Platform Thread는 소수만 사용
  • 메모리 사용량: Traditional Thread는 Thread당 1-2MB, Virtual Thread는 수 KB만 사용

Virtual Thread는 Platform Thread 위에서 실행되는 경량 Thread로, 수백만 개를 생성해도 메모리 사용량이 크게 증가하지 않습니다.

실험 설계

테스트 환경

  • 애플리케이션: Spring Boot 3.2 + MySQL 8.0
  • 테스트 도구: K6 Load Testing
  • 테스트 시간: 7분 (420초)
  • 최대 동시 사용자: 35 VUs
  • 테스트 시나리오: 6단계 점진적 부하 증가

테스트 시나리오

각 요청은 다음과 같은 I/O 작업을 수행합니다:

  1. 데이터베이스 조회: 상품 정보 조회 (100~300ms 랜덤 지연)
  2. 외부 API 호출: 재고 정보 조회 (150~400ms 랜덤 지연)
  3. 캐시 조회: 추천 상품 조회 (50~200ms 랜덤 지연)
  4. 가격 정보 조회: 경쟁사 가격 조회 (100~250ms 랜덤 지연)

예상 응답 시간 범위:

  • 최소: 약 200ms (모든 I/O 작업이 최소 지연 시간으로 완료)
  • 최대: 약 1,150ms (모든 I/O 작업이 최대 지연 시간으로 완료)
  • 평균: 약 675ms (중간값 기준)

실제 I/O 작업들은 병렬로 처리되므로, 가장 오래 걸리는 작업의 시간에 따라 전체 응답 시간이 결정됩니다.

실험 결과

Virtual Thread 성능 (K6 상세 결과)

실제 K6 테스트 출력:

█ THRESHOLDS 

  errors
  ✓ 'rate<0.10' rate=0.00%

  http_req_duration
  ✓ 'p(95)<5000' p(95)=549.56ms

    {expected_response:true}
    ✓ 'p(90)<3000' p(90)=503.73ms

  response_time
  ✓ 'p(95)<5000' p(95)=549.59ms


█ TOTAL RESULTS 

  checks_total.......................: 56252   133.808896/s
  checks_succeeded...................: 100.00% 56252 out of 56252
  checks_failed......................: 0.00%   0 out of 56252

  ✓ Virtual Thread - Status is 200
  ✓ Virtual Thread - Response time < 3s
  ✓ Virtual Thread - Has product data
  ✓ Virtual Thread - Content-Type is JSON

  CUSTOM
  errors..................................................................: 0.00%  0 out of 14063
  requests................................................................: 14063  33.452224/s
  response_time...........................................................: avg=405.66ms min=204.79ms med=394.2ms  max=1.45s p(90)=503.73ms p(95)=549.59ms

  HTTP
  http_req_duration.......................................................: avg=405.64ms min=118.33ms med=394.19ms max=1.45s p(90)=503.73ms p(95)=549.56ms
    { expected_response:true }............................................: avg=405.64ms min=118.33ms med=394.19ms max=1.45s p(90)=503.73ms p(95)=549.56ms
  http_req_failed.........................................................: 0.00%  0 out of 14064
  http_reqs...............................................................: 14064  33.454603/s

  EXECUTION
  iteration_duration......................................................: avg=606.59ms min=321.43ms med=598.56ms max=1.67s p(90)=741.54ms p(95)=787.29ms
  iterations..............................................................: 14063  33.452224/s
  vus.....................................................................: 1      min=1          max=35
  vus_max.................................................................: 35     min=35         max=35

  NETWORK
  data_received...........................................................: 10 MB  24 kB/s
  data_sent...............................................................: 2.0 MB 4.9 kB/s

running (7m00.4s), 00/35 VUs, 14063 complete and 0 interrupted iterations
default ✓ [======================================] 00/35 VUs  7m0s

기본 성능 지표:

  • 총 요청 수: 14,064건
  • 성공률: 100% (0 errors)
  • 평균 응답 시간: 405.66ms
  • 중간값 응답 시간: 394.2ms
  • 최대 응답 시간: 1.45s
  • 90th percentile: 503.73ms
  • 95th percentile: 549.59ms
  • 처리량 (RPS): 33.45 req/s

체크 결과:

  • 총 체크 수: 56,252개 (모든 체크 성공)
  • 체크 성공률: 100%
  • 응답 시간 < 3초: ✓ 모든 요청 통과
  • 상태 코드 200: ✓ 모든 요청 성공
  • JSON 응답: ✓ 모든 요청 정상

Traditional Thread 성능 (K6 상세 결과)

실제 K6 테스트 출력:

█ THRESHOLDS 

  errors
  ✓ 'rate<0.10' rate=0.00%

  http_req_duration
  ✓ 'p(95)<5000' p(95)=1.53s

    {expected_response:true}
    ✓ 'p(90)<3000' p(90)=1.45s

  response_time
  ✓ 'p(95)<5000' p(95)=1.53s


█ TOTAL RESULTS 

  checks_total.......................: 29248   69.533637/s
  checks_succeeded...................: 100.00% 29248 out of 29248
  checks_failed......................: 0.00%   0 out of 29248

  ✓ Traditional Thread - Status is 200
  ✓ Traditional Thread - Response time < 3s
  ✓ Traditional Thread - Has product data
  ✓ Traditional Thread - Content-Type is JSON

  CUSTOM
  errors..................................................................: 0.00%  0 out of 7312
  requests................................................................: 7312   17.383409/s
  response_time...........................................................: avg=966.61ms min=207.29ms med=990.32ms max=2.27s p(90)=1.45s p(95)=1.53s

  HTTP
  http_req_duration.......................................................: avg=966.5ms  min=155.13ms med=990.3ms  max=2.27s p(90)=1.45s p(95)=1.53s
    { expected_response:true }............................................: avg=966.5ms  min=155.13ms med=990.3ms  max=2.27s p(90)=1.45s p(95)=1.53s
  http_req_failed.........................................................: 0.00%  0 out of 7313
  http_reqs...............................................................: 7313   17.385787/s

  EXECUTION
  iteration_duration......................................................: avg=1.16s    min=309.47ms med=1.18s    max=2.47s p(90)=1.65s p(95)=1.74s
  iterations..............................................................: 7312   17.383409/s
  vus.....................................................................: 1      min=1         max=35
  vus_max.................................................................: 35     min=35        max=35

  NETWORK
  data_received...........................................................: 5.2 MB 12 kB/s
  data_sent...............................................................: 1.1 MB 2.7 kB/s

running (7m00.6s), 00/35 VUs, 7312 complete and 0 interrupted iterations
default ✓ [======================================] 00/35 VUs  7m0s

기본 성능 지표:

  • 총 요청 수: 7,313건
  • 성공률: 100% (0 errors)
  • 평균 응답 시간: 966.61ms
  • 중간값 응답 시간: 990.32ms
  • 최대 응답 시간: 2.27s
  • 90th percentile: 1.45s
  • 95th percentile: 1.53s
  • 처리량 (RPS): 17.38 req/s

체크 결과:

  • 총 체크 수: 29,248개 (모든 체크 성공)
  • 체크 성공률: 100%
  • 응답 시간 < 3초: ✓ 모든 요청 통과
  • 상태 코드 200: ✓ 모든 요청 성공
  • JSON 응답: ✓ 모든 요청 정상

객관적 분석 및 해석

테스트 결과 검증

위 K6 테스트 결과를 분석하면 몇 가지 중요한 사실을 확인할 수 있습니다:

1. 테스트 환경의 일관성

  • 동일한 테스트 조건: 두 테스트 모두 7분간 실행, 최대 35 VUs
  • 동일한 검증 기준: 모든 임계값(THRESHOLDS) 동일하게 설정
  • 동일한 체크 항목: Status 200, Response time < 3s, JSON 응답 등

2. 처리량 차이의 실제 의미

Virtual Thread:   14,063 iterations (33.45 req/s)
Traditional Thread: 7,312 iterations (17.38 req/s)
비율: 1.92배 (약 92% 증가)

이는 동일한 하드웨어 환경에서 Virtual Thread가 거의 2배에 가까운 처리 성능을 보여준다는 의미입니다.

3. 응답 시간 분포 분석

Virtual Thread 응답 시간 분포:

  • 최소: 204.79ms (실제 I/O 대기 시간에 근접)
  • 중간값: 394.2ms (대부분의 요청이 400ms 내외)
  • 평균: 405.66ms (중간값과 유사하여 일관된 분포)
  • 95th: 549.59ms (상위 5% 요청도 550ms 이내)

Traditional Thread 응답 시간 분포:

  • 최소: 207.29ms (Virtual Thread와 유사한 최적 조건)
  • 중간값: 990.32ms (대부분의 요청이 1초 소요)
  • 평균: 966.61ms (중간값과 유사하지만 Virtual Thread 대비 2.4배)
  • 95th: 1.53s (상위 5% 요청이 1.5초 이상)

성능 차이의 원인 분석

1. Thread Pool 포화 현상

Traditional Thread의 결과에서 주목할 점:

  • 최소 응답 시간(207.29ms)은 Virtual Thread와 거의 동일
  • 하지만 중간값이 990.32ms로 급격히 증가

이는 Thread Pool이 포화되어 요청 대기 시간이 발생했음을 의미합니다.

2. Context Switching과 Thread 관리 오버헤드

Context Switching이란?

  • OS가 CPU를 한 Thread에서 다른 Thread로 전환하는 과정
  • 현재 Thread의 상태(레지스터, 스택 포인터 등)를 저장하고 다음 Thread의 상태를 복원
  • Traditional Thread는 OS 수준에서 Context Switching 발생
  • Virtual Thread는 JVM 수준에서 관리되어 Context Switching 비용 최소화

테스트 결과의 한계점

1. 테스트 규모의 제한

  • VU 수: 35개로 제한적 (실제 운영 환경 대비 소규모)
  • 테스트 시간: 7분간 (장시간 부하 테스트 필요)
  • 단일 시나리오: I/O 집약적 작업만 테스트

2. 환경적 제약

  • 로컬 환경: 실제 분산 환경과 다를 수 있음
  • 네트워크 지연: 실제 외부 API 호출 시뮬레이션의 한계
  • 데이터베이스: 단일 MySQL 인스턴스 사용

3. 측정되지 않은 요소들

  • 메모리 사용량: 실제 힙 메모리 사용량 미측정
  • CPU 사용률: 프로세서 리소스 사용 패턴 미확인
  • GC 영향: 가비지 컬렉션 빈도 및 영향 미분석

실제 운영 환경 적용 시 고려사항

1. 확장성 예측

현재 테스트 결과를 바탕으로 한 추정:

  • 현재 성능: Virtual Thread 33.45 req/s vs Traditional Thread 17.38 req/s
  • 1000 VU 환경 예상: 비례적으로 증가한다면 Virtual Thread가 여전히 우위
  • 실제 제약: 데이터베이스 커넥션 풀, 네트워크 대역폭 등이 병목이 될 수 있음

2. 비즈니스 임팩트 계산

서버 비용 절감 계산:

필요 서버 수 = 목표 처리량 / 서버당 처리량
Traditional Thread: 100,000 req/s ÷ 17.38 req/s = 5,756대
Virtual Thread: 100,000 req/s ÷ 33.45 req/s = 2,989대
절감율: (5,756 - 2,989) ÷ 5,756 = 48.1%

주의사항: 이는 선형적 확장을 가정한 것으로, 실제 환경에서는 다른 병목 요소들이 영향을 줄 수 있습니다.

3. 사용자 경험 개선

응답 시간 개선 효과:

  • 평균 응답 시간: 966.61ms → 405.66ms (58% 개선)
  • 95th percentile: 1.53s → 549.59ms (64% 개선)

사용자 만족도 예상 효과:

  • 1초 이내 응답률: Traditional Thread 약 50% → Virtual Thread 약 75%
  • 페이지 이탈률 감소 및 전환율 향상 기대

결론 및 권장사항

1. 도입 권장 시나리오

즉시 적용 가능한 영역:

  • I/O 집약적 애플리케이션: 테스트 결과가 직접적으로 적용 가능
    • RESTful API 서버 (데이터베이스 조회, 외부 API 호출)
    • 파일 처리 시스템 (업로드, 다운로드, 변환)
    • 메시지 큐 처리 (Kafka, RabbitMQ 컨슈머)
  • 마이크로서비스 아키텍처: 서비스 간 통신이 많은 환경
    • 서비스 메시 환경에서 다수의 downstream 호출
    • 분산 트랜잭션 처리
  • API 게이트웨이: 다수의 백엔드 서비스 호출이 필요한 경우

2. 신중한 검토가 필요한 경우

기술적 제약사항:

  • CPU 집약적 작업: 계산 위주의 작업에서는 성능 향상 제한적
    • 복잡한 알고리즘 처리
    • 이미지/비디오 인코딩
    • 암호화/복호화 작업
  • 기존 최적화가 잘 된 시스템: 개선 효과가 제한적일 수 있음
  • ThreadLocal 의존성이 높은 코드: 마이그레이션 비용 고려 필요

3. 단계적 도입 전략

  1. PoC 단계: 제한적 범위에서 성능 검증
  2. A/B 테스트: 실제 트래픽으로 비교 검증
  3. 점진적 확대: 검증된 영역부터 단계적 적용

이러한 객관적 분석을 통해 Virtual Thread의 장점과 한계를 명확히 이해하고, 실제 도입 시 합리적인 의사결정을 내릴 수 있습니다.

결론

이번 실험을 통해 Virtual Thread가 I/O 집약적인 애플리케이션에서 Traditional Thread 대비 현저한 성능 향상을 제공함을 확인했습니다.

핵심 성능 지표

  • 처리량 1.9배 증가 (17.38 → 33.45 req/s)
  • 평균 응답 시간 2.4배 개선 (966.61ms → 405.66ms)
  • 95th percentile 2.8배 향상 (1.53s → 549.59ms)
  • 서버 비용 48% 절감 가능

실제 측정된 장점

  • 모든 요청에서 100% 성공률 달성
  • 일관된 성능으로 사용자 경험 향상
  • 동일한 하드웨어에서 더 높은 처리량 달성

Virtual Thread, 만능 해결책이 아닌 현명한 선택지

하지만 Virtual Thread가 모든 상황에서 Traditional Thread를 대체해야 하는 것은 아닙니다. 이번 실험 결과가 보여주는 것은 특정 조건에서의 성능 우위이며, 실제 도입 시에는 다음과 같은 균형잡힌 접근이 필요합니다.

Virtual Thread가 효과적인 영역

  • I/O 대기 시간이 많은 애플리케이션
  • 외부 API 호출이 빈번한 마이크로서비스
  • 동시 연결 수가 많은 웹 서버
  • 파일 처리나 네트워크 통신 중심의 배치 작업

기존 Traditional Thread가 여전히 적합한 영역

  • CPU 집약적 연산 작업
  • 이미 최적화가 잘 된 고성능 시스템
  • ThreadLocal에 의존도가 높은 레거시 코드
  • 안정성이 최우선인 미션 크리티컬 시스템

지속가능한 시스템 발전을 위한 접근

Virtual Thread는 기존 시스템을 전면 교체하라는 명령이 아니라, 점진적 개선을 위한 선택지입니다.

1. 기존 시스템과의 공존

  • 현재 잘 동작하는 Traditional Thread 기반 시스템을 유지하면서
  • 새로운 기능이나 병목 구간에만 Virtual Thread 적용
  • 하이브리드 아키텍처를 통한 점진적 전환

2. 비즈니스 가치 중심의 도입

  • 기술적 우수성보다는 실제 비즈니스 문제 해결에 집중
  • 개발팀의 학습 비용과 운영 복잡성 증가 고려
  • ROI가 명확한 영역부터 우선 적용

3. 장기적 관점의 기술 전략

  • 한 번에 모든 것을 바꾸려 하지 않고
  • 팀의 역량과 시스템의 특성에 맞는 속도로 진화
  • 기술 부채를 늘리지 않는 선에서 점진적 개선

실용적 권장사항

Virtual Thread는 혁신적인 기술이지만 신중한 도입이 필요한 도구입니다.

  • 새로운 프로젝트 I/O 집약적이라면 Virtual Thread를 기본 선택지로 고려
  • 기존 시스템 성능 병목이 명확한 구간부터 점진적 적용
  • 레거시 시스템 안정성을 해치지 않는 범위에서 제한적 도입

결국 Virtual Thread의 진정한 가치는 더 나은 사용자 경험과 효율적인 리소스 활용을 통해 지속가능한 서비스를 만드는 데 있습니다. 기술 자체의 우수성보다는 우리가 해결하고자 하는 문제에 얼마나 적합한지가 더 중요한 판단 기준이 되어야 합니다.

Java 21을 사용하고 있다면, Virtual Thread를 하나의 강력한 옵션으로 고려해보시기 바랍니다. 특히 I/O 집약적인 웹 애플리케이션에서는 기존 시스템의 성능을 한 단계 끌어올릴 수 있는 현실적인 솔루션이 될 것입니다.

profile
항상 배우는 자세로 🪴

0개의 댓글