본문 바로가기

Development/Spring

[Spring] ThreadLocal에 대해 알아보자 + SecurityContextHolder, RequestContextHolder

Java Spring에서는 Thread 마다 고유한 데이터를 가지기 위해 ThreadLocal 클래스를 사용합니다. 이 글에서는 ThreadLocal이 무엇이며 어디서 활용되고 있는지에 대해 알아보겠습니다.

Thread

스레드(Thread)는 프로세스 내에서 실행되는 가장 작은 단위의 작업이다. 하나의 프로세스는 여러 스레드를 가질 수 있으며, 각 스레드는 독립적으로 실행된다. 스레드는 동일한 메모리 공간을 공유하며, 서로 다른 스레드가 동시에 실행될 수 있기 때문에 멀티스레딩을 통해 병렬 처리가 가능해진다.

 

Spring 애플리케이션, 특히 웹 애플리케이션에서는 일반적으로 요청당 스레드(Thread-Per-Request) 모델을 사용한다.

이는 클라이언트의 HTTP 요청마다 별도의 스레드를 생성하여 그 요청을 처리하는 방식이다. 각 요청은 독립된 스레드에서 처리되므로, 동시에 여러 요청을 병렬로 처리할 수 있다.

ThreadLocal

ThreadLocal은 각 스레드가 고유한 값을 가질 수 있도록 하는 메커니즘이다. 이는 스레드마다 독립적인 변수를 저장하고 접근할 수 있게 한다.

웹 서버에서 각 요청은 보통 독립된 스레드에서 처리되기 때문에, 요청 간에 공유되지 않고 독립적인 데이터를 유지할 필요가 있다. 예를 들어, 사용자 인증 정보의 경우 ThreadLocal을 사용하면 각 스레드가 요청받은 사용자의 인증 데이터를 안전하게 보관하고 사용할 수 있다.

public class ThreadLocal<T>

ThreadLocal이 구현된 코드를 살펴보자.

 

일단 클래스 상단에 javadoc을 읽어보겠다.

