Java/동시성

ThreadLocal

창욱씨 2020. 5. 26. 21:50
반응형

ThreadLocal 정리


1. ThreadLocal이란?

  • 각 스레드마다 독립적인 데이터를 저장할 수 있도록 해주는 Java 클래스
  • 실제 데이터 저장 → 각 Thread 내부의 ThreadLocalMap
  • ThreadLocal → 그 Map에 접근하기 위한 인터페이스(열쇠) 역할

2. 구조

ThreadLocal (열쇠, 개발자가 선언)
       │
       ├── Thread-1의 ThreadLocalMap { ThreadLocal → "Alice" }
       ├── Thread-2의 ThreadLocalMap { ThreadLocal → "Bob"   }
       └── Thread-3의 ThreadLocalMap { ThreadLocal → "Charlie" }
  • ThreadLocal 객체는 코드에서 선언한 수만큼만 생성 (보통 static)
  • ThreadLocal 객체 자체가 ThreadLocalMap의 key로 사용됨

3. 데이터 저장 슬롯

  • ThreadLocal 개수 = 각 스레드가 저장할 수 있는 최대 슬롯 수
  • set()을 호출한 ThreadLocal에 대해서만 실제 데이터가 저장됨
  • ThreadLocal이 5개라도 set()을 2번만 호출하면 2개만 저장됨

4. 메모리 부하

  • ThreadLocal이 많아질수록 스레드 수만큼 곱해져 메모리 부하 증가
    ThreadLocal 10개 × 스레드 100개 = 1000개의 데이터
  • Thread Pool 환경에서 remove() 미호출 시 → 메모리 누수(Memory Leak) 위험

5. 실무 주의사항

규칙 이유
ThreadLocal은 최소한으로 선언 메모리 부하 감소
사용 후 반드시 remove() 호출 메모리 누수 방지
여러 데이터는 객체로 묶어서 하나의 ThreadLocal로 관리 슬롯 수 최소화

6. 실무 사용 예시

① 로그인 사용자 정보 전달 (가장 흔한 사례)

Spring 같은 웹 프레임워크에서 로그인한 사용자 정보를 메서드 파라미터 없이 어디서든 꺼내 쓸 때 사용합니다.

public class UserContextHolder {
    private static final ThreadLocal<UserInfo> context = new ThreadLocal<>();

    public static void set(UserInfo user) { context.set(user); }
    public static UserInfo get() { return context.get(); }
    public static void clear() { context.remove(); }
}

// 요청 시작 시 (Filter 또는 Interceptor)
UserContextHolder.set(loginUser);

// 비즈니스 로직 어디서든
UserInfo user = UserContextHolder.get(); // 파라미터 없이 꺼내 사용

// 요청 종료 시
UserContextHolder.clear(); // 반드시 정리!

② 로그 추적 ID (MDC)

요청마다 고유한 ID를 부여해서 여러 로그를 하나의 요청으로 묶어 추적할 때 사용합니다.

// 요청 시작 시 (Filter)
String requestId = UUID.randomUUID().toString();
MDC.put("requestId", requestId); // 내부적으로 ThreadLocal 사용

// 로그 출력 시 requestId가 자동으로 포함됨
log.info("주문 처리 시작"); // [requestId=abc-123] 주문 처리 시작
log.info("결제 완료");      // [requestId=abc-123] 결제 완료

// 요청 종료 시
MDC.clear();

③ 트랜잭션 관리 (Spring 내부)

Spring의 @Transactional이 내부적으로 ThreadLocal을 사용해서 같은 스레드 내에서 동일한 DB 커넥션을 유지합니다.

// 개발자가 직접 쓰진 않지만 Spring 내부에서 이렇게 동작
TransactionSynchronizationManager.bindResource(dataSource, connection);
// → ThreadLocal에 DB 커넥션을 저장해두고
//   같은 트랜잭션 내 모든 쿼리가 동일한 커넥션을 사용하도록 보장

④ 데이터베이스 멀티 테넌시 (DB 분기)

SaaS 서비스에서 테넌트(고객사)별로 다른 DB에 접근할 때 사용합니다.

public class TenantContextHolder {
    private static final ThreadLocal<String> tenantId = new ThreadLocal<>();

    public static void set(String id) { tenantId.set(id); }
    public static String get() { return tenantId.get(); }
    public static void clear() { tenantId.remove(); }
}

// 요청마다 테넌트 ID 저장
TenantContextHolder.set("company-A");

// DB 접근 시 테넌트에 맞는 DB로 자동 분기
String tenant = TenantContextHolder.get(); // "company-A"
DataSource ds = dataSourceMap.get(tenant); // company-A 전용 DB 사용
반응형