Contents
see ListStructured Concurrency란?
Java 24의 JEP 505 Structured Concurrency(5차 프리뷰)는 여러 스레드에서 실행되는 관련 작업들을 하나의 작업 단위로 묶어, 에러 처리와 취소를 단순화하는 API다. 자식 스레드의 생명주기가 부모에 종속되어 스레드 누수와 취소 지연 같은 문제를 근본적으로 해결한다.
기존 방식의 문제점
// 기존: ExecutorService 사용 시 문제
ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();
try {
Future<User> userFuture = executor.submit(() -> findUser(userId));
Future<Order> orderFuture = executor.submit(() -> findOrder(orderId));
// 문제 1: findUser()가 실패해도 findOrder()는 계속 실행
// 문제 2: 여기서 예외 발생 시 두 Future 모두 방치 (스레드 누수)
User user = userFuture.get();
Order order = orderFuture.get();
return new UserOrder(user, order);
} finally {
executor.shutdown();
}StructuredTaskScope 기본 사용법
컴파일: javac --release 24 --enable-preview Main.java
실행: java --enable-preview Main
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Subtask;
// ShutdownOnFailure: 하나라도 실패하면 나머지 자동 취소
UserOrder fetchUserOrder(long userId, long orderId) throws Exception {
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
Subtask<User> userTask = scope.fork(() -> findUser(userId));
Subtask<Order> orderTask = scope.fork(() -> findOrder(orderId));
scope.join(); // 모든 서브태스크 완료 대기
// 둘 다 성공한 경우에만 여기에 도달
return new UserOrder(userTask.get(), orderTask.get());
}
// try-with-resources가 scope를 닫으면 미완료 서브태스크 자동 취소
}Joiner 전략 패턴
Java 24에서는 다양한 Joiner 전략을 통해 서브태스크 완료 조건을 제어한다.
// 1. 모든 태스크 성공 필수 (기본)
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow())) {
var a = scope.fork(() -> callServiceA());
var b = scope.fork(() -> callServiceB());
scope.join();
return combine(a.get(), b.get());
}
// 2. 첫 번째 성공 결과 사용 (나머지 취소)
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.<String>anySuccessfulResultOrThrow())) {
scope.fork(() -> queryMirror1(query));
scope.fork(() -> queryMirror2(query));
scope.fork(() -> queryMirror3(query));
return scope.join(); // 가장 빨리 성공한 결과 반환
}
// 3. 모든 태스크 완료 대기 (성공/실패 무관)
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAll())) {
var tasks = urls.stream()
.map(url -> scope.fork(() -> fetchUrl(url)))
.toList();
scope.join();
return tasks.stream()
.filter(t -> t.state() == Subtask.State.SUCCESS)
.map(Subtask::get)
.toList();
}Virtual Threads와 결합
Structured Concurrency는 Virtual Threads(JEP 444, 정식)와 결합하면 극대화된다. fork()로 생성되는 각 서브태스크가 Virtual Thread에서 실행되어 수천 개의 동시 작업도 가볍게 처리한다.
// 대량 API 호출: 1000개 요청을 동시에 안전하게 처리
List<String> fetchAllProducts(List<Long> productIds) throws Exception {
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAll())) {
var subtasks = productIds.stream()
.map(id -> scope.fork(() -> {
// 각각 Virtual Thread에서 실행
return httpClient.send(
HttpRequest.newBuilder()
.uri(URI.create("/api/products/" + id))
.build(),
HttpResponse.BodyHandlers.ofString()
).body();
}))
.toList();
scope.join();
return subtasks.stream()
.filter(t -> t.state() == Subtask.State.SUCCESS)
.map(Subtask::get)
.toList();
}
}에러 처리와 타임아웃
import java.time.Instant;
import java.time.Duration;
// 타임아웃 설정
try (var scope = StructuredTaskScope.open(
StructuredTaskScope.Joiner.awaitAllSuccessfulOrThrow(),
cf -> cf.withTimeout(Duration.ofSeconds(5)))) {
var user = scope.fork(() -> findUser(1L));
var order = scope.fork(() -> findOrder(100L));
scope.join(); // 5초 내에 모두 완료되지 않으면 TimeoutException
return new UserOrder(user.get(), order.get());
}
// 타임아웃 발생 시 모든 서브태스크 자동 취소기존 코드 마이그레이션 체크리스트
1. ExecutorService + Future 패턴을 StructuredTaskScope로 교체
2. CompletableFuture.allOf() 대신 awaitAllSuccessfulOrThrow 사용
3. CompletableFuture.anyOf() 대신 anySuccessfulResultOrThrow 사용
4. --enable-preview 플래그 필수 (정식 포함 전까지)
5. 스레드 풀 크기 설정 불필요 - Virtual Thread가 자동 관리
Structured Concurrency는 Java의 동시성 프로그래밍을 근본적으로 바꾸는 패러다임 전환이다. 프리뷰 단계이지만 Virtual Threads와 함께 사용하면 복잡한 스레드 관리 없이도 안전하고 효율적인 동시성 코드를 작성할 수 있다.
java
| No | 작성일 | Title |
|---|---|---|
| 3356 | 2026. 08. 14. | Java record와 sealed interface로 안전한 API 응답 모델 만들기 |
| 3324 | 2026. 08. 05. | Java 가상 스레드 실전 적용 가이드: I/O 중심 API의 동시성 구조 바꾸기 |
| 3292 | 2026. 07. 28. | Java 메모리 사용량이 계속 늘어날 때: Heap Dump와 JFR로 누수 원인 찾기 |
| 3261 | 2026. 07. 20. | Java record로 API DTO를 단순하게 만드는 방법: 검증·변환·직렬화 실전 적용 |
| 3235 | 2026. 07. 12. | Java API 동시 호출 폭주 대응: Thread Pool, timeout, 장애 격리로 안정성 올리기 |
| 3178 | 2026. 07. 03. | Java 메모리 누수 진단 가이드: jcmd와 JFR로 원인 좁히기 |
| 3130 | 2026. 06. 25. | Java 가상 스레드 적용 전 점검: 블로킹 I/O와 동시성 제한 운영 가이드 |
| 3074 | 2026. 06. 17. | Java 스레드 작업 취소 설계: interrupt, Future, ExecutorService 종료를 제대로 연결하기 |
| 2985 | 2026. 06. 09. | Java 컨테이너 메모리 운영 가이드: 힙, 직접 메모리, GC 로그를 함께 점검하기 |
| 2928 | 2026. 06. 01. | Java Virtual Threads 운영 전환 가이드: 블로킹 API, 커넥션 풀, 관측 지표 점검법 |