/**
 * This class provides thread-local variables.  These variables differ from
 * their normal counterparts in that each thread that accesses one (via its
 * {@code get} or {@code set} method) has its own, independently initialized
 * copy of the variable.  {@code ThreadLocal} instances are typically private
 * static fields in classes that wish to associate state with a thread (e.g.,
 * a user ID or Transaction ID).
 *

해석해 보자면, ThreadLocal 클래스는 이름 그대로 thread-local 변수들을 제공한다.

이 변수들은 각 스레드가 (get 또는 set 메서드를 통해) 접근할 때마다 독립적으로 초기화된 자신의 복제본을 가지며, 일반 변수와 다르다고 기술되어 있다.

 

여기서 일반 변수에 대해 생각해 보자면 기본적으로 스레드는 공통된 데이터를 공유한다.

하나의 프로세스 내부 구조, 이미지 출처: https://media.geeksforgeeks.org/wp-content/uploads/20190522155604/Untitled-Diagram-361.png

 

위 그림처럼 하나의 프로세스에 여러 스레드가 존재할 수 있으며, Code, Data 등 여러 데이터를 공유한다. 따라서 일반 변수는 스레드간에 공유된 데이터를 가진다.

하지만 ThreadLocal은 쓰레드 별로 다른 데이터를 가질 수 있다는 것을 의미한다.

 

아래는 javadoc에 있는 예제 클래스이다.

import java.util.concurrent.atomic.AtomicInteger;
 *
 * public class ThreadId {
 *     // Atomic integer containing the next thread ID to be assigned
 *     private static final AtomicInteger nextId = new AtomicInteger(0);
 *
 *     // Thread local variable containing each thread's ID
 *     private static final ThreadLocal<Integer> threadId =
 *         new ThreadLocal<Integer>() {
 *             @Override protected Integer initialValue() {
 *                 return nextId.getAndIncrement();
 *         }
 *     };
 *
 *     // Returns the current thread's unique ID, assigning it if necessary
 *     public static int get() {
 *         return threadId.get();
 *     }
 * }

 

위 예제 코드는 ThreadLocal 변수를 통해 스레드 별로 고유의 데이터를 가질 수 있다는 것을 보여준다.

 

ThreadId 클래스는 각 스레드에 고유한 ID를 부여하는 클래스이다.

Id는 AtomicInteger 타입인데, 이 타입은 원자적으로 값을 증가시켜서 여러 스레드에서 동시에 접근해도 안전하게 값을 증가시킬 수 있다(thread-safe).

 

threadId 필드는 ThreadLocal 변수를 선언한다.

initialValue() 메서드를 오버라이드하여 각 스레드에 대해 최초로 get() 메서드가 호출될 때 초기 값을 설정한다.

(이 메서드는 밑에서 다시 설명하겠다.)

 

각 스레드는 해당 스레드가 살아있는 동안 thread-local 변수의 사본에 대한 암묵적인(implicit) 참조를 유지하며, ThreadLocal 인스턴스가 접근 가능한 한 이 참조를 유지한다.

스레드가 종료된 후에는, 해당 thread-local 인스턴스의 모든 사본은 (다른 참조가 존재하지 않는 한) 가비지 컬렉션의 대상이 된다.

ThreadLocal은 어떻게 스레드마다 구분되는가? -> Hash

ThreadLocal 객체는 각 스레드에 대해 별도의 값을 저장하고 관리하기 위해 각 스레드가 고유한 해시 코드를 사용하여 해당 값을 찾고 저장할 수 있는 해시 테이블(ThreadLocalMap)을 유지한다.

public class ThreadLocal<T> {
    /**
     * ThreadLocals rely on per-thread linear-probe hash maps attached
     * to each thread (Thread.threadLocals and
     * inheritableThreadLocals).  The ThreadLocal objects act as keys,
     * searched via threadLocalHashCode.  This is a custom hash code
     * (useful only within ThreadLocalMaps) that eliminates collisions
     * in the common case where consecutively constructed ThreadLocals
     * are used by the same threads, while remaining well-behaved in
     * less common cases.
     */
    private final int threadLocalHashCode = nextHashCode();

    /**
     * The next hash code to be given out. Updated atomically. Starts at
     * zero.
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

 

threadlocalHashCode

ThreadLocals는 각 스레드에 연결된 선형 탐사 (linear-probe) 해시 맵에 의존한다고 기술되어 있다.

선형 탐사 해시맵에 짚고 가자면, 해시 충돌 발생 시 선형 탐사는 해시 테이블의 다음 인덱스(즉, 현재 인덱스 + 1)를 확인한다. 만약 그 인덱스도 사용 중이라면, 그다음 인덱스를 확인하는 식으로 빈 인덱스를 찾을 때까지 계속 진행한다.
빈 인덱스를 찾으면 그곳에 데이터를 저장하여 해시 충돌을 해결하는 방식이다.


ThreadLocal 객체는 키로 작동하며, threadLocalHashCode를 통해 검색된다.
이는 ThreadLocalMaps 내에서만 유용한 커스텀 해시 코드로, 동일한 스레드에서 연속적으로 생성된 ThreadLocals가 사용되는 일반적인 경우 충돌을 제거하는 동시에, 덜 일반적인 경우에서도 적절하게 동작한다고 기술되어 있다.

 

nextHashCode

다음 해시 코드를 생성하기 위해 사용되는 원자적 정수(Atomic Integer)이다. AtomicInteger를 사용하여 동시성 문제없이 안전하게 값을 증가시킬 수 있다.

샤로운 해시 코드를 생성할 때마다 이 필드의 값을 증가시키고, 새로운 threadLocalHashCode 값을 할당한다.

 

HASH_INCREMENT, nextHashCode()

HASH_INCREMENT는 연속적으로 생성되는 해시 코드 사이의 차이값을 나타낸다. 이 값은 해시 테이블의 크기가 2의 거듭제곱인 경우 해시 값이 고르게 분포되도록 돕는다. 

nextHashCode()  메서드는 nextHashCode 필드의 값을 AtomicInteger의 getAndAdd 메서드를 사용하여 HASH_INCREMENT만큼 증가시킨 후, 증가된 값으로 설정한다. 이를 통해 각 ThreadLocal 객체는 고유한 해시 코드를 가지게 된다.

ThreadLocal은 어디에 데이터를 저장하는가? -> ThreadLocalMaps

ThreadLocal은 스레드가 접근할 때마다 해당 스레드만의 데이터를 반환해야 한다. 이를 위해 각 스레드가 자신만의 데이터를 저장할 수 있는 자료구조가 ThreadLocalMap이다.

 

ThreadLocalMap 클래스는 ThreadLocal 변수의 값을 각 스레드별로 저장하고 관리하기 위한 해시 맵이다.

이 클래스에서는 내부적으로 WeakReference를 상속한 Entry 클래스를 가지는데, ThreadLocalMap에서 WeakReference를 사용하는 이유는 ThreadLocal 객체가 더 이상 참조되지 않으면 해당 엔트리를 가비지 컬렉션 하기 위해서이다.
아래 코드는 WeakReference를 상속한 Entry 클래스를 구현한 코드이다.

Entry 클래스는 ThreadLocal 필드와 value 필드를 가진다.

  static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

 

ThreadLocalMaps의 생성자는 두 가지가 있다.

우선, 처음 ThreadLocalMap이 생성되었을 때 호출하는 생성자를 살펴보겠다.

/**
 * Construct a new map initially containing (firstKey, firstValue).
 * ThreadLocalMaps are constructed lazily, so we only create
 * one when we have at least one entry to put in it.
 */
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

