제육's 휘발성 코딩
article thumbnail
Published 2025. 3. 10. 18:09
ThreadLocal 이해하기 🔷 Spring
반응형

1. ThreadLocal이란?

ThreadLocal스레드별로 독립적인 변수를 제공하는 자바 클래스입니다. 일반적인 변수는 모든 스레드가 공유하지만, ThreadLocal로 선언된 변수는 각 스레드가 자신만의 값을 가질 수 있습니다. 이를 통해 스레드 간 데이터 간섭을 방지하고, 스레드 안전성을 확보할 수 있죠.

쉽게 말해, ThreadLocal은 "스레드마다 개인 사물함을 주는" 개념이라고 생각하면 됩니다. 각 스레드가 자신의 사물함에 데이터를 넣고 꺼낼 수 있지만, 다른 스레드의 사물함에는 접근할 수 없습니다.

1.1. 주요 특징

  • 스레드 격리: 각 스레드는 자신만의 ThreadLocal 값을 가집니다.
  • 간단한 사용법: set(), get(), remove() 메서드로 쉽게 관리 가능.
  • 메모리 누수 주의: 사용 후 정리하지 않으면 메모리 누수가 발생할 수 있음.

1.2. 동시성 발생 예시

ThreadLocal을 사용하지 않고 일반 변수를 공유하면, 멀티스레드 환경에서 동시성 문제가 발생할 수 있습니다. 아래는 그 예제입니다.

<code />
public class NoThreadLocalExample { private static String sharedValue = ""; // 모든 스레드가 공유하는 변수 public static void main(String[] args) { Runnable task = () -> { sharedValue = Thread.currentThread().getName() + " Value"; // 값 설정 try { Thread.sleep(100); // 경쟁 조건을 강조하기 위해 지연 추가 } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + ": " + sharedValue); }; Thread thread1 = new Thread(task, "Thread-1"); Thread thread2 = new Thread(task, "Thread-2"); thread1.start(); thread2.start(); } }

출력결과

출력 결과 매번 다른 값이 출력 됨

2. ThreadLocal 동작 원리

ThreadLocal은 내부적으로 Thread 객체 안에 ThreadLocalMap이라는 맵을 유지합니다. 이 맵은 ThreadLocal 객체를 키로, 해당 스레드의 값을 값으로 저장합니다. 하지만 이 간단한 설명만으로는 어떻게 스레드별로 값이 격리되는지 완전히 이해하기 어렵죠. 좀 더 깊이 들어가 보겠습니다.

2.1. ThreadLocal의 내부 구조

  • Thread 클래스와의 관계: 자바에서 모든 스레드는 Thread 클래스의 인스턴스로 표현됩니다. Thread 클래스에는 ThreadLocal.ThreadLocalMap threadLocals라는 필드가 존재합니다. 이 필드는 각 스레드마다 고유한 ThreadLocalMap을 저장합니다.

  • ThreadLocalMap이란?: ThreadLocalMap은 ThreadLocal 객체와 해당 스레드의 값을 쌍으로 저장하는 해시맵과 유사한 자료구조입니다. 키는 ThreadLocal 인스턴스이고, 값은 해당 스레드가 설정한 데이터입니다.

  • 격리 원리: threadLocals는 Thread 객체 내부에 속해 있으므로, 각 스레드는 자신만의 ThreadLocalMap을 갖게 됩니다. 즉, 스레드 A의 ThreadLocalMap과 스레드 B의 ThreadLocalMap은 완전히 별개의 객체입니다.

2.2. 동작 과정

2.2.1. 값 설정 (set 호출):

  • threadLocal.set("Value")를 호출하면, 현재 스레드(Thread.currentThread())의 threadLocals 맵에 접근합니다.
    만약 threadLocals가 null이라면 새로 ThreadLocalMap을 생성하고, ThreadLocal 객체(키)와 "Value"(값)를 맵에 저장합니다.
  • 이미 맵이 존재한다면, 해당 ThreadLocal 키에 새로운 값을 업데이트합니다.

2.2.2. 값 조회 (get 호출):

  • threadLocal.get()을 호출하면, 현재 스레드의 threadLocals 맵에서 해당 ThreadLocal 키에 매핑된 값을 반환합니다.
  • 값이 없으면 ThreadLocal의 initialValue() 메서드를 호출해 기본값을 설정합니다(기본적으로 null).

2.2.3. 값 제거 (remove 호출):

