BACKEND

[java]ThreadLocal 사용법

handcraft 2025. 8. 21. 16:49

ThreadLocal은 각각의 쓰레드 별로 별도의 저장공간을 제공하는 컨테이너 == 물건보관 창고

 

"각각 물건을 보관하면 각각 물건을 잘 보관할수 있게
겹치거나 사라지지 않게"

일반적으로 변수는 여러 스레드에 의해 공유될 수 있다. 만약 여러 스레드가 동시에 같은 변수를 읽고 쓰게 되면, 데이터가 꼬이거나 예상치 못한 오류가 발생할 수 있다. 이를 해결하기 위해 synchronized나 Lock 같은 동기화 메커니즘을 사용하기도 하지만, 이는 스레드가 순서를 기다려야 하므로 성능 저하를 일으킬 수 있다.

 

// 각 스레드마다 독립적인 사용자 이름을 저장할 ThreadLocal 변수
    private static final ThreadLocal<String> userContext = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
        // 스레드 풀 생성
        ExecutorService executor = Executors.newFixedThreadPool(2);

        // 첫 번째 작업: user1 로그인
        executor.submit(() -> {
            System.out.println("스레드 " + Thread.currentThread().getName() + " - user1 로그인 시작");
            userContext.set("user1"); // 현재 스레드의 userContext에 "user1" 저장
            printUserInfo();
            // 작업이 끝나면 꼭 remove() 호출하여 메모리 누수 방지
            userContext.remove(); 
            System.out.println("스레드 " + Thread.currentThread().getName() + " - user1 로그아웃");
        });

        // 두 번째 작업: user2 로그인
        executor.submit(() -> {
            System.out.println("스레드 " + Thread.currentThread().getName() + " - user2 로그인 시작");
            userContext.set("user2"); // 현재 스레드의 userContext에 "user2" 저장
            printUserInfo();
            userContext.remove();
            System.out.println("스레드 " + Thread.currentThread().getName() + " - user2 로그아웃");
        });

        executor.shutdown();
        executor.awaitTermination(1, TimeUnit.MINUTES);
    }

    private static void printUserInfo() {
        // 현재 스레드에 저장된 사용자 이름 가져오기
        String username = userContext.get();
        System.out.println("스레드 " + Thread.currentThread().getName() + " - 현재 로그인된 사용자: " + username);
    }
 }
 
 결과 
 스레 pool-1-thread-1 - user1 로그인 시작
스레 pool-1-thread-2 - user2 로그인 시작
스레 pool-1-thread-1 - 현재 로그인된 사용자: user1
스레 pool-1-thread-2 - 현재 로그인된 사용자: user2
스레 pool-1-thread-1 - user1 로그아웃
스레 pool-1-thread-2 - user2 로그아웃

 

언제 사용?

하나의 작업 요청에 대한 모든 정보를 담는 '데이터 클래스'입니다. 마치 택배 송장처럼, 누가(username), 어떤 물건을(kollection, drama), 어디로(cineroomIds) 보내는지 같은 정보를 한 곳에 모아둡니다.

 

모든 로직에서 누가 했는지 기록을 남기기 위해 사용 (나의 경우 공통 entity를 만들고 어디서든 사용자 정보를 가져올 때 사용했다.   )

 

 

만든 흐름은 이렇다. 

요청 시작 >> 컨텍스트 설정 >> 데이터 사용 >> 데이터 종료 

 

  • 요청 시작 (Request Start)
    • 사용자가 웹 애플리케이션에 HTTP 요청을 보냅니다. (예: 로그인, 게시글 작성 등)
    • 웹 서버는 이 요청을 처리하기 위해 스레드 풀에서 스레드 하나를 가져와 할당합니다.
  • 컨텍스트 설정 (Set Context)
    • 애플리케이션의 필터(Filter) 또는 인터셉터(Interceptor) 같은 진입점에서 요청을 가로챕니다.
    • 이 시점에 요청에서 필요한 정보(예: 로그인한 사용자 정보, 트랜잭션 ID, 요청 ID)를 추출하여 ThreadLocal 변수에 저장합니다.
    • 코드 예시: StageContext.set(new StageRequest(username, ...))
  • 데이터 사용 (Use Data)
    • 이제 요청을 처리하는 스레드 안에서는 애플리케이션의 어느 계층(서비스, 리포지토리 등)에서든 ThreadLocal에 저장된 데이터를 쉽게 가져와 사용할 수 있습니다.
    • 별도의 파라미터 전달 없이도 ThreadLocal.get() 메서드만 호출하면 되므로, 코드가 훨씬 간결해집니다.
    • 코드 예시: StageRequest req = StageContext.get(); String user = req.getUsername();
  • 컨텍스트 정리 (Clear Context)
    • 요청 처리가 모두 끝나고 응답을 보내기 직전에, 다시 필터나 인터셉터가 동작합니다.
    • 이때 ThreadLocal에 저장된 데이터를 **반드시 제거(remove)**해야 합니다.
    • 코드 예시: StageContext.clear()
    • 이 과정이 매우 중요한데, 만약 제거하지 않으면 스레드 풀에 반환된 스레드가 이전 요청의 데이터를 그대로 가지고 있게 되어, 다음 요청에서 예상치 못한 오류를 일으키거나 메모리 누수를 발생시킬 수 있습니다.

 

ThreadLocal 사용시 주의점

사용법은 간단하지만 꼭 주의해야할 점이 있다. ThreadLocalContext를 사용후에는 반납시 자동으로 값이 초기화되지 않기 때문에 Thread Pool 처럼 전체 Pool안에서 재활용하며 application이 구동될 경우 신규 context 생성 시 이전 쓰레기값이 채워진채로 재사용될 수가 있다.