Entry [] 타입의 table을 초기화한다.

그 후 ThreadLocal의 threadLocalHashCode를 활용하여 해시 값을 계산한다. 그 후 계산된 해시 값을 인덱스로 사용하여 테이블에 새로운 Entry 객체를 추가한다.

 

setThreshhold를 통해 임계값을 설정한다. 이때 임계값은 해시 맵이 재해시(rehash)를 수행하기 전에 허용할 수 있는 최대 엔트리 수를 결정한다. (이는 set() 함수에서 결정한다.)
또한 지연 초기화 방식을 사용하는데 이는 불필요한 메모리 사용을 줄이고, 성능을 최적화하는 데 도움을 준다. Map은 처음으로 ThreadLocal 변수가 설정될 때까지 생성되지 않으므로, ThreadLocal 변수가 실제로 필요하지 않을 경우에는 맵이 생성되지 않는다.

 

아래 코드는 ThreadLocalMaps의 두 번째 생성자이다.

InheritableThreadLocal이 ThreadLocalMap을 생성할 때 사용된다. (InheritableThreadLocal는 아래에서 다룬다.)

이 생성자는 ThreadLocal의 값이 상속될 때 사용되며, 부모 스레드의 값들을 현재 스레드에 복사하여 초기화하는 역할을 한다. 

private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (Entry e : parentTable) {
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }
        }

각 엔트리에서 ThreadLocal 값을 가져온다. ThreadLocal 값이 null이 아니라면 childValue 메서드를 호출하여 부모 값으로부터 자식 값을 생성한다.

해시 값을 계산하여 자식 테이블의 인덱스를 결정하고 새로운 엔트리를 삽입한다. 

이 때 해시 충돌이 발생하면 다음 인덱스를 찾아가며 적절한 위치를 찾는다.

 

이러한 과정을 통해 현재 스레드에 부모 스레드 값을 복사한다.

ThreadLocal은 어떻게 초기화되는가 -> initialValue()

    /**
     * Returns the current thread's "initial value" for this
     * thread-local variable.  This method will be invoked the first
     * time a thread accesses the variable with the {@link #get}
     * method, unless the thread previously invoked the {@link #set}
     * method, in which case the {@code initialValue} method will not
     * be invoked for the thread.  Normally, this method is invoked at
     * most once per thread, but it may be invoked again in case of
     * subsequent invocations of {@link #remove} followed by {@link #get}.
     *
     * <p>This implementation simply returns {@code null}; if the
     * programmer desires thread-local variables to have an initial
     * value other than {@code null}, {@code ThreadLocal} must be
     * subclassed, and this method overridden.  Typically, an
     * anonymous inner class will be used.
     *
     * @return the initial value for this thread-local
     */
    protected T initialValue() {
        return null;
    }

initialValue()는 현재 스레드의 ThreadLocal 변수에 대한 초기값을 반환한다.

