Spring Security와 JWT에 대해 학습하던 도중, 의문점이 생기게 되었다.
인증 필터를 따로 만든 클래스에서, 다음과 같은 메소드를 볼 수 있었다.
여기서 의문인 부분은 바로 SecurityContextHolder.setContext(context)
이거였다.
왜 의문이 들었는지, 결국 진실은 무엇이었는지 살펴보는 시간을 가져본다.
스프링의 WAS에서는 스레드 풀이 존재하며, 각 요청에 대해 스레드를 실행하는 형태다.
각각의 스레드는 스텍 공간이 따로 주어지지만, 서버에 존재하는 객체 즉, 힙 영역
에 존재하는 데이터에 대해선 공유
할 수 있다.
사용자 A와 사용자 B의 요청은 각각의 스레드에서 처리되지만, 힙 영역
에 존재하는 공유 자원
에 접근하게 되면 동기화 문제
가 발생할 수도 있다.
여기서, 다음 코드를 다시 살펴보자.
해당 코드를 해석하자면 다음과 같다.
- 빈 SecurityContext를 생성한다.
- 인증 객체를 생성한다.
- 1번에서 만들어진 SecurityContext에 인증 객체를 설정한다.
- SecurityContextHolder에 SecurityContext를 설정한다.
4번에 대해 의문이 들었다.
코드를 보면 클래스 레벨에서 메소드를 호출하는 모습을 볼 수 있다. 즉, 따로 객체가 만들어지지 않는 모습을 볼 수 있다. 이 말은, 모든 요청(스레드)마다 같은 SecurityContextHolder
를 사용한다는 것이다.
그렇다면, 사용자 A와 B가 거의 동시에 서버에 요청을 보냈을 때, 최종적으로 누구의 인증 객체가 담긴 SecurityContext가 SecurityContextHolder에 저장되는 것인가?
A도 SecurityContextHolder.setContext(context)
를 호출하고
B도 SecurityContextHolder.setContext(context)
를 호출하게 될 텐데
그렇다면 다음과 같은 상황이 되지 않는가?
하지만, 디버깅을 직접 해보면 마치 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();
}
}
서버가 실행되면 기본적으로 SecurityContextHolder
의 strategy
가 MODE_THREADLOCAL
인 것을 볼 수 있다. 그리고, SecurityContextHolder.getContext()
의 메소드를 보면 strategy.getContext()
를 호출하는 모습을 볼 수 있다.
기본 전략이 MODE_THREADLOCAL
라서 strategy
의 구현체는 ThreadLocalSecurityContextHolderStrategy
가 된다. 해당 클래스의 getContext()
를 살펴보자.
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);
}
...
}
해당 클래스의 필드인 contextHolder
의 get
과 set
이 getContext()
와 setContext()
에서 호출되는 모습을 볼 수 있다.
그렇다면, contextHolder
필드가 가리키는 ThreadLocal
객체는 무엇인가? 그냥 Thread
클래스는 많이 들어봤지만, 차이점이 있는 클래스인가?
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);
}
}
...
}
ThreadLocal
의 get
이나 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