Structured 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와 함께 사용하면 복잡한 스레드 관리 없이도 안전하고 효율적인 동시성 코드를 작성할 수 있다.