이 메서드는 스레드가 해당 변수에 처음 접근할 때, 즉 get 메서드를 호출할 때 처음으로 호출된다. 단, 스레드가 이미 set 메서드를 호출하여 값을 설정한 경우에는 이 메서드가 호출되지 않는다. 그렇기 때문에 일반적으로 이 메서드는 각 스레드당 최대 한 번 호출된다. 그러나 이후에 remove 메서드를 호출한 후 get 메서드를 다시 호출하는 경우에는 다시 호출될 수 있다.
이 메서드의 기본 구현은 단순히 null을 반환한다. 즉, ThreadLocal 변수의 초기값으로 null을 사용하도록 되어 있다.

 

여기서 get() 메서드가 어떻게 구현되어 있는지 보면 ThreadLocalMap이 null 인 경우에 setInitialValue()를 호출한다.

public T get() {
    Thread t = Thread.currentThread();
    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();
}

 setInitialValue() 함수는 아래와 같이 구현되어 있다.

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

위 메서드의 첫 줄에서 initialValue를 호출한다.

결과적으로 initialValue() 메서드는 처음 get() 함수가 호출되었을 때 setInitialValue()를 호출하고 이후 initialValue를 호출하는 순서를 알 수 있다.

이후 현재 Thread와 그 Thread의 ThreadLocalMap을 얻고 Map을 set 하거나 createMap을 통해 ThreadLocalMap을 생성한다.

ThreadLocalMap에 어떤 과정으로 데이터를 저장하는가? -> set() 

ThreadLocal 클래스의 set 함수는 현재 스레드의 ThreadLocal 변수에 값을 저장하는 역할을 한다.

 

ThreadLocal 클래스의 set은 아래와 같이 구현되어 있다.

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        map.set(this, value);
    } else {
        createMap(t, value);
    }
}

현재 Thread 객체를 가져오고, 그 Thread의 ThreadLocalMap을 가져온다.

Map이 null이면 새로 만들고, 있으면 map.set을 호출하여 저장할 value를 넘긴다.

 

ThreadLocalMap의 set의 구현은 아래와 같이 구현되어 있다.

/**
 * Set the value associated with key.
 *
 * @param key the thread local object
 * @param value the value to be set
 */
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }

        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

현재 ThreadLocal의 table을 순회하면서 해당 키가 이미 존재하면 값을 업데이트한다. (if (k == key) {...})
Stale Entry (k == null)를 발견하면 replaceStaleEntry 메서드를 호출하여 새 엔트리로 교체한다.

여기서 Stale Entry란, Garbage Collection에 의해 더 이상 유효하지 않은 thread-local 값을 key로 갖는 Entry이다.

 

위 모두 해당하지 않으면 새로운 엔트리를 추가한다.
엔트리를 추가한 후 크기를 증가시키고 일부 슬롯을 청소해야 하거나 사이즈가 임계값(threshold) 보다 크면 테이블을 재해시(rehash)한다.

ThreshLocal은 어떻게 값을 찾는가? -> get()

ThreadLocal 클래스에서 value 데이터를 찾는 과정은 주로 get 메서드와 ThreadLocalMap 내부의 getEntry 메서드를 통해 이루어진다.

public T get() {
    Thread t = Thread.currentThread();
    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();
}

위 코드는 ThreadLocal 클래스에 구현되어 있는 get() 함수이다.

현재 Thread를 가지고 ThreadLocalMap을 가져온다.

map이 null이 아니라면 현재 ThreadLocal key에 대한 Entry를 불러온다. 

 

Entry를 가져오는 함수는 ThreadLocalMap에 getEntry 함수이다.

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

현재 ThreadLocal의 threadLocalHashCode를 사용하여 인덱스를 계산하고, table에서 인덱스에 해당하는 Entry를 가져온다.

해당 ThreadLocal의 Entry값이 맞다면 데이터를 return 하고 아니면 getEntryAfterMiss를 호출한다.