  • threadLocal.remove()는 현재 스레드의 threadLocals 맵에서 해당 ThreadLocal 키와 값을 삭제합니다.

2.2.4. 스레드별 변수 보장의 핵심

  • 스레드별 맵: ThreadLocalMap이 Thread 객체에 속해 있기 때문에, 각 스레드는 독립적인 저장소를 가집니다. 스레드 A가 ThreadLocal에 값을 설정해도 스레드 B의 ThreadLocalMap에는 영향을 주지 않습니다.
  • 참조 관리: ThreadLocal 객체 자체는 모든 스레드가 공유하지만, 실제 데이터는 각 스레드의 ThreadLocalMap에 저장되므로 격리가 보장됩니다.
  • WeakReference 사용: ThreadLocalMap의 키(ThreadLocal)는 WeakReference로 관리됩니다. 이는 ThreadLocal 객체가 더 이상 참조되지 않을 때 가비지 컬렉션에 의해 제거될 수 있게 해줍니다. 하지만 값은 강한 참조로 유지되므로, 스레드가 종료되고 remove()를 호출하지 않으면 메모리 누수가 발생할 수 있습니다.

2.3. 동작 예시

스레드 1과 스레드 2가 동일한 ThreadLocal 인스턴스를 사용할 때:

  • 스레드 1: threadLocal.set("Value1") → 스레드 1의 ThreadLocalMap에 {threadLocal: "Value1"} 저장.
  • 스레드 2: threadLocal.set("Value2") → 스레드 2의 ThreadLocalMap에 {threadLocal: "Value2"} 저장.
  • 스레드 1: threadLocal.get() → "Value1" 반환.
  • 스레드 2: threadLocal.get() → "Value2" 반환.

이처럼 각 스레드는 자신의 ThreadLocalMap만 참조하므로 값이 서로 간섭하지 않습니다.

3. 코드로 살펴보기

간단한 예제를 통해 ThreadLocal의 사용법을 알아봅시다.

<code />
public class ThreadLocalExample { private static final ThreadLocal threadLocal<String> = new ThreadLocal<>(); public static void main(String[] args) { // 스레드 1 Thread thread1 = new Thread(() -> { threadLocal.set("Thread-1 Value"); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); threadLocal.remove(); // 사용 후 제거 }, "Thread-1"); // 스레드 2 Thread thread2 = new Thread(() -> { threadLocal.set("Thread-2 Value"); System.out.println(Thread.currentThread().getName() + ": " + threadLocal.get()); threadLocal.remove(); }, "Thread-2"); thread1.start(); thread2.start(); } }

출력 결과 (쓰레드 별 고유 값 보장)

  • threadLocal.set()으로 각 스레드가 자신의 값을 설정하고, get()으로 꺼냅니다.
  • 스레드 1과 스레드 2의 값이 서로 간섭하지 않는 것을 확인할 수 있죠.

3.1. 사용자 세션 관리

웹 애플리케이션에서 요청별로 사용자 정보를 ThreadLocal에 저장하면, 스레드 풀 환경에서도 안전하게 사용자 데이터를 관리할 수 있습니다.

<code />
public class UserContext { private static final ThreadLocal<String> userId = new ThreadLocal<>(); public static void setUser(String id) { userId.set(id); } public static String getUser() { return userId.get(); } public static void clear() { userId.remove(); } }

3.2. 트랜잭션 컨텍스트 전달

Spring 프레임워크에서 TransactionSynchronizationManager는 ThreadLocal을 사용해 트랜잭션 상태를 스레드별로 관리합니다.

주의할 점: 메모리 누수
ThreadLocal을 사용할 때는 반드시 사용 후 remove()를 호출해야 합니다. 스레드 풀이 사용되는 환경(예: 웹 서버)에서 스레드가 재사용되면, 이전에 저장된 ThreadLocal 값이 남아 있을 수 있어 메모리 누수가 발생할 수 있습니다.

해결법

  • 작업 종료 후 항상 remove() 호출.
  • try-finally 블록으로 보장:
<code />
try { threadLocal.set("Some Value"); // 작업 수행 } finally { threadLocal.remove(); }

3.3. ThreadLocal vs 전역 변수 vs synchronized

ThreadLocal vs 전역 변수 vs syncronized

반응형
profile

제육's 휘발성 코딩

@sasca37

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요! 맞구독은 언제나 환영입니다^^