[TIL] SecurityContextHolder와 ThreadLocal

phdljr·2023년 11월 8일
3

TIL

목록 보기
27/70

Spring Security와 JWT에 대해 학습하던 도중, 의문점이 생기게 되었다.

인증 필터를 따로 만든 클래스에서, 다음과 같은 메소드를 볼 수 있었다.
여기서 의문인 부분은 바로 SecurityContextHolder.setContext(context) 이거였다.

왜 의문이 들었는지, 결국 진실은 무엇이었는지 살펴보는 시간을 가져본다.


서문

스프링의 WAS에서는 스레드 풀이 존재하며, 각 요청에 대해 스레드를 실행하는 형태다.

각각의 스레드는 스텍 공간이 따로 주어지지만, 서버에 존재하는 객체 즉, 힙 영역에 존재하는 데이터에 대해선 공유할 수 있다.

사용자 A와 사용자 B의 요청은 각각의 스레드에서 처리되지만, 힙 영역에 존재하는 공유 자원에 접근하게 되면 동기화 문제가 발생할 수도 있다.

여기서, 다음 코드를 다시 살펴보자.

의문점

해당 코드를 해석하자면 다음과 같다.

  1. 빈 SecurityContext를 생성한다.
  2. 인증 객체를 생성한다.
  3. 1번에서 만들어진 SecurityContext에 인증 객체를 설정한다.
  4. SecurityContextHolder에 SecurityContext를 설정한다.

4번에 대해 의문이 들었다.

코드를 보면 클래스 레벨에서 메소드를 호출하는 모습을 볼 수 있다. 즉, 따로 객체가 만들어지지 않는 모습을 볼 수 있다. 이 말은, 모든 요청(스레드)마다 같은 SecurityContextHolder를 사용한다는 것이다.

그렇다면, 사용자 A와 B가 거의 동시에 서버에 요청을 보냈을 때, 최종적으로 누구의 인증 객체가 담긴 SecurityContext가 SecurityContextHolder에 저장되는 것인가?

A도 SecurityContextHolder.setContext(context)를 호출하고
B도 SecurityContextHolder.setContext(context)를 호출하게 될 텐데
그렇다면 다음과 같은 상황이 되지 않는가?

하지만, 디버깅을 직접 해보면 마치 SecurityContextHolder가 각각의 스레드에 있는것처럼 실행되는 모습을 볼 수 있다.

객체가 저장되는 메모리 영역은 힙일텐데, 어떻게 이런 일이 가능한 것인가?

궁금해서 SecurityContextHolder를 뜯어봤다.

SecurityContextHolder

public class SecurityContextHolder {

	...

	static {
		initialize();
	}

	private static void initialize() {
		initializeStrategy();
		initializeCount++;
	}

	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;
		}
		if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
			strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
			return;
		}
		if (strategyName.equals(MODE_GLOBAL)) {
			strategy = new GlobalSecurityContextHolderStrategy();
			return;
		}
		// Try to load a custom strategy
		try {
			Class<?> clazz = Class.forName(strategyName);
			Constructor<?> customStrategy = clazz.getConstructor();
			strategy = (SecurityContextHolderStrategy) customStrategy.newInstance();
		}
		catch (Exception ex) {
			ReflectionUtils.handleReflectionException(ex);
		}
	}
    
    ...
    
    public static SecurityContext getContext() {
		return strategy.getContext();
	}
}

서버가 실행되면 기본적으로 SecurityContextHolderstrategyMODE_THREADLOCAL인 것을 볼 수 있다. 그리고, SecurityContextHolder.getContext()의 메소드를 보면 strategy.getContext()를 호출하는 모습을 볼 수 있다.

기본 전략이 MODE_THREADLOCAL라서 strategy의 구현체는 ThreadLocalSecurityContextHolderStrategy가 된다. 해당 클래스의 getContext()를 살펴보자.

ThreadLocalSecurityContextHolderStrategy

final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {

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

	@Override
	public SecurityContext getContext() {
		return getDeferredContext().get();
	}

	@Override
	public Supplier<SecurityContext> getDeferredContext() {
		Supplier<SecurityContext> result = contextHolder.get();
		if (result == null) {
			SecurityContext context = createEmptyContext();
			result = () -> context;
			contextHolder.set(result);
		}
		return result;
	}

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

해당 클래스의 필드인 contextHoldergetsetgetContext()setContext()에서 호출되는 모습을 볼 수 있다.

그렇다면, contextHolder 필드가 가리키는 ThreadLocal객체는 무엇인가? 그냥 Thread 클래스는 많이 들어봤지만, 차이점이 있는 클래스인가?

ThreadLocal

ThreadLocal 클래스는 각각의 스레드별로 별도의 저장 공간을 제공하는 컨테이너다.

멀티 스레드 환경에서 각각의 스레드에게 별도의 자원을 제공함으로써, 공유되는 서비스에서 별도의 자원에 접근하게끔 하여 각각의 스레드가 상태를 가질 수 있도록 도와준다.

코드를 직접 살펴보니 다음과 같다.

public class ThreadLocal<T> {

	...
    
    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();
    }
    
    ...
    
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    
    ...
    
}

ThreadLocalget이나 set 메소드를 살펴보면, getMap(t)을 통해 ThreadLocalMap을 불러오는 코드를 볼 수 있다. 매개변수로 현재 실행되고 있는 스레드를 넘기는 것을 보니, 이 객체가 스레드별로 구분되어져 있는 저장 공간인 듯 하다.

다음 그림처럼, ThreadLocal을 사용하면 스레드별로 저장 공간이 주어지는 것 같다.

결론

각각의 요청은 스레드별로 처리되며, 각각의 스레드는 저장 창고인 ThreadLocalMap이 있다. 이를 사용하기 위해선 ThreadLocal을 통해 접근해야 한다.

SecurityContextHolder의 기본 전략은 ThreadLocalSecurityContextHolderStrategy를 사용하는 것이며, 이는 ThreadLocal 객체를 가지고 있다.

해당 전략은 각각의 요청 스레드에 대해 ThreadLocalMap을 가져와 SecurityContext를 저장하거나 가져오는 방식이었다.

ThreadLocal을 사용하기 때문에, 스레드별로 저장 공간이 생기는 원리였다.


참조

https://catsbi.oopy.io/3ddf4078-55f0-4fde-9d51-907613a44c0d
https://www.youtube.com/watch?v=ljrgXkzagaU&ab_channel=%EC%9D%B8%ED%94%84%EB%9F%B0inflearn

profile
난 Java도 좋고, 다른 것들도 좋아

0개의 댓글