본문 바로가기
Language/Java

[JAVA] 쓰레드 로컬(ThreadLocal)의 마법 : 쓰레드별 독립적인 데이터 관리

by Papa Martino V 2026. 1. 21.
728x90

ThreadLocal
ThreadLocal

 

자바 멀티쓰레드 환경에서 공유되는 객체나 변수는 항상 동시성 문제의 위험을 안고 있습니다. synchronizedvolatile 키워드를 사용하여 동기화를 적용할 수 있지만, 이는 성능 저하를 야기하거나 구현이 복잡해지는 단점이 있습니다. 때로는 여러 쓰레드가 동일한 변수를 공유하더라도, 각 쓰레드마다 독립적인 값을 가져야 할 필요가 있습니다. 이러한 요구사항을 우아하게 해결해주는 것이 바로 ThreadLocal입니다. ThreadLocal은 쓰레드 단위로 변수를 관리하여, 마치 전역 변수처럼 보이지만 실제로는 각 쓰레드에 독립적인 사본을 제공하는 특별한 메커니즘입니다. 본 포스팅에서는 ThreadLocal의 개념부터 동작 원리, 실무에서의 활용 사례, 그리고 주의해야 할 메모리 누수(Memory Leak) 문제까지 심층적으로 다루어 보겠습니다.

1. ThreadLocal이란 무엇인가?

ThreadLocaljava.lang 패키지에 속하며, 특정 변수를 쓰레드별로 독립적인 공간에 저장하도록 해주는 클래스입니다. 즉, 어떤 쓰레드에서 ThreadLocal 변수에 값을 저장하면, 그 값은 해당 쓰레드만이 접근할 수 있고 다른 쓰레드에서는 전혀 영향을 받지 않습니다. 마치 각 쓰레드가 자신만의 전용 작업 공간을 가지는 것과 같습니다.

주요 특징:

  • 격리성(Isolation): 각 쓰레드는 ThreadLocal에 저장된 자신의 값에만 접근할 수 있습니다.
  • 가시성(Visibility): 쓰레드 내에서는 항상 최신 값이 보장됩니다 (동시성 문제와 무관).
  • 편의성: 복잡한 동기화 메커니즘 없이도 쓰레드별 고유 데이터를 쉽게 관리할 수 있습니다.

2. ThreadLocal의 내부 동작 원리

ThreadLocal은 언뜻 보면 마법처럼 느껴질 수 있습니다. 그 비밀은 Thread 클래스 내부에 숨겨져 있는 ThreadLocal.ThreadLocalMap 필드에 있습니다. 각 쓰레드는 자신만의 ThreadLocalMap 인스턴스를 가지고 있습니다. 이 맵은 ThreadLocal 객체 자체를 키(Key)로, 저장하려는 값을 값(Value)으로 사용하여 데이터를 저장합니다.

따라서 어떤 쓰레드가 threadLocalVar.set(value)를 호출하면, 해당 쓰레드의 ThreadLocalMap(threadLocalVar, value) 쌍이 저장됩니다. 다른 쓰레드가 동일한 threadLocalVar.get()을 호출하면, 그 쓰레드의 ThreadLocalMap에서 자신의 고유한 값을 가져오게 됩니다.


3. 실무적인 활용 사례 (Sample Example)

ThreadLocal은 웹 애플리케이션의 요청 처리나 데이터베이스 트랜잭션 관리 등 다양한 곳에서 유용하게 사용됩니다.

3.1. 사용자 세션 정보 관리

웹 서버에서 각 HTTP 요청을 별도의 쓰레드로 처리할 때, 요청을 처리하는 동안 로그인한 사용자 정보를 저장하고 싶을 때 유용합니다.

public class UserSession {
    private static final ThreadLocal<String> currentUser = new ThreadLocal<>();

    public static void setCurrentUser(String user) {
        currentUser.set(user);
    }

    public static String getCurrentUser() {
        return currentUser.get();
    }

    public static void clear() {
        currentUser.remove(); // 중요: 메모리 누수 방지
    }

    public static void main(String[] args) {
        Runnable task = () -> {
            String threadName = Thread.currentThread().getName();
            UserSession.setCurrentUser("User_" + threadName);
            System.out.println(threadName + " - Current User: " + UserSession.getCurrentUser());
            UserSession.clear();
        };

        new Thread(task, "Thread-1").start();
        new Thread(task, "Thread-2").start();
    }
}

3.2. 트랜잭션 컨텍스트 관리

스프링(Spring) 프레임워크의 트랜잭션 매니저도 내부적으로 ThreadLocal을 사용하여 현재 쓰레드의 트랜잭션 컨텍스트를 관리합니다.


4. ThreadLocal 사용 시 주의사항: 메모리 누수(Memory Leak)

ThreadLocal은 매우 유용하지만, 잘못 사용하면 메모리 누수를 발생시킬 수 있습니다. 그 이유는 ThreadLocalMap의 키(Key)가 WeakReference로 구현되어 있기 때문입니다.

  • WeakReference Key: ThreadLocal 객체에 대한 외부 참조가 사라지면, 가비지 컬렉터는 ThreadLocal 객체를 회수할 수 있습니다. 그러면 ThreadLocalMap 내의 해당 엔트리는 키가 null이 됩니다.
  • StrongReference Value: 문제는 값(Value)은 StrongReference로 유지된다는 것입니다. 만약 ThreadLocalMap에 키가 null인 엔트리가 쌓이고, 해당 쓰레드가 계속 살아있다면(특히 쓰레드 풀 환경에서), 이 값들은 가비지 컬렉션되지 못하고 메모리에 계속 남아있게 됩니다.

해결책: ThreadLocal 사용을 마친 후에는 반드시 threadLocalVar.remove()를 호출하여 ThreadLocalMap에서 해당 엔트리를 제거해야 합니다. 특히 웹 서버의 쓰레드 풀처럼 쓰레드가 재활용되는 환경에서는 이 작업이 필수적입니다.


내용 출처 및 참고 문헌

  • Oracle Java SE 21 Documentation: Class ThreadLocal<T>
  • Stack Overflow: What is a ThreadLocal?
  • Baeldung: Guide to ThreadLocal in Java
  • "Effective Java 3rd Edition" (Joshua Bloch) - Item 84: ThreadLocal 사용 시 주의
728x90