ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Spring] 스프링 핵심 원리 - ThreadLocal
    Study/Spring 2025. 4. 19. 23:15

    일반 변수와 ThreadLocal 변수 비교

    1) 일반 변수 필드

    • 여러 스레드가 같은 인스턴스의 필드(멤버 변수)에 접근하면 처음 스레드가 보관한 데이터가 사라질 수 있음

     

    예시)

    • thread-A가 userA라는 값을 저장하고, thread-B가 userB라는 값을 저장하면 직전에 thread-A가 저장한 userA 값은 덮어씌워짐

     

    2) ThreadLocal로 선언된 필드

    • ThreadLocal은 해당 스레드만 접근할 수 있는 특별한 저장소를 말함
    • 즉, ThreadLocal을 사용하면 각 스레드마다 별도의 내부 저장소를 제공함. 따라서 같은 인스턴스의 스레드 로컬 필드에 접근해도 문제 없음
    • 자바는 언어 차원에서 스레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공

     

    예시)

    • thread-A가 userA라는 값을 저장하면 스레드 로컬은 thread-A 전용 보관소에 데이터를 보관함
    • thread-B가 userB라는 값을 저장하면 스레드 로컬은 thread-B 전용 보관소에 데이터를 보관함

    • 스레드 로컬을 통해 데이터를 조회할 때도 thread-A가 조회하면 스레드 로컬은 thread-A 전용 보관소에서 userA 데이터를 반환해주고, thread-B가 조회하면 thread-B 전용 보관소에서 userB 데이터를 반환해줌

     

    ThreadLocal 주의사항

    • 스레드 로컬의 값을 사용 후 제거하지 않으면 WAS(톰캣)처럼 스레드 풀을 사용해 스레드를 재사용하는 경우 문제가 발생할 수 있음
      • 다음 요청이 이전 요청의 데이터를 보게되는 문제

     

    상황

    더보기

    사용자A 저장 요청

    1. 사용자A가 저장 HTTP를 요청함
    2. WAS는 스레드 풀에서 스레드를 하나 조회함
    3. 해당 요청 처리를 위해 스레드 thread-A가 할당됨
    4. thread-A는 사용자A의 데이터를 스레드 로컬에 저장함
    5. 스레드 로컬의 thread-A 전용 보관소에 사용자A 데이터를 보관함

     

    사용자A 저장 요청 완료

    1. 사용자A의 HTTP 응답이 반환되어 끝남
    2. WAS는 사용이 끝난 thread-A를 스레드 풀에 반환함 (스레드 풀을 통해 스레드를 재사용함)
    3. thread-A는 스레드 풀에 아직 살아있으므로, 스레드 로컬의 thread-A 전용 보관소에 사용자A 데이터도 아직 저장되어 있음

     

    사용자B 조회 요청

    1. 사용자B가 조회를 위한 새로운 HTTP 요청함
    2. WAS 스레드 풀에서 스레드를 하나 조회함
    3. 스레드 thread-A가 할당됨 (물론 다른 스레드가 할당될 수도 있음)
    4. 이번에는 조회 요청이므로, thread-A는 스레드 로컬에서 데이터를 조회함
    5. 스레드 로컬은 thread-A 전용 보관소에 있는 사용자A 값을 반환함
    6. 결과적으로 사용자A 데이터가 반환됨
    7. 사용자B는 사용자A의 정보를 조회하게 됨

     

    즉, 사용자B가 사용자A의 데이터를 보게되는 상황이 발생하게 됨

     

    방안

    • 요청이 끝날 때 스레드 로컬의 값을 ThreadLocal.remove()를 통해 꼭 제거해야 함

     

    추가) 스레드 로컬 동작 이해

    • ThreadLocal은 스레드마다 독립된 값을 저장할 수 있게 해주는 클래스
      • 즉, 각 스레드가 자신만의 변수 복사본을 가지도록 해주는 메커니즘
    • 멀티스레드 환경에서 공유되지 않는 상태를 유지해야할 때 유용
      • ex) 로그 트레이싱, 트랜잭션 ID 저장, 로그인 사용자 정보 유지 등

     

    예제)

    더보기
    public class Example {
        private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();
    
        public static void main(String[] args) {
            threadLocal.set("main-thread-value");
    
            new Thread(() -> {
                threadLocal.set("child-thread-value");
                System.out.println(threadLocal.get()); // child-thread-value
            }).start();
    
            System.out.println(threadLocal.get()); // main-thread-value
        }
    }
    • 각 스레드가 threadLocal.set()을 호출하면 해당 스레드만 접근 가능한 저장소에 값이 저장됨
    • 다른 스레드에서는 해당 값이 보이지 않음

     

    내부 동작 원리

    • ThreadLocal은 값을 직접 저장하지 않음
    • 각 Thread 인스턴스 내부에 ThreadLocalMap이라는 Map이 존재
    • 해당 Map의 key는 ThreadLocal 인스턴스 자신이고, value는 해당 값이 됨
    Thread A
     └── ThreadLocalMap
          └── [ThreadLocal 인스턴스] → "A의 값"
    
    Thread B
     └── ThreadLocalMap
          └── [ThreadLocal 인스턴스] → "B의 값"

     

    코드 살펴보기

    1) ThreadLocal.set(T value) 호출했을 때

    더보기
    // ThreadLocal class
    
    public void set(T value) {
        set(Thread.currentThread(), value);
        if (TRACE_VTHREAD_LOCALS) {
            dumpStackIfVirtualThread();
        }
    }
    
    private void set(Thread t, T value) {
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    • 현재 실행중인 Thread.currentThread()에서 ThreadLocalMap을 가져옴
    • 해당 ThreadLocal 인스턴스를 key로 설정하여 Map에 값 저장

     

    2) ThreadLocal.get() 호출했을 때

    더보기
    public T get() {
        return get(Thread.currentThread());
    }
    
    private T get(Thread t) {
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T) e.value;
                return result;
            }
        }
        return setInitialValue(t);
    }
    • 현재 스레드의 ThreadLocalMap에서 현재 ThreadLocal 인스턴스를 key로 가지는 값 반환

     

    참고

    댓글

Designed by Tistory.