
Java 21에서 정식으로 도입된 Virtual Thread는 높은 동시성 처리를 위한 혁신적인 기능입니다. 하지만 실제 현업에서 Virtual Thread를 도입할 때는 신중한 접근이 필요합니다. 이번 글에서는 카카오페이의 Virtual Thread 도입 경험을 바탕으로 실무 적용 시 고려사항을 살펴보고, 더불어 현재 사용 가능한 API들의 특징과 선택 기준을 알아보겠습니다.
리소스 효율성
개발 생산성
라이브러리 호환성
인프라 부하
시스템 안정성
spring.threads.virtual.enabled=false@Configuration
public class AsyncConfig {
@Bean
public Executor customTaskExecutor() {
return new TaskExecutorAdapter(
new VirtualThreadTaskExecutor("custom-task-")
);
}
}Virtual Thread를 구현할 때는 두 가지 주요 API를 선택할 수 있습니다: Preview 상태인 StructuredTaskScope와 안정화된 CompletableFuture입니다. 각각의 특징과 적용 사례를 살펴보겠습니다.
Preview 기능은 Java가 새로운 기능을 정식으로 채택하기 전에 개발자 커뮤니티의 피드백을 받기 위해 시험적으로 제공하는 기능입니다. StructuredTaskScope는 Project Loom의 일부로 제안된 구조적 동시성(Structured Concurrency) 구현체입니다.
컴파일러 설정
# 컴파일 시
javac --enable-preview --release 21 YourFile.java
# 실행 시
java --enable-preview YourFile
경고 메시지
java.util.concurrent.StructuredTaskScope.ShutdownOnFailure은(는)
테스트 버전 API이며 향후 릴리즈 버전에서 제거될 수 있습니다
public class ImageUploader {
public void uploadImages(List<ImageData> images) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
// 각 이미지에 대한 업로드 태스크 생성
for (ImageData image : images) {
scope.fork(() -> {
uploadImage(image);
return null;
});
}
// 모든 작업 완료 대기
scope.join().throwIfFailed();
} catch (InterruptedException | ExecutionException e) {
logger.error("이미지 업로드 중 오류 발생", e);
throw new ImageUploadException("업로드 실패", e);
}
}
}
public class ImageUploader {
public void uploadImages(List<ImageData> images) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<Void>> futures = images.stream()
.map(image -> CompletableFuture.runAsync(
() -> uploadImage(image),
executor
))
.toList();
// 모든 작업 완료 대기
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.join();
} catch (Exception e) {
logger.error("이미지 업로드 중 오류 발생", e);
throw new ImageUploadException("업로드 실패", e);
}
}
}
실제 업무에서 자주 마주치는 대량 데이터 처리 시나리오를 통해 두 API의 사용법을 비교해보겠습니다.
public class UserDataProcessor {
private final UserRepository userRepository;
private final NotificationService notificationService;
// StructuredTaskScope 사용 예시
public void processUsersWithStructuredScope(List<Long> userIds) {
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
for (Long userId : userIds) {
scope.fork(() -> {
User user = userRepository.findById(userId);
processUserData(user);
notificationService.notify(user);
return null;
});
}
scope.join().throwIfFailed();
} catch (Exception e) {
handleError(e);
}
}
// CompletableFuture 사용 예시
public void processUsersWithCompletableFuture(List<Long> userIds) {
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<CompletableFuture<Void>> futures = userIds.stream()
.map(userId -> CompletableFuture.runAsync(() -> {
User user = userRepository.findById(userId);
processUserData(user);
notificationService.notify(user);
}, executor))
.toList();
CompletableFuture.allOf(futures.toArray(CompletableFuture[]::new))
.join();
} catch (Exception e) {
handleError(e);
}
}
}
Virtual Thread는 Java의 동시성 처리에 있어 큰 혁신을 가져왔습니다. Preview API인 StructuredTaskScope는 더 간결하고 직관적인 코드를 작성할 수 있게 해주지만, 아직 실험적인 단계에 있습니다. 반면 CompletableFuture를 사용한 Stable API 방식은 더 안정적이지만 코드가 다소 복잡해질 수 있습니다.
프로젝트의 성격과 요구사항을 고려하여 적절한 방식을 선택하는 것이 중요합니다. Preview 기능들이 정식으로 채택되면 StructuredTaskScope를 사용하는 것이 더 좋은 선택이 될 것으로 예상됩니다.