/**
 * Version of getEntry method for use when key is not found in
 * its direct hash slot.
 *
 * @param  key the thread local object
 * @param  i the table index for key's hash code
 * @param  e the entry at table[i]
 * @return the entry associated with key, or null if no such
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

getEntryAfterMiss는 키를 해시 슬롯에서 찾지 못했을 때 호출된다.

만약 key가 null 인 경우, expungeStaleEntry()를 호출하는데 이 메서드는 특정 슬롯에 위치한 스테일(만료된)한 엔트리를 정리하고, 해당 슬롯 이후에 나오는 모든 null 슬롯까지의 모든 엔트리를 재해싱(rehashing)하여 해시 충돌을 해결하는 기능을 한다.

값이 null이 아닐 때까지 다음 인덱스로 이동하면서, key에 일치하는 Entry를 검색하고 찾지 못하면 null을 return 한다.

 

위 순서를 정리하자면 

  1. 현재 Thread에 ThreadLocalMap을 가져온다.
  2. 현재 ThreadLocal의 HashCode를 사용하여 인덱스를 계산하고 ThreadLocalMap에서 해당 인덱스에 데이터를 가져온다.
  3. 만약 데이터가 null이라면 expungeStaleEntry를 호출한다.

ThreadLocal 요약

이미지 출처: https://oopy.lazyrockets.com/api/v2/notion/image?src=https%3A%2F%2Fs3-us-west-2.amazonaws.com%2Fsecure.notion-static.com%2F9b580c69-ee30-4c68-b8fd-a63bc6f8013f%2FUntitled.png&blockId=8f28da93-b16e-4335-84c7-d84e9bd8fd8d
IntelliJ Debug 모드로 살펴 본 Thread 내부 구조

  • 각 Thread마다 ThreadLocalMap을 가진다. 이때 key는 각 ThreadLocal의 threadLocalHashCode를  & (INITIAL_CAPACITY - 1) 연산한 값이다. 
  • ThreadLocalMap은 key, value를 가지는 Entry라는 객체로 사용한다.
  • ThreadLocal 객체의 다음 인덱스가 계산된다.
  • Entry는 WeakReference를 상속하여 ThreadLocal 객체가 더 이상 참조되지 않으면 해당 엔트리를 가비지 컬렉션 할 수 있도록 한다.
  • 만약 해시 충돌이 발생하면 선형 탐사 방식을 통해 새로운 인덱스를 계산한다.

어디서 사용되는가?

SecurityContextHolder

Spring Security에서는 SecurityContext를 사용하여 인증 및 권한 정보를 관리한다. 이 SecurityContext는 SecurityContextHolder에 저장이 된다. 이 때 정보를 저장하는 전략(strategy)은 여러 가지가 있는데, 기본적으로 ThreadLocal을 통해 저장이 된다. 따라서 각 스레드마다 보안 정보를 유지할 수 있다. 

 

SecurityContextHolder

public class SecurityContextHolder {

	public static final String MODE_THREADLOCAL = "MODE_THREADLOCAL";

	public static final String MODE_INHERITABLETHREADLOCAL = "MODE_INHERITABLETHREADLOCAL";
		
    // ... 중략

	private static void initializeStrategy() {
		if (MODE_PRE_INITIALIZED.equals(strategyName)) {
			Assert.state(strategy != null, "When using " + MODE_PRE_INITIALIZED
					+ ", setContextHolderStrategy must be called with the fully constructed strategy");
			return;
		}
		if (!StringUtils.hasText(strategyName)) {
			// Set default
			strategyName = MODE_THREADLOCAL;
		}
		if (strategyName.equals(MODE_THREADLOCAL)) {
			strategy = new ThreadLocalSecurityContextHolderStrategy();
			return;
		}

위 코드에서 strategyName = MODE_THREADLOCAL;을 보면 기본 전략으로 MODE_THREADLOCAL을 사용한다.

그리고 ThreadLocalSecurityContextHolderStrategy 객체를 생성하는데, 이 객체는 아래와 같이 구현되어 있다.

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

    private static final ThreadLocal<SecurityContext> contextHolder = new ThreadLocal<>();

    @Override
    public void clearContext() {
       contextHolder.remove();
    }

    @Override
    public SecurityContext getContext() {
       SecurityContext ctx = contextHolder.get();
       if (ctx == null) {
          ctx = createEmptyContext();
          contextHolder.set(ctx);
       }
       return ctx;
    }

    @Override
    public void setContext(SecurityContext context) {
       Assert.notNull(context, "Only non-null SecurityContext instances are permitted");
       contextHolder.set(context);
    }

    @Override
    public SecurityContext createEmptyContext() {
       return new SecurityContextImpl();
    }

}

맨 위에 ThreadLocal <SecurityContext> contextHolder = new ThreadLocal<>(); 코드를 보고 SecurityContextHolder의 strategy 타입은 ThreadLocal를 사용한다는 것을 알 수 있다. get, set 함수도 ThreadLocal에 함수를  그대로 사용한다.

 

SecurityContext를 얻기 위해 사용되는 아래 코드는 이런 과정을 거친다.

SecurityContext context = SecurityContextHolder.getContext();

 

contextHolder는 ThreadLocal<SecurityContext> 타입이므로, ThreadLocal에 get 함수를 호출한다.

현재 Thread의 ThreadLocalMap을 얻어온다.

그 후 SecurityContext를 Key로 하여 인덱스를 계산한 Entry를 가져와야 한다.

만약 가져온 Entry가 null이 아니며, key(SecurityContext)를 참조하고 있다면 이 Entry를 리턴하고, 아니면 getEntryAfterMiss를 호출한다.

 

참고로 refersTo()는 레퍼런스 객체가 특정 객체를 참조하는지 여부를 테스트하는 메서드이다.

ThreadLocalMap은 WeakReference을 상속한 Entry 객체를 저장하기 때문에, WeakReference 객체가 특정 객체를 참조하는지 테스트하는 것으로 이해할 수 있다. 

 

return 한 Entry는 아래와 같은 정보를 가지고 있다.

다시 ThreadLocal의 get 함수로 돌아와서, Entry의 value를 리턴하면 SecurityContext 객체가 리턴된다.

RequestContextHolder

RequestContextHolder는 스레드에 바인딩된 RequestAttributes 객체 형태로 웹 요청을 노출하기 위한 Holder 클래스이다.

이 클래스는 주로 웹 애플리케이션에서 현재 스레드에 관련된 요청 정보를 저장하고 접근하는 데 사용된다. 

ServletRequestAttributes requestAttributes =
        (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
HttpServletRequest servletRequest = requestAttributes.getRequest();
servletRequest.getHeader("my-header");
servletRequest.getCookies();

 

 

위 코드는 HttpServletRequest 객체를 읽어 현재 ServletRequest의 대한 정보를 얻어오는 간단한 코드이다.

이 클래스 또한 ThreadLocal를 사용하는데, 구현된 코드를 살펴보자.

public abstract class RequestContextHolder {
    private static final boolean jsfPresent = ClassUtils.isPresent("javax.faces.context.FacesContext", RequestContextHolder.class.getClassLoader());
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
    private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");

    public RequestContextHolder() {
    }

    public static void resetRequestAttributes() {
        requestAttributesHolder.remove();
        inheritableRequestAttributesHolder.remove();
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes) {
        setRequestAttributes(attributes, false);
    }

    public static void setRequestAttributes(@Nullable RequestAttributes attributes, boolean inheritable) {
        if (attributes == null) {
            resetRequestAttributes();
        } else if (inheritable) {
            inheritableRequestAttributesHolder.set(attributes);
            requestAttributesHolder.remove();
        } else {
            requestAttributesHolder.set(attributes);
            inheritableRequestAttributesHolder.remove();
        }

    }

    @Nullable
    public static RequestAttributes getRequestAttributes() {
        RequestAttributes attributes = (RequestAttributes)requestAttributesHolder.get();
        if (attributes == null) {
            attributes = (RequestAttributes)inheritableRequestAttributesHolder.get();
        }

        return attributes;
    }
    // ...

ThreadLocal <RequestAttributes> 타입의 RequestAttributesHolder와 inheritableRequestAttributesHolder를 가지고 있다.

근데 이 두 변수 사이의 차이점은 RequestAttributesHolder가 생성하는 NamedThreadLocal 객체는 ThreadLocal을 상속하는데,

inheritableRequestAttributesHolder가 생성하는 NamedInheritableThreadLocal는 InheritableThreadLocal를 상속한다.

InheritableThreadLocal

InheritableThreadLocal은 ThreadLocal의 하위 클래스로, 부모 스레드에서 자식 스레드로의 값 상속을 제공한다.

자식 스레드가 생성될 때, 부모 스레드가 값을 가지고 있는 모든 상속 가능한 스레드 로컬 변수들에 대한 초기 값들을 받는다.

그렇다면 일반적인 경우, 자식의 값과 부모의 값은 동일하다.

그러나 이 클래스의 childValue 메서드를 오버라이드하여 부모의 값을 자식의 값으로 변환할 수 있다. 즉 전파(Propagation)가 가능하다.

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    /**
     * Creates an inheritable thread local variable.
     */
    public InheritableThreadLocal() {}

    /**
     * Computes the child's initial value for this inheritable thread-local
     * variable as a function of the parent's value at the time the child
     * thread is created.  This method is called from within the parent
     * thread before the child is started.
     * <p>
     * This method merely returns its input argument, and should be overridden
     * if a different behavior is desired.
     *
     * @param parentValue the parent thread's value
     * @return the child thread's initial value
     */
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */
    @Override
    ThreadLocalMap getMap(Thread t) {
        return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */
    @Override
    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

 

getMap() 메서드를 보면, ThreadLocal의 getMap()의 경우 t.threadLocals를 리턴했는데, 이 클래스에선 t.inheritableThreadLocals를 리턴한다. 즉 threadLocals와는 별도로 데이터를 갖고 있다.

 

또한 Thread 클래스에서 inheritableThreadLocals를 생성하는 코드를 보면 아래와 같이 구현되어 있다.

if (parentMap != null
                        && parentMap != ThreadLocal.ThreadLocalMap.NOT_SUPPORTED
                        && parentMap.size() > 0) {
                    this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parentMap);
                }
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }

