오늘은 Java의 Thread Local에 대해서 알아보자
ThreadLocal이란 Java에서 지원하는 Thread safe한 기술로 멀티 스레드 환경에서 각각의 스레드에게 별도의 저장공간을 할당하여 별도의 상태를 갖을 수 있게끔 도와준다.
ThreadLocal이 필요한 이유
예를들어 Spring의 tomcat을 보면 매 요청마다 생성해놓은 Thread pool에서 Thread를 할당하여 유저의 요청을 처리하도록 되어있다. 여기서 문제가 발생하는데 Spring에서 bean을 등록하게 되면 해당 객체는 단 1개만 만들어져서 모든 Thread가 공유하여 사용하도록 되어있다. 이때 해당 인스턴스의 특정 필드를 모든 Thread가 공유하게 되는 것인데 여기서 Thread 동기화 문제가 발생하게 된다.
이해하기 쉽도록 그림을 통해 예시를 들어보도록 하겠다.
동시성 문제
결과적으로 Thread-A 입장에서는 저장한 데이터와 조회한 데이터가 다른 문제가 발생한다. 이처럼 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라 한다. 이런 동시성 문제는 여러 쓰레드가 같은 인스턴스의 필드에 접근해야 하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질 수 록 자주 발생한다. 특히 스프링 빈 처럼 싱글톤 객체의 필드를 변경하며 사용할 때 이러한 동시성 문제를 조심해야 한다.
참고
이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다. 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다. 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
ThreadLocal
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 쉽게 이야기해서 물건 보관 창구를 떠올리면 된다. 여러 사람이 같은 물건 보관 창구를 사용하더라도 창구 직원은 사용자를 인식해서 사용자별로 확실하게 물건을 구분해준다.
그렇다면 ThreadLocal은 어떻게 쓰레드마다 고유한 저장공간을 보장해 줄 수 있는걸까??
이를 이해하기 위해서느 Thread class와 ThreadLocal class를 모두 살펴보아야 한다.
public class Thread implements Runnable {
//...logics
ThreadLocal.ThreadLocalMap threadLocals = null;
}
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);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
두 클래스를 살펴보면 Thread는 객체는 threadLocals라는 인스턴스변수를 가지고 있는데 ThreadLocal클래스를 이용해 ThreadLocal 내부의 ThreadLocalMap이라는 클래스를 이용해 key/value로 데이터를 보관하고 있다.
그리고 ThreadLocal의 get, set등의 메서드들의 원리도 Thread에서 현재 수행중인 thread를 currentThread() 메서드를 통해 꺼낸 뒤 이 Thread에서 ThreadLocalMap을 찾아 리턴하는 것이다.
Spring security
스프링 시큐리티(Spring Security)에서는 SecurityContextHolder에 SecurityContext 안에 Authentication을 보관한다. 여기서 SecurityContextHolder는 SecurityContext를 저장하는 방식을 전략패턴으로 유연하게 대응하는데, 이 중 기본 전략이 MODE_THREADLOCAL로 ThreadLocal을 사용하여 SecurityContext를 보관하는 방식이다.
public class SecurityContextHolder {
//...
//SecurityContextHolderStrategy 안에 SecurityContext가 보관된다.
private static SecurityContextHolderStrategy strategy;
private static void initialize() {
if (!StringUtils.hasText(strategyName)) {
// Set default
strategyName = MODE_THREADLOCAL; //기본 전략이 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++;
}
}
package org.springframework.security.core.context;
import org.springframework.util.Assert;
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을 사용할 때 반드시 인지해야할 주의할 점이 있다. 앞서 이야기했듯이 우리가 사용하는 WAS(tomcat)은 Thread pool 기반으로 동작한다. 따라서 ThreadLocal을 사용할 때 사용 후에 비워주지 않는다면 해당 Thread를 부여받게 되는 다른 사용자가 기존에 세팅된 ThreadLocal의 데이터를 공유하게 될 수도 있다.
그렇기에, Thread 의 사용이 끝나는 시점에 Thread Pool에 반환을 하기 직전 반드시 ThreadLocal을 초기화시켜주는 작업을 해줘야 한다.
reference