티스토리 뷰

반응형

Java 백엔드 핵심 난제: 멀티스레드 동시성 이슈와 ThreadLocal 동작 원리 및 가비지 컬렉션 최적화

1. 멀티스레드 환경의 아킬레스건: 동시성(Concurrency) 이슈와 상태 공유

Tomcat과 같은 현대 웹 애플리케이션 서버(WAS)는 수많은 사용자의 요청을 처리하기 위해 멀티스레드(Multi-Thread) 모델을 기반으로 동작합니다. 대다수의 Spring 백엔드 컴포넌트는 메모리 효율성과 초기화 오버헤드를 줄이기 위해 **싱글톤(Singleton) 스코프**로 생성되어 전역에서 공유됩니다.

이때 싱글톤 객체 내부에서 상태 변화가 가능한 인스턴스 필드(멤버 변수)를 공유하게 되면, 여러 스레드가 동시에 동일한 메모리 주소에 접근하여 값을 덮어쓰는 '데이터 레이스(Data Race)' 및 동시성 오류가 발생합니다. 앞선 요청의 데이터가 뒤이은 요청에 의해 오염되는 이 현상은 금융 거래, 인증 세션 가로채기 등 엔터프라이즈 환경에서 매우 치명적인 비즈니스 결함으로 이어집니다.

동시성 해결 패러다임 제어 아키텍처 및 원리 기회비용 및 트레이드오프
동기화 락 (synchronized / Lock) 자원 접근 영역에 모니터 락을 부여하여 하나의 스레드만 순차 진입하도록 강제 제어. 스레드 대기 상태 전환(Context Switch) 오버헤드로 인한 전체 처리량 저하.
파티셔닝 격리 (ThreadLocal) 공유 변수를 두지 않고, 각 스레드 고유의 내부 전용 저장소에 데이터를 격리 보관. 스레드 풀 반환 시 명시적 리셋 청소를 누락할 경우 치명적인 메모리 누수 위험.

2. Java ThreadLocal 내부 구조: Thread와 ThreadLocalMap의 관계

ThreadLocal은 객체 동기화(Locking) 없이 멀티스레드 환경을 안전하게 구축하는 최고의 대안입니다. 트랜잭션 컨텍스트, 사용자 인증 세션 토큰, 로그 추적용 Trace ID 전파 등에 핵심적으로 활용됩니다. 많은 개발자가 'ThreadLocal 객체 내부에 스레드별 데이터 Map이 매핑되어 있을 것'이라고 직관적으로 추측하지만, 실제 JVM 내부 아키텍처는 이를 완전히 반대로 구현하고 있습니다.

실제 데이터 저장소인 ThreadLocalMap은 ThreadLocal이 아닌 실행 중인 Thread 객체 내부의 인스턴스 필드로 존재합니다. 즉, 각 스레드는 독립적인 자신만의 주머니(threadLocals)를 차고 있으며, ThreadLocal 인스턴스는 해당 주머니의 데이터를 식별하기 위한 **'조회용 키(Key)'** 역할만 수행합니다. 이 구조 덕분에 스레드가 소멸할 때 스레드에 격리되었던 로컬 데이터 맵도 자연스럽게 물리 메모리에서 함께 지워질 수 있는 기반을 갖추게 됩니다.

3. 코드 예제: Spring 필터/인터셉터 기반 로그 추적기(Trace ID) 구현

공유 싱글톤 빈 구조에서 동시성 충돌 없이 웹 요청마다 고유한 로그 추적 식별 코드를 전파하는 정밀 컴포넌트 아키텍처 구현 예제입니다.

public class LogTraceContext {
    // 스레드별 격리 공간 선언 (ThreadLocal)
    private static final ThreadLocal<String> traceIdHolder = new ThreadLocal<>();

    public static void setTraceId(String traceId) {
        traceIdHolder.set(traceId);
    }

    public static String getTraceId() {
        return traceIdHolder.get();
    }

    // [중요 트러블슈팅] 스레드 풀 반환 전 반드시 호출해야 하는 링킹 해제 메서드
    public static void clear() {
        traceIdHolder.remove();
    }
}

4. Stack Overflow 경고: 스레드 풀(Thread Pool)과 약한 참조(WeakReference) 누수 비극

Stack Overflow에서 ThreadLocal 관련 질문의 90%는 **"스레드 풀 환경에서의 데이터 오염 및 메모리 누수"**에 집중되어 있습니다. 현대 웹 어플리케이션은 성능 향상을 위해 스레드를 새로 만들지 않고 ThreadPool(톰캣 기본 200개 등) 에 미리 생성해두고 재사용합니다.

어떤 요청 처리 과정에서 특정 스레드가 ThreadLocal에 유저 정보를 설정한 뒤, 작업을 마치고 스레드 풀에 반환되었다고 가정해 봅시다. 만약 remove()를 호출해 데이터를 명시적으로 지우지 않았다면, 해당 스레드는 이전 유저의 세션 정보가 채워진 채로 다음 사용자의 요청을 할당받게 됩니다. 결과적으로 다음 사용자는 로그인하지 않았음에도 타인의 대시보드가 보이는 심각한 보안 장애를 겪게 됩니다.

추가적으로, ThreadLocalMap의 Key는 **약한 참조(WeakReference)** 구조로 설계되어 전역 참조가 끊어지면 쉽게 가비지 컬렉터에 수집되지만, 내부의 실제 값(Value)은 스레드가 살아있는 한 강한 참조(Strong Reference) 형태로 결합되어 있어 GC가 건드리지 못합니다. 스레드 풀의 스레드는 애플리케이션 수명과 동기화되어 영구적으로 살아있기 때문에, remove() 처리가 누락된 대용량 Value 데이터 객체들이 힙 영역에 계속해서 쌓이며 백엔드 WAS 시스템 전체를 자바 OutOfMemoryError(OOM) 크래시로 몰고 갑니다.

📚 공식 기술 레퍼런스 (References)

  • Java SE Platform Language Specifications (Class ThreadLocal): "java.lang.ThreadLocal - ThreadLocalMap Garbage Collection Mechanics" 코어 명세 표준 준수
  • Apache Tomcat Architecture Docs: "Thread Pool Executor Subsystem - Local Variable Memory Protection" 장애 완화 지침 인용
  • Spring Framework Core Guide: "RequestContextHolder and Thread-Bound Attributes Patterns" 구조 모델 분석 반영

#자바동시성이슈 #ThreadLocal원리 #ThreadLocalMap #스레드풀메모리누수 #WeakReferenceGC #싱글톤동시성오류 #Spring로그추적기 #OutOfMemoryError방지

공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2026/06   »
1 2 3 4 5 6
7 8 9 10 11 12 13
14 15 16 17 18 19 20
21 22 23 24 25 26 27
28 29 30
글 보관함
반응형