여기서 주목해야 하는 점은 InheritedMap을 생성할 때 parentMap을 얕은 복사(Shallow copy)를 수행한다는 점이다.

따라서 inheritedMap의 변경 사항이 parentMap에도 반영된다.

왜 Request Attributes는 ThreadLocal, Request Context는 inheritableThreadLocal을 사용하는가?

그러면 궁금한 점이 왜 RequestContextHolder는 Request attributes를 저장할 때 ThreadLocal을 사용하고,

Request Context를 저장할 때 inheritableThreadLocal 사용하는 것 일까?

 

Request Attributes는 HTTP 요청과 관련된 정보들(예: HTTP 메서드, 요청 URL, 요청 헤더, 요청 파라미터 등)을 포함한다.

Spring은 이러한 Request Attributes를 ThreadLocal을 통해 저장한다. ThreadLocal은 특정 스레드에 바인딩된 데이터를 저장하고 관리하기 때문에, 각 HTTP 요청마다 스레드가 독립된 Request Attributes를 가지게 된다.

이는 Tomcat과 같은 서블릿 컨테이너가 각 요청에 대해 별도의 스레드를 할당하는 구조와 일치하여, 각 요청이 독립적인 상태를 유지할 수 있다.

ServletRequestAttributes에서 request Attributes


Request Context는 Request Attributes보다 더 넓은 범위의 정보를 포함할 수 있으며, 사용자 ID, 트랜잭션 ID, 인증 정보 등과 같은 데이터를 포함할 수 있다. Spring은 Request Context를 InheritableThreadLocal을 통해 저장한다.
이러한 구조는 스레드가 하위 작업을 처리하기 위해 자식 스레드를 생성하는 경우(예: 비동기 처리, @Async 어노테이션 사용)에도 자식 스레드가 부모 스레드의 Request Context에 접근할 수 있게 한다.

이를 통해 자식 스레드가 부모 스레드의 컨텍스트를 공유하여 요청과 관련된 작업을 일관되게 처리할 수 있다.

Test Transaction context가 inheritableThreadLocals를 참조하고 있다.

마치며

  • ThreadLocal의 역할과 어떻게 구현되어서, 어떤 흐름으로 동작하는지를 살펴보았다.
  • 멀티 스레딩 환경에서 개발 하면서 문제가 발생한다면  ThreadLocal와 관련된 스레드간 데이터 동기화 문제인지를 고려해보면 좋을 것 같다.

참고