3 minute read

ScopedValue — 등장 배경부터 철학까지

ScopedValue란 무엇인가

JDK 공식 문서에서는 ScopedValue를 다음과 같이 정의합니다.

“A scoped value is a container object that allows a data value to be safely and efficiently shared by a method with its direct and indirect callees within the same thread, and with child threads, without resorting to method parameters.”

— JEP 506, OpenJDK

즉, ScopedValue는 한 메서드가 자신이 호출하는 모든 하위 메서드들과 자식 스레드에게 데이터를 안전하고 효율적으로 공유할 수 있게 해주는 컨테이너 객체입니다. 이 과정에서 파라미터를 통해 값을 일일이 넘길 필요가 없습니다. 쉽게 말해, “명시적인 인자 전달 없이 호출 체인 전체와 그 하위 스레드에서 동일한 불변 데이터를 안전하게 읽을 수 있게 하는 메커니즘” 이라고 이해할 수 있습니다.

왜 ScopedValue가 JDK에 새로 도입되었는가

ScopedValue는 ThreadLocal의 구조적 한계를 근본적으로 해결하기 위해 설계된 새로운 컨텍스트 전달 메커니즘입니다.

특히, 가상 스레드와 구조적 동시성이 도입된 현대 자바 환경에서

ThreadLocal의 한계가 명확히 드러나면서 그 대안으로 등장했습니다.

ThreadLocal의 문제설명

수명 불명확

set()한 값을 remove()하지 않으면, 스레드 풀 재사용 시 이전 요청의 값이 그대로 남아 다음 요청에 영향을 미치거나 메모리 누수, 데이터 오염이 발생할 수 있습니다.

비동기 전파 불가

부모 스레드에 저장된 ThreadLocal 값은 자식 태스크나 가상 스레드로 전파되지 않습니다. 따라서 비동기 처리나 병렬 실행 시 컨텍스트가 단절되어, 요청 단위의 데이터를 일관되게 유지하기 어렵습니다.

가변성 문제

ThreadLocal은 어디서든 set()으로 값을 변경할 수 있기 때문에 프로그램의 상태 추적이 어렵고, 예측하기 힘든 동시성 버그가 발생하기 쉽습니다. 특히 여러 컴포넌트가 동일한 키를 사용하는 경우에는 디버깅이 거의 불가능해집니다.

비용 문제

가상 스레드 환경처럼 수천, 수만 개의 스레드가 동시에 실행되는 상황에서는 ThreadLocal의 조회 및 저장 자체가 성능 병목으로 작용할 수 있습니다. 컨텍스트를 스레드 단위로 보관하는 구조가 근본적으로 비효율적인 셈입니다.

이처럼 ThreadLocal은 단일 요청·단일 스레드 환경에서는 유용했지만, 비동기 및 병렬 실행이 일반화된 오늘날의 서버 환경에서는 안전하지도, 효율적이지도 않은 컨텍스트 전달 수단이 되었습니다. ScopedValue는 이러한 문제를 해결하기 위한 불변 기반의 스코프 컨텍스트 모델입니다.

왜 ScopedValue가 중요한가

1. 가상 스레드

가상 스레드에서는 스레드 재사용이 빈번하게 일어나므로, ThreadLocal 값이 이전 요청에서 남아 컨텍스트 오염을 일으킬 수 있습니다.

ScopedValue는 스레드가 아니라 실행 스코프에 묶여 동작하므로, 가상 스레드 환경에서 완벽히 격리된 컨텍스트를 제공합니다.

2. 구조적 동시성

부모 태스크가 여러 자식 태스크를 병렬로 실행할 때,부모의 컨텍스트(requestId, traceId 등)를 자식 태스크와 공유해야 하는 경우가 많습니다. ScopedValue는 이러한 구조적 동시성 환경에서 부모 바인딩이 자식으로 안전하게 전파되도록 설계되었습니다.

3. 현대 서버 개발의 컨텍스트 전달

HTTP 요청 단위로 requestId, userId, traceId 같은 메타데이터를 전달해야 합니다. ThreadLocal은 수명과 동시성 문제로 이러한 요구를 만족시키기 어렵습니다. ScopedValue는 요청 단위 스코프 내에서 불변 컨텍스트를 안전하게 공유할 수 있도록 합니다.

Spring 웹 백엔드 환경에서의 실제 예시

1. ScopedValue 정의

// RequestContext.java
public class RequestContext {
    public static final ScopedValue<String> REQUEST_ID = ScopedValue.newInstance();
}

2. HTTP 요청 필터에서 바인딩

@Component
public class RequestIdFilter implements Filter {

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {

        String requestId = UUID.randomUUID().toString();

        ScopedValue.where(RequestContext.REQUEST_ID, requestId).run(() -> {
            try {
                chain.doFilter(req, res);
            } catch (IOException | ServletException e) {
                throw new RuntimeException(e);
            }
        });
    }
}

이 필터는 요청 시작 시 새로운 requestId를 생성하여 ScopedValue에 바인딩하고, 요청 스코프가 끝나면 자동으로 해제되도록 보장합니다.

3. 서비스 계층에서 사용

@Service
public class OrderService {

    public void processOrder(String orderId) {
        System.out.println("Processing order " + orderId + " with requestId: "
                + RequestContext.REQUEST_ID.get());
    }
}

서비스 계층에서는 인자 전달 없이 RequestContext.REQUEST_ID.get()으로 현재 요청의 requestId를 읽을 수 있습니다.

재바인딩(Rebinding)의 의미와 차이점

ScopedValue는 바인딩된 값을 set()으로 변경할 수 없지만, 더 안쪽 스코프에서 새로운 값으로 재바인딩 할 수 있습니다.

이것은 기존 값을 덮어쓰는 것이 아니라, 바인딩 스택에 새로운 레이어를 추가하여 기존 값을 가리는 구조입니다.

ScopedValue.where(USER, "admin").run(() -> {
    System.out.println(USER.get()); // admin

    ScopedValue.where(USER, "guest").run(() -> {
        System.out.println(USER.get()); // guest (새 레이어)
    });

    System.out.println(USER.get()); // admin (복귀)
});

스코프 종료 시 스택이 자동으로 pop 되며,이전 바인딩으로 안전하게 복귀합니다. 즉, 값은 불변이지만 컨텍스트는 계층적으로 변화합니다.

이는 ThreadLocal의 set() 대비 훨씬 안전한 방식입니다.

정리

ScopedValue는 기존 ThreadLocal의 구조적 한계를 극복하기 위해 도입된, 불변 컨텍스트 전달용 스코프 기반 저장소입니다.

  • 상위 스코프에서 한 번 바인딩된 값은 하위 호출 체인과 자식 스레드에서 안전하게 읽기 전용으로 공유됩니다.
  • 스코프(블록)가 종료되면 값은 자동 해제되어 메모리 누수 걱정이 없습니다.
  • 하위 스코프에서 같은 키를 새 값으로 재바인딩할 수는 있지만, 덮어쓰기가 아니라 새 레이어가 쌓이는 방식이므로 본래의 불변성은 훼손되지 않습니다.

가상 스레드(Virtual Thread) 환경처럼 대량의 스레드 재사용이 일어나는 모던 자바 생태계에서, ScopedValue는 ThreadLocal의 가변성, 수명 관리, 비동기 전파 문제를 완벽하게 해결한 훌륭한 대안입니다.

참고 자료

Updated:

Comments