개요
Spring Security 에서 ThreadLocal에대해서 언급한적이 있어 글로 남겨봅니다.
ThreadLocal이란?
ThreadLocal은 Java에서 제공하는 클래스로, java.lang 패키지에 존재합니다.
각 스레드마다 독립적인 변수를 가질 수 있게 해주는 기능인데 쉽게 말해, ThreadLocal에 저장된 데이터는 해당 Thread만 접근할 수 있는 데이터 저장소라고 할 수 있습니다. 개인이 가지는 사물함이라고 생각하시면 쉽습니다.
동시성 문제
그럼 왜 ThreadLocal을 알아야 할까요? JAVA는 멀티쓰레딩 환경으로 한 메소드에 동시에 접근이 가능합니다. 그래서 항상 동시성문제에 신경써야하죠. 특히 Spring Container에서는 Bean으로 등록해 객체를 싱글톤으로 관리해 자원을 최소화 합니다. 하나의 객체를 여러명이 사용하면 문제가 생기기 마련입니다. 읽기 메소드는 멱등성을 보장받아 문제가 생기지않지만 수정, 생성, 삭제 등 데이터가 변경되었을 때 문제가 생기죠.
싱글톤패턴에 대해서 알고싶다면 아래 글을 읽어보세요!
자바(JAVA) - 싱글톤 패턴(Singleton Pattern)
싱글톤 패턴 싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생
tmd8633.tistory.com
자바(JAVA) - 싱글톤 패턴(Singleton Pattern)
싱글톤 패턴 싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생
tmd8633.tistory.com
public class BankAccount {
private int balance = 0;
// 동시성 문제가 발생하는 메서드
public int transfer(int amount) {
int currentBalance = balance; // 현재 잔액 읽기
balance = currentBalance + amount; // 잔액 업데이트
return balance;
}
public int getBalance() {
return balance;
}
}
가령 BankAccount 객체가 싱글톤으로 관리되고, 여러 사용자가 BankAccount를 사용한다고 했을때,
- 사용자A : bankAccount.transfer(1000)
- 사용자A 현재 잔액 읽음 : 현재 잔액 0
- 사용자B : bankAccount.transfer(1000)
- 사용자B 현재 잔액 읽음 : 현재 잔액 0
- 사용자A : 잔액 업데이트 - 결과 반환 1000
- 사용자B : 잔액 업데이트 - 결과 반환 1000
최종 잔액이 2000이 될 것으로 예상했지만 동시성 문제로 최종 잔액이 1000이 되었습니다.
ThreadLocal의 특징
- 스레드 안전성: 각 스레드가 자신만의 독립된 변수를 가지므로, 동기화 없이도 스레드 안전성을 보장합니다.
- 데이터 격리: 다른 스레드의 데이터에 접근할 수 없어 데이터 격리가 완벽하게 이루어집니다.
- 성능: 동기화가 필요 없으므로, synchronized 키워드 사용 대비 성능상 이점이 있습니다.
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();
}
boolean isPresent() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
return map != null && map.getEntry(this) != null;
}
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 void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
static class ThreadLocalMap {
static class Entry extends WeakReference<java.lang.ThreadLocal<?>> {
Object value;
Entry(java.lang.ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
ThreadLocal은 ThreadLocalMap을 가지고있고 여기에서 key, value로 데이터를 보관합니다.
그리고 이때 get()메소드에서 Thread.currentThread()를 사용해 Thread를 꺼내고 그 ThreadLocalMap을 반환해서 가져오게됩니다.
이제 개념을 알았으니 어디에서 사용하고 있는지 간단하게 알아보겠습니다.
사용 사례
Spring Security
public class SecurityContextHolder {
// ... 코드 생략
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");
} else {
if (!StringUtils.hasText(strategyName)) {
strategyName = "MODE_THREADLOCAL"; // ThreadLocal 전략이 Default
}
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 {
Class<?> clazz = Class.forName(strategyName);
Constructor<?> customStrategy = clazz.getConstructor();
strategy = (SecurityContextHolderStrategy)customStrategy.newInstance();
} catch (Exception ex) {
ReflectionUtils.handleReflectionException(ex);
}
}
}
}
final class ThreadLocalSecurityContextHolderStrategy implements SecurityContextHolderStrategy {
private static final ThreadLocal<Supplier<SecurityContext>> contextHolder = new ThreadLocal();
// ... 코드 생략
}
Transaction
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager implements ResourceTransactionManager, InitializingBean {
// ... 코드 생략
protected Object doGetTransaction() {
DataSourceTransactionObject txObject = new DataSourceTransactionObject();
txObject.setSavepointAllowed(this.isNestedTransactionAllowed());
ConnectionHolder conHolder = (ConnectionHolder)TransactionSynchronizationManager.getResource(this.obtainDataSource());
txObject.setConnectionHolder(conHolder, false);
return txObject;
}
protected void doBegin(Object transaction, TransactionDefinition definition) {
// ... 코드 생략
if (txObject.isNewConnectionHolder()) {
TransactionSynchronizationManager.bindResource(this.obtainDataSource(), txObject.getConnectionHolder());
}
// ... 코드 생략
}
}
public abstract class TransactionSynchronizationManager {
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations = new NamedThreadLocal("Transaction synchronizations");
private static final ThreadLocal<String> currentTransactionName = new NamedThreadLocal("Current transaction name");
private static final ThreadLocal<Boolean> currentTransactionReadOnly = new NamedThreadLocal("Current transaction read-only status");
private static final ThreadLocal<Integer> currentTransactionIsolationLevel = new NamedThreadLocal("Current transaction isolation level");
private static final ThreadLocal<Boolean> actualTransactionActive = new NamedThreadLocal("Actual transaction active");
// ... 코드 생략
}
Hibernate
public class ThreadLocalSessionContext extends AbstractCurrentSessionContext {
private static final CoreMessageLogger LOG = (CoreMessageLogger)Logger.getMessageLogger(CoreMessageLogger.class, ThreadLocalSessionContext.class.getName());
private static final Class<?>[] SESSION_PROXY_INTERFACES = new Class[]{Session.class, SessionImplementor.class, EventSource.class, LobCreationContext.class};
private static final ThreadLocal<Map<SessionFactory, Session>> CONTEXT_TL = ThreadLocal.withInitial(HashMap::new);
// ... 코드 생략
}
ThreadLocal 주의사항
메모리 누수
ThreadLocal 사용 후에는 반드시 remove()를 호출하여 메모리 누수를 방지해야 합니다. 특히 스레드 풀을 사용하는 환경에서는 더욱 중요합니다.
try {
// ThreadLocal 사용
userContext.set(new UserContext());
// 비즈니스 로직
} finally {
// 반드시 삭제
userContext.remove();
}
가능하면 try-with-resources 패턴을 사용해 자원을 반납하면 안전하게 사용할 수 있습니다.
성능 고려사항
- ThreadLocal은 각 스레드마다 별도의 메모리를 사용합니다.
- 많은 수의 ThreadLocal 변수를 사용하면 메모리 사용량이 증가할 수 있습니다.
- get()과 set() 연산은 매우 빠르지만, 너무 빈번한 접근은 피하는 것이 좋습니다.
static final 선언
static final로 선언하는걸 권장합니다. 위에 사용 사례를 보시면 모두 static final로 선언되어있는걸 보실 수 있습니다.
- static 사용 이유
- ThreadLocal 객체 자체는 모든 스레드가 공유해도 됩니다
- 각 스레드마다 값을 따로 저장하는 것은 ThreadLocal의 내부 구현이 처리함
- 불필요한 인스턴스 생성을 방지할 수 있음
- 전역적으로 접근이 필요한 경우가 많음 (예: Transaction, Security 등)
- final 사용 이유
- ThreadLocal 인스턴스 자체는 변경될 필요가 없음
- 한 번 생성된 후에는 참조가 변경되면 안됨
- 실수로 ThreadLocal 참조를 바꾸는 것을 방지
- 불변성을 보장하여 스레드 안전성을 높임
끝!
'Language > JAVA' 카테고리의 다른 글
[Java] Stream - 병렬 스트림 (0) | 2024.11.26 |
---|---|
[JAVA] StringTokenizer 에 대해서 (0) | 2024.11.14 |
[JAVA] Record와 Sealed (0) | 2024.11.13 |
[JAVA] 상속(Inheritance)과 복합(Composition) (0) | 2024.11.12 |
[JAVA] String + 연산을 왜 쓰지말아야할까 (0) | 2024.09.24 |