-
JEP 506 Scoped ValuesJava/Java25 2025. 12. 16. 10:21
JEP 506: Scoped Values
ThreadLocal이 가진 문제점(변경 가능성, 무제한 수명, 가상 스레드에서의 비효율성)을 해결하고, 안전하고 효율적으로 스레드 내 및 하위 스레드 간에 불변(Immutable) 데이터를 공유할 수 있는 새로운 메커니즘을 제공합니다
incubator(20) : https://openjdk.org/jeps/429
preview(21) : https://openjdk.org/jeps/446
2nd preview(22) : https://openjdk.org/jeps/464
3rd preview(23) : https://openjdk.org/jeps/481
4th preview(24) : https://openjdk.org/jeps/487
final (25) : https://openjdk.org/jeps/506
Scoped Values 란?
- Scoped Value는 메서드 호출 스택과 자식 스레드에 걸쳐 불변 데이터를 안전하게 공유할 수 있는 기능입니다.
- 전통적인 ThreadLocal과 유사하지만, 값의 기간(lifetime)이 명확하게 제한되며, 불변성(immutable)을 보장합니다.
- 이로 인해 동시성 코드에서 안전하면서도 효율적인 컨텍스트 전달이 가능해집니다.
Scoped Values은 숨겨진 메서드 파라미터처럼 작동하면서, 호출자(caller) → 피호출자(callee) → 자식 스레드 전반에 불변 컨텍스트를 전달합니다
왜 필요한가?
기존 ThreadLocal 의 문제점
값의 기간 불분명 ThreadLocal은 명시적으로 제거하지 않으면 값이 계속 유지됩니다. 메모리 누수 위험 스레드 풀 환경에서 ThreadLocal 값이 누적될 수 있습니다. 복잡한 전달 메서드 체인을 수행하면서 여러 파라미터를 넘겨야 하는 경우가 많습니다. Scope Values 의 기본 개념
- 불변 데이터 공유: Scoped Value는 한 스레드 내에서 메서드 호출 스택을 따라, 그리고 구조화된 동시성(Structured Concurrency)을 사용하는 경우 하위 스레드와 불변 데이터를 안전하게 공유합니다.
- 유한한 수명: 값이 설정되는 것은 특정 코드 블록(스코프) 내에서만 유효하며, 블록 실행이 완료되면 값은 자동으로 이전 상태로 복원되거나 바인딩이 해제됩니다. ThreadLocal처럼 개발자가 명시적으로 remove()를 호출할 필요가 없습니다.
- 능력 기반 보안 모델 (Capability-based Security): Scoped Value 객체 자체를 소유한 코드만이 그 값에 접근할 수 있습니다. 이는 누구나 접근 가능한 ThreadLocal보다 더 나은 보안을 제공합니다.
Goal
- 사용 편의성 (Ease of use): 데이터 흐름을 추론하기 쉽게 만듭니다.
- 이해 가능성 (Comprehensibility): 공유 데이터의 수명이 코드의 구문 구조(Syntactic Structure)에서 명확하게 드러납니다.
- 견고성 (Robustness): 데이터를 공유한 호출자(Caller)는 정당한 호출 수신자(Callee)만 데이터를 검색할 수 있도록 합니다.
- 성능 (Performance): 데이터의 불변성과 수명 관리의 명확성을 통해 런타임 최적화를 가능하게 합니다. 특히 가상 스레드(Virtual Threads) 환경에서 ThreadLocal보다 훨씬 효율적입니다.
출처 : https://openjdk.org/jeps/446
Non-Goal
- 자바 프로그래밍 언어를 바꾸는 것이 목표는 아닙니다.
- ThreadLocal 변수 사용을 중단하도록 요구하거나 기존 API를 더 이상 사용하지 않도록 하는 것이 목표는 아닙니다 .
Scoped Values의 Life Cycle (생명 주기)
- 인스턴스 생성: ScopedValue.newInstance()를 사용하여 정적 필드로 선언합니다. 이 인스턴스가 "키" 역할을 합니다.
- 값 바인딩 (Binding): ScopedValue.where(key, value).run(Runnable) 또는 .call(Callable) 메서드를 사용하여 특정 값으로 바인딩된 스코프를 생성하고 실행합니다.
- 값 접근 (Accessing): 스코프 내에서 key.get() 또는 key.orElse(defaultValue) 등을 사용하여 바인딩된 값을 읽습니다.
Scoped Values 선언
// 예시 1: ScopedValue 인스턴스 선언 import java.lang.ScopedValue; public class UserContext { // 요청에 대한 사용자 ID를 저장할 ScopedValue public static final ScopedValue<String> REQUEST_USER_ID = ScopedValue.newInstance(); // 요청 ID를 저장할 ScopedValue public static final ScopedValue<Long> REQUEST_ID = ScopedValue.newInstance(); }선언 & 사용 & 자식 호출 스택에서 사용
import java.lang.ScopedValue; public class BasicExample { // ScopedValue 선언 private static final ScopedValue<String> USER_CONTEXT = ScopedValue.newInstance(); public static void main(String[] args) { // ScopedValue 바인딩 및 실행 ScopedValue.where(USER_CONTEXT, "user123") .run(() -> { System.out.println("처리 중: " + USER_CONTEXT.get()); processRequest(); }); } static void processRequest() { // 자식 호출 스택에서도 접근 가능 String user = USER_CONTEXT.get(); System.out.println("요청 처리 중인 사용자: " + user); } }여러 Scoped Values binding
import java.lang.ScopedValue; public class MultipleValuesExample { private static final ScopedValue<String> USER_ID = ScopedValue.newInstance(); private static final ScopedValue<String> TRANSACTION_ID = ScopedValue.newInstance(); private static final ScopedValue<Integer> REQUEST_COUNT = ScopedValue.newInstance(); public static void main(String[] args) { // 여러 ScopedValues를 동시에 바인딩 ScopedValue.where(USER_ID, "user-789") .where(TRANSACTION_ID, "tx-2024-001") .where(REQUEST_COUNT, 1) .run(() -> { System.out.println("사용자: " + USER_ID.get()); System.out.println("트랜잭션: " + TRANSACTION_ID.get()); System.out.println("요청 횟수: " + REQUEST_COUNT.get()); nestedOperation(); }); } static void nestedOperation() { System.out.println("중첩된 작업에서 접근: " + USER_ID.get()); } }중첩된 바인딩 (rebinding)
import java.lang.ScopedValue; public class RebindingExample { private static final ScopedValue<String> CONTEXT = ScopedValue.newInstance(); public static void main(String[] args) { ScopedValue.where(CONTEXT, "기본 컨텍스트") .run(() -> { System.out.println("외부 컨텍스트: " + CONTEXT.get()); runWithCustomContext(); System.out.println("외부 컨텍스트 복귀: " + CONTEXT.get()); }); } static void runWithCustomContext() { // 중첩된 범위에서 새로운 값 바인딩 ScopedValue.where(CONTEXT, "사용자 정의 컨텍스트") .run(() -> { System.out.println("중첩된 컨텍스트: " + CONTEXT.get()); deepOperation(); }); } static void deepOperation() { System.out.println("깊은 호출에서의 컨텍스트: " + CONTEXT.get()); } }구조화된 동시성과 Scoped Values 상속
import java.lang.ScopedValue; import java.util.concurrent.*; import java.util.function.Supplier; public class StructuredConcurrencyExample { private static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance(); public static void main(String[] args) throws Exception { ScopedValue.where(REQUEST_ID, "req-12345") .run(() -> { System.out.println("메인 스레드에서 요청 ID: " + REQUEST_ID.get()); try (var scope = new StructuredTaskScope.ShutdownOnFailure()) { // 자식 가상 스레드에서 ScopedValue 상속 Supplier<String> task1 = scope.fork(() -> { System.out.println("작업 1 - 스레드: " + Thread.currentThread()); System.out.println("작업 1에서 요청 ID: " + REQUEST_ID.get()); return "결과1"; }); Supplier<String> task2 = scope.fork(() -> { System.out.println("작업 2 - 스레드: " + Thread.currentThread()); System.out.println("작업 2에서 요청 ID: " + REQUEST_ID.get()); return "결과2"; }); scope.join().throwIfFailed(); System.out.println("작업 1 결과: " + task1.get()); System.out.println("작업 2 결과: " + task2.get()); } }); } }logging context 관리
import java.lang.ScopedValue; import java.time.LocalDateTime; import java.time.format.DateTimeFormatter; import java.util.function.Supplier; public class LoggingContextExample { // 로깅에 필요한 컨텍스트 정보 private static final ScopedValue<String> TRACE_ID = ScopedValue.newInstance(); private static final ScopedValue<String> USER_ID = ScopedValue.newInstance(); private static final ScopedValue<String> SERVICE_NAME = ScopedValue.newInstance(); // 로거 클래스 static class Logger { private static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS"); public static void info(String message) { log("INFO", message, null); } public static void error(String message, Throwable throwable) { log("ERROR", message, throwable); } private static void log(String level, String message, Throwable throwable) { String traceId = TRACE_ID.isBound() ? TRACE_ID.get() : "N/A"; String userId = USER_ID.isBound() ? USER_ID.get() : "anonymous"; String serviceName = SERVICE_NAME.isBound() ? SERVICE_NAME.get() : "unknown"; String timestamp = LocalDateTime.now().format(formatter); String logEntry = String.format("[%s] [%s] [%s] [%s] [%s] %s", timestamp, level, traceId, userId, serviceName, message); if (throwable != null) { logEntry += " - Exception: " + throwable.getMessage(); } System.out.println(logEntry); } } // 서비스 클래스 static class OrderService { public void processOrder(String orderId) { Logger.info("주문 처리 시작: " + orderId); try { validateOrder(orderId); chargePayment(orderId); shipOrder(orderId); Logger.info("주문 처리 완료: " + orderId); } catch (Exception e) { Logger.error("주문 처리 실패: " + orderId, e); throw e; } } private void validateOrder(String orderId) { Logger.info("주문 검증: " + orderId); if (orderId == null || orderId.isEmpty()) { throw new IllegalArgumentException("주문 ID가 비어있습니다"); } } private void chargePayment(String orderId) { Logger.info("결제 처리: " + orderId); // 외부 결제 서비스 호출 시뮬레이션 callExternalService(() -> "결제 성공: " + orderId); } private void shipOrder(String orderId) { Logger.info("배송 준비: " + orderId); } // 외부 서비스 호출 (보안 컨텍스트 재설정) private String callExternalService(Supplier<String> serviceCall) { // 외부 서비스 호출 시 보안 컨텍스트를 게스트로 변경 return ScopedValue.where(USER_ID, "external-service") .call(() -> { Logger.info("외부 서비스 호출 시작"); String result = serviceCall.get(); Logger.info("외부 서비스 호출 완료"); return result; }); } } public static void main(String[] args) { // 요청 컨텍스트 설정 ScopedValue.where(TRACE_ID, "trace-12345") .where(USER_ID, "user-67890") .where(SERVICE_NAME, "order-service") .run(() -> { Logger.info("요청 수신"); OrderService orderService = new OrderService(); orderService.processOrder("order-001"); // 중첩된 컨텍스트에서 다른 작업 실행 ScopedValue.where(SERVICE_NAME, "inventory-service") .run(() -> { Logger.info("재고 서비스 작업 시작"); // 재고 관련 작업 Logger.info("재고 확인 완료"); }); Logger.info("요청 처리 완료"); }); } }Reference
Java 25 Scoped and Stable Values | Java 25 New Features
https://www.youtube.com/watch?v=85ujZJ1ZaP4
'Java > Java25' 카테고리의 다른 글
Java 25 Performance Improvement (0) 2025.12.21 JEP 513 Flexible Constructor Bodies (0) 2025.12.20 JEP 512 Compact Source File & Instance Main Method (0) 2025.12.20 Java 22 신규 및 변경 기능 (0) 2025.12.14 Java 25 new features (0) 2025.10.27