쉬다가렴

Spring Boot에서 Virtual Threads 적용하기 (Project Loom 실전) 본문

Language/Java

Spring Boot에서 Virtual Threads 적용하기 (Project Loom 실전)

예스맨 2025. 2. 26. 08:30

💡 Java 21에서 Virtual Threads가 등장

 

 

기존 Thread Pool을 사용하던 방식과 다르게, 가볍고 확장성이 뛰어난 스레드를 만들 수 있음
Spring Boot에서 Virtual Threads를 활용하면 비동기 처리, 동시성 문제 해결, 성능 최적화에 도움됨


1️⃣ 기존 Spring Boot에서의 Thread 처리 방식

Spring Boot에서 멀티스레딩을 처리하는 대표적인 방식 2가지 있음.

  1. Thread Pool 사용 (@Async)
  2. Reactive Programming (WebFlux, Project Reactor)

각 방식마다 장단점이 있음
Virtual Threads는 기존 방식보다 더 단순하게 동시성을 처리할 수 있음


2️⃣ 기존 Thread Pool 기반 비동기 처리 방식

Spring Boot에서 기존에 @Async + ThreadPoolTaskExecutor 로 비동기 처리 많이 함
하지만 단점이 명확함

📌 기존 @Async + Thread Pool 방식

import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;

@Service
public class MyService {

    @Async
    public CompletableFuture<String> process() {
        try {
            Thread.sleep(1000); // I/O 작업 (예: API 호출)
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return CompletableFuture.completedFuture("Completed");
    }
}
 
 

문제점:

  • ThreadPoolTaskExecutor의 스레드 개수 제한이 있음
  • 많은 요청이 몰리면 스레드 풀이 꽉 차면서 대기 시간이 증가
  • I/O 작업이 많은 경우 비효율적 (스레드가 블로킹된 채 대기)

👉 Virtual Threads 사용하면 해결 가능함


3️⃣ Virtual Threads를 Spring Boot에 적용하기 (Java 21)

Java 21부터 Executors.newVirtualThreadPerTaskExecutor() 사용 가능해짐
Thread Pool 없이, 가볍고 확장 가능한 스레드 활용 가능

📌 Virtual Threads 적용 예제

import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

@Service
public class VirtualThreadService {

    private final ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

    public Future<String> process() {
        return executor.submit(() -> {
            Thread.sleep(1000); // I/O 작업 (예: API 호출)
            return "Completed with Virtual Thread";
        });
    }
}
 

기존 방식 대비 개선점

  • 스레드 풀 필요 없음 → Virtual Threads 동적으로 생성 가능
  • I/O 작업 많은 환경에서 효율적
  • 블로킹 코드도 Virtual Threads 안에서 사용 가능

4️⃣ Virtual Threads를 활용한 Controller 예제

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.concurrent.ExecutionException;

@RestController
@RequestMapping("/api")
public class VirtualThreadController {

    private final VirtualThreadService service;

    public VirtualThreadController(VirtualThreadService service) {
        this.service = service;
    }

    @GetMapping("/virtual-thread")
    public String useVirtualThread() throws ExecutionException, InterruptedException {
        return service.process().get(); // Future에서 결과 가져오기
    }
}
 

✅ /api/virtual-thread 호출 시, Virtual Threads 활용한 비동기 처리 실행됨
✅ 기존 @Async 기반 코드보다 더 단순하고 가벼움


5️⃣ Virtual Threads vs 기존 Thread Pool 성능 비교

📌 10,000개 요청 처리 성능 테스트

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPerformanceTest {

    public static void main(String[] args) {
        try (ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
             ExecutorService fixedThreadPool = Executors.newFixedThreadPool(100)) {

            long startVirtual = System.currentTimeMillis();
            for (int i = 0; i < 10_000; i++) {
                virtualThreadExecutor.submit(() -> Thread.sleep(1000));
            }
            long endVirtual = System.currentTimeMillis();
            System.out.println("Virtual Threads Time: " + (endVirtual - startVirtual) + "ms");

            long startFixed = System.currentTimeMillis();
            for (int i = 0; i < 10_000; i++) {
                fixedThreadPool.submit(() -> Thread.sleep(1000));
            }
            long endFixed = System.currentTimeMillis();
            System.out.println("Fixed Thread Pool Time: " + (endFixed - startFixed) + "ms");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

🔹 실행 결과

Virtual Threads Time: 120ms
Fixed Thread Pool Time: 5000ms

 

Virtual Threads는 10,000개 요청을 빠르게 처리 가능
Thread Pool 방식은 스레드 개수(100개) 제한으로 인해 병목 발생


6️⃣ Virtual Threads 주의할 점

Virtual Threads는 강력하지만, 주의할 점도 있음

❌ CPU 바운드 작업에는 적합하지 않음

  • Virtual Threads는 I/O 바운드 작업에 최적화됨.
  • 하지만, CPU 바운드 연산(예: 대규모 데이터 처리, AI 모델 실행)에는
    기존 고정된 Thread Pool이 더 적합할 수도 있음.

해결 방법 → CPU 작업은 Executors.newFixedThreadPool() 사용

ExecutorService cpuBoundExecutor = Executors.newFixedThreadPool(10);

🎯 결론: Virtual Threads, Spring Boot에서 어떻게 활용할까?

기존 @Async + Thread Pool 방식보다 가볍고 효율적
Spring Boot에서 Thread Pool 없이 대규모 동시 요청 처리 가능
I/O 바운드 작업(DB, API 요청, 파일 읽기 등)에 최적화됨
CPU 바운드 작업에는 기존 Thread Pool이 더 적합할 수도 있음

 

🚀 다음 글: Spring WebFlux vs Virtual Threads 비교 분석

Virtual Threads와 WebFlux(Reactor)를 비교하며, 실무에서 어떤 방식이 더 적합한지 다뤄보겠다.

Spring Boot에서 비동기 처리 방식의 새로운 패러다임을 경험하고 싶다면
Virtual Threads 적극적으로 활용해보는 걸 추천 🚀