자바 언어·플랫폼 용어 사전
모던 자바Structured Concurrency · StructuredTaskScope · Scoped Value · JEP 506

구조적 동시성

여러 작업의 수명을 코드 블록에 묶는 모델. 누수와 취소 누락을 구조로 막는다.

여러 동시 작업의 수명을 코드 블록에 묶는 모델.

try (var scope = StructuredTaskScope.open()) {
    var user  = scope.fork(() -> fetchUser(id));
    var order = scope.fork(() -> fetchOrders(id));
    scope.join();                       // 블록을 벗어나기 전에 모두 끝난다
    return new Page(user.get(), order.get());
}

기존 방식의 문제

var f1 = executor.submit(taskA);
var f2 = executor.submit(taskB);
var a = f1.get();     // 여기서 예외가 나면
var b = f2.get();     // f2 는 계속 돌고 있다 — 아무도 취소하지 않는다
  • 한쪽이 실패해도 다른 쪽이 계속 돈다 (자원 낭비 · 유령 작업)
  • 취소 전파를 매번 손으로 짜야 한다
  • 스택트레이스가 끊긴다 — 누가 이 작업을 시작했는지 알 수 없다

원인은 '작업의 수명' 과 '코드 블록' 이 분리돼 있다는 점이다 structured 는 이 둘을 다시 붙인다 — 마치 try 블록이 자원을 묶듯이

실패 정책

  • 하나라도 실패하면 전부 취소 — → 모두 필요한 조합 작업
  • 하나라도 성공하면 나머지 취소 — → 여러 소스 중 가장 빠른 응답 채택
  • Joiner 로 정책을 지정한다 (JDK 25 에서 API 가 정리됐다)

ScopedValue — ThreadLocal 의 대체

private static final ScopedValue<User> CURRENT = ScopedValue.newInstance();

ScopedValue.where(CURRENT, user).run(() -> {
    handle();          // 이 블록 안에서만 보인다. 자식 작업에 자동 상속
});

ThreadLocal 의 문제

  • 언제 지워야 하는지 불명확 → 스레드 풀에서 값이 남아 다음 요청에 샌다
  • 가변이라 어디서 바뀌는지 추적이 어렵다
  • 가상 스레드를 수십만 개 만들면 그만큼 복제된다

ScopedValue 는 불변이고 범위가 명확해 이 문제들이 구조적으로 사라진다

가상 스레드와 짝을 이루는 이유

작업마다 스레드를 만드는 것이 싸져야(가상 스레드) 블록 단위로 묶는 모델이 성립한다. 두 기능은 함께 설계됐다.

취소가 실제로 전파되는 조건

자바의 취소는 협조적이다 — 강제로 죽이지 않는다 scope 가 취소되면 자식 스레드에 인터럽트를 건다 자식이 인터럽트를 확인하거나 블로킹 지점에 있어야 멈춘다

따라서

  • 인터럽트를 삼키는 코드가 있으면 안 멈춘다
  • CPU 를 계속 도는 루프는 직접 확인해야 한다
    • if (Thread.interrupted()) throw new InterruptedException();

"취소했는데 안 멈춘다" 의 원인은 대부분 InterruptedException 을 잡고 무시한 코드다

관찰 가능성이 좋아진다

  • 기존 — 스레드 덤프에 풀 스레드가 각자 떠 있다
    • 누가 이 작업을 시작했는지 알 수 없다

구조적 부모-자식 관계가 런타임에 유지된다

  • 스레드 덤프가 트리로 보인다
  • 어느 요청의 어느 단계에서 막혔는지 바로 읽힌다

동시성 코드의 디버깅 난이도가 크게 달라지는 지점이다

면접 함정

  • "try-with-resources처럼 생겼으니 자원 정리용" → 작업의 수명과 취소를 묶는 것이다.
  • "ScopedValue는 ThreadLocal의 이름만 바꾼 것" → 불변 + 범위 한정이라는 점이 본질이다.

함께 보면 좋은 용어

노트에서 맥락과 함께 보기 — 모듈·모던 Java