이전 게시글에서 Spring의 @Async를 공부하면서, decorate()
를 오버라이드해 ThreadLocal을 복사하는 코드를 따로 작성해주어야 한다는 내용이 있었다.
public class CustomDecorator implements TaskDecorator{
@Override
public Runnable decorate(Runnable runnable){
//현재 요청의 RequestAttribute를 가져옴
RequestAttributes attributes = RequestContextHolder.currentRequestAttributes();
return () -> {
try{
//작업 실행 전에 RequestAttributes를 설정
RequestContextHolder.setRequestAttributes(attributes);
//작업 실행
runnable.run()
} finally{
//작업 실행 후에 RequestAttributes를 제거
RequestContextHolder.resetRequestAttributes();
}
};
}
}
생각해보니 ThreadLocal이 무엇인지, 위의 코드가 왜 필요한지도 제대로 이해하지 못한 것 같아 따로 정리를 해보았다.
Spring에서는 요청마다 새로운 스레드가 생성된다. 하지만 @Controller
, @Service
, @Repository
등은 싱글톤으로 동작한다. 그렇다면 이전의 요청에서 남아 있던 데이터가 현재의 다른 요청에 영향을 주지는 않을까?
싱글톤만 사용한다면 싱글톤 객체는 여러 스레드에게 공유되므로, 내부 자원들도 모두 공유되어 위와 같은 문제가 발생할 수 있다.
Java에서는 ThreadLocal로 이러한 문제를 해결한다.
ThreadLocal은 멀티스레드 환경에서 각각의 스레드에게 별도의 자원을 제공해, 공유되는 서비스에서 별도의 자원에 접근하도록 하여 스레드가 각각의 상태를 가지도록 한다.
이러한 ThreadLocal의 장점은 SecurityContextHolder나 RequestContextHolder에서 요긴하게 사용된다.
모든 요청이 같은 자원을 공유했을 때 예상되는 가장 큰 문제 중 하나는 인증이다. 예를 들어 Thread A와 Thread B, 2개의 스레드가 있는 상황이 있다.
위의 문제는 ThreadLocal을 이용하면, 스레드마다 다른 상태를 가질 수 있으므로 해결되며, 인증/인가를 담당하는 Spring Security에서도 기본 전략으로 ThreadLocal을 이용하여 인증 정보를 보관한다.
스프링 시큐리티에서는 SecurityContextHolder에 SecurityContext -> Authentication 순서로 인증 정보를 보관한다. 이때, SecurityContextHolder는 SecurityContext를 저장하는 방식을 전략패턴으로 여러가지 지원하는데, 그 중 기본 전략이 ThreadLocal을 사용해 SecurityContext를 보관하는 MODE_THREADLOCAL
이다.
public class SecurityContextHolder {
//...
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL; //기본 전략
}
if (strategyName.equals(MODE_THREADLOCAL)) {
strategy = new ThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_INHERITABLETHREADLOCAL)) {
strategy = new InheritableThreadLocalSecurityContextHolderStrategy();
}
else if (strategyName.equals(MODE_GLOBAL)) {
strategy = new GlobalSecurityContextHolderStrategy();
}
else {
// 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);
}
}
initializeCount++;
}
}
HttpServletRequest 객체는 Java 웹 애플리케이션에서 클라이언트의 HTTP 요청에 대한 정보를 담고 있는 객체이다.
웹 어플리케이션은 여러 스레드에서 동작할 수 있으며, 각 클라이언트의 요청에 대한 처리는 병렬적으로 이뤄질 수 있으므로 스레드 간 HttpServletRequest를 공유하는 것은 안전하지 않다.
따라서 HttpServletRequest를 조회할 수 있는 RequestContextHolder에서 ThreadLocal을 이용해 현재 스레드에서만 이에 접근할 수 있도록 한다.
public abstract class RequestContextHolder {
...
private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new NamedThreadLocal("Request attributes");
private static final ThreadLocal<RequestAttributes> inheritableRequestAttributesHolder = new NamedInheritableThreadLocal("Request context");
...
public static void resetRequestAttributes() {
requestAttributesHolder.remove();
inheritableRequestAttributesHolder.remove();
}
}
ThreadLocal이 어느 상황에 주로 사용되는지는 이해했다. 그렇다면 ThreadLocal은 어떻게 스레드별로 자원을 구분해서 사용하도록 하는 것일까?
public class Thread implements Runnable {
//...
ThreadLocal.ThreadLocalMap threadLocals = null;
}
Thread 객체는 ThreadLocal Class를 이용해 ThreadLocal 내의 ThreadLocalMap에 접근해 key-value로 데이터를 보관한다.
public class ThreadLocal<T> {
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
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 remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
//...
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
ThreadLocalMap 구조에서 각 ThreadLocal 인스턴스는 Thread 객체를 key로 하고, 해당 ThreadLocal에 저장된 값은 value로 하는 Entry 객체에 연결된다.
이를 바탕으로 ThreadLocal의 get() 동작 방식을 확인하면,
1. currentThread()
로 현재 스레드를 호출하고
2. getMap(t)
으로 스레드가 참조하고 있는 인스턴스 변수인 threadLocals를 받아온다. threadLocals는 위의 Thread Class에 작성되어 있는 ThreadLocalMap 타입의 인스턴스이다.
3. 이후, 해당 스레드의 ThreadLocal 인스턴스와 연결된 Entry값을 map.getEntity()
로 받아와
4. ThreadLocal에 저장된 값을 result로 반환한다.
WAS(Web Application Service에서는 Thread Pool을 사용하므로, 스레드 반납 시 ThreadLocal 안의 정보를 지워주지 않으면 기존 정보가 남아있을 수 있다. 따라서, 스레드 반환 전에 ThreadLocal을 초기화시켜주어야 한다.
clearContext()
resetRequestAttributes()