ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JEP 506 Scoped Values
    Java/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 (생명 주기)

    1. 인스턴스 생성: ScopedValue.newInstance()를 사용하여 정적 필드로 선언합니다. 이 인스턴스가 "키" 역할을 합니다.
    2. 값 바인딩 (Binding): ScopedValue.where(key, value).run(Runnable) 또는 .call(Callable) 메서드를 사용하여 특정 값으로 바인딩된 스코프를 생성하고 실행합니다.
    3. 값 접근 (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

    댓글

Designed by Tistory.