Contents
see ListJava 25에서 드디어 Structured Concurrency가 정식 API로 확정되었습니다. Java 19부터 인큐베이터로 시작하여 여러 프리뷰를 거친 끝에, StructuredTaskScope API가 완전히 재설계되어 안정화되었습니다. Virtual Threads와 결합하면 수백만 개의 동시 작업을 안전하고 체계적으로 관리할 수 있습니다.
Structured Concurrency란
구조적 동시성은 동시 실행되는 작업들을 하나의 작업 단위로 묶어 관리하는 프로그래밍 패러다임입니다. 부모 작업이 자식 작업의 생명주기를 완전히 제어하며, 에러 전파와 취소가 자동으로 이루어집니다. 이는 기존의 비구조적 동시성(ExecutorService, CompletableFuture)이 가진 작업 누수, 취소 어려움, 에러 손실 문제를 근본적으로 해결합니다.
기존 방식의 문제점
// 기존 ExecutorService 방식의 문제
Response handleRequest() throws Exception {
ExecutorService executor = Executors.newCachedThreadPool();
try {
Future<UserData> userFuture = executor.submit(() -> fetchUser(userId));
Future<OrderData> orderFuture = executor.submit(() -> fetchOrders(userId));
// 문제 1: fetchUser가 실패해도 fetchOrders는 계속 실행
UserData user = userFuture.get();
// 문제 2: 여기서 예외 발생하면 orderFuture는 영원히 대기
OrderData orders = orderFuture.get();
return new Response(user, orders);
} finally {
executor.shutdown();
// 문제 3: shutdown 후에도 작업이 즉시 종료되지 않을 수 있음
}
}StructuredTaskScope 기본 사용법
Java 25에서 재설계된 StructuredTaskScope는 Joiner 인터페이스를 통해 결과 수집 전략을 정의합니다.
import java.util.concurrent.StructuredTaskScope;
import java.util.concurrent.StructuredTaskScope.Joiner;
import java.util.concurrent.StructuredTaskScope.Subtask;
// 기본: 모든 서브태스크가 완료될 때까지 대기
Response handleRequest(String userId) throws Exception {
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
Subtask<UserData> userTask = scope.fork(() -> fetchUser(userId));
Subtask<OrderData> orderTask = scope.fork(() -> fetchOrders(userId));
scope.join(); // 모든 서브태스크 완료 대기
// 결과 안전하게 접근
return new Response(userTask.get(), orderTask.get());
}
// try-with-resources로 자동 정리
// 하나가 실패하면 다른 작업도 자동 취소
}Joiner 전략 패턴
awaitAll - 모두 성공해야 할 때
// 결제 처리: 모든 단계가 성공해야 함
PaymentResult processPayment(PaymentRequest request) throws Exception {
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
var validateTask = scope.fork(() -> validateCard(request.cardNumber()));
var fraudTask = scope.fork(() -> checkFraud(request));
var balanceTask = scope.fork(() -> checkBalance(request.amount()));
scope.join();
// 모든 검증이 통과해야 다음 단계 진행
if (validateTask.get() && fraudTask.get() && balanceTask.get()) {
return executePayment(request);
}
throw new PaymentRejectedException("검증 실패");
}
}anySuccessfulResultOrThrow - 가장 빠른 성공 결과
// 여러 미러 서버 중 가장 빠른 응답 사용
String fetchFromMirrors(String resource) throws Exception {
try (var scope = StructuredTaskScope.open(
Joiner.anySuccessfulResultOrThrow())) {
scope.fork(() -> fetchFrom("mirror1.example.com", resource));
scope.fork(() -> fetchFrom("mirror2.example.com", resource));
scope.fork(() -> fetchFrom("mirror3.example.com", resource));
// 첫 번째 성공 결과 반환, 나머지는 자동 취소
return scope.join();
}
}allSuccessfulOrThrow - 모든 성공 결과 수집
// 여러 API에서 데이터를 수집하여 합산
List<StockPrice> getStockPrices(List<String> symbols) throws Exception {
try (var scope = StructuredTaskScope.open(
Joiner.allSuccessfulOrThrow())) {
for (String symbol : symbols) {
scope.fork(() -> fetchStockPrice(symbol));
}
// 모든 서브태스크의 성공 결과를 리스트로 반환
return scope.join();
}
}커스텀 Joiner 구현
특수한 결과 수집 전략이 필요한 경우 Joiner를 직접 구현할 수 있습니다.
// 최소 N개 성공하면 진행하는 커스텀 Joiner
public class AtLeastNJoiner<T> implements Joiner<T, List<T>> {
private final int minRequired;
private final List<T> results = new CopyOnWriteArrayList<>();
private final AtomicInteger successCount = new AtomicInteger(0);
public AtLeastNJoiner(int minRequired) {
this.minRequired = minRequired;
}
@Override
public boolean onComplete(Subtask<? extends T> subtask) {
if (subtask.state() == Subtask.State.SUCCESS) {
results.add(subtask.get());
return successCount.incrementAndGet() >= minRequired;
}
return false;
}
@Override
public List<T> result() {
return Collections.unmodifiableList(results);
}
}
// 사용: 5개 중 3개만 성공하면 진행
try (var scope = StructuredTaskScope.open(new AtLeastNJoiner<>(3))) {
for (String server : servers) {
scope.fork(() -> queryServer(server));
}
List<ServerResponse> responses = scope.join();
}Scoped Values와 함께 사용하기
Java 25에서 함께 정식화된 ScopedValue는 ThreadLocal의 안전한 대안으로, Structured Concurrency와 자연스럽게 결합됩니다.
// ScopedValue 정의
private static final ScopedValue<RequestContext> CONTEXT = ScopedValue.newInstance();
void handleRequest(HttpRequest request) {
var context = new RequestContext(request.userId(), request.traceId());
ScopedValue.runWhere(CONTEXT, context, () -> {
try (var scope = StructuredTaskScope.open(Joiner.awaitAll())) {
// 모든 서브태스크에서 CONTEXT 접근 가능
scope.fork(() -> {
String traceId = CONTEXT.get().traceId();
return auditLog(traceId);
});
scope.fork(() -> {
String userId = CONTEXT.get().userId();
return loadProfile(userId);
});
scope.join();
}
});
}Virtual Threads와의 시너지
StructuredTaskScope는 내부적으로 Virtual Threads를 사용합니다. 수만 개의 서브태스크를 fork해도 메모리 부담이 거의 없습니다.
// 10,000개 URL을 동시에 크롤링
List<PageContent> crawlAll(List<String> urls) throws Exception {
try (var scope = StructuredTaskScope.open(
Joiner.allSuccessfulOrThrow())) {
for (String url : urls) {
scope.fork(() -> crawlPage(url)); // 각각 Virtual Thread에서 실행
}
return scope.join(); // 모든 결과 수집
}
}Java 25의 Structured Concurrency는 동시성 프로그래밍의 복잡성을 획기적으로 줄여줍니다. Virtual Threads의 경량성과 StructuredTaskScope의 안전한 작업 관리, ScopedValue의 컨텍스트 전파가 결합되어, Java는 대규모 동시 처리 분야에서 가장 강력한 플랫폼으로 자리매김하고 있습니다.