spring

ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법)

devJK93 2025. 3. 20.

 

 

자바에서 싱글톤 패턴static 변수를 사용할 때, 멀티스레드 환경에서는 동시성 문제가 발생할 수 있다.
특히 웹 애플리케이션에서 이런 문제가 생기면 예상치 못한 버그나 데이터 오염이 일어날 수도 있다.

1. 동시성 문제란? (한 번 겪으면 밤 샌다…😨)

멀티스레드 환경에서는 여러 개의 쓰레드가 동시에 실행되면서 같은 데이터를 접근하거나 수정할 수 있다. 그런데 만약 공유 자원을 안전하게 관리하지 않으면 한 쓰레드가 변경한 값이 다른 쓰레드에도 영향을 미쳐서 데이터가 꼬이는 문제가 생긴다.

ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법) - 1. 동시성 문제란? (한 번 겪으면 밤 샌다…😨)
동시성 문제

🛑 예제: 동시성 문제가 발생하는 코드

아래 코드는 여러 사용자의 세션 정보를 저장하는 클래스. 싱글톤 패턴을 사용했는데, 여기서 동시성 문제가 발생할 수 있다.

    public class UserSessionManager {
        private static UserSessionManager instance = new UserSessionManager();
        private String userData;

        private UserSessionManager() {}

        public static UserSessionManager getInstance() {
            return instance;
        }

        public void setUserData(String data) {
            this.userData = data;
        }

        public String getUserData() {
            return this.userData;
        }
    }

    public class ThreadIssueExample {
        public static void main(String[] args) {
            UserSessionManager session = UserSessionManager.getInstance();
            
            Thread t1 = new Thread(() -> {
                session.setUserData("User A 데이터");
                try {
                    Thread.sleep(100); // t1이 값을 설정하고 잠시 대기
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("Thread 1: " + session.getUserData()); // 예상과 다른 값이 나올 가능성 증가
            });

            Thread t2 = new Thread(() -> {
                session.setUserData("User B 데이터");
                System.out.println("Thread 2: " + session.getUserData());
            });

            t1.start();
            t2.start();
        }
    }

 

이 코드가 실행되면 예상치 못한 데이터가 출력될 수 있다.
쓰레드마다 다른 사용자의 데이터를 저장해야 하는데, 싱글톤 객체의 필드가 공유되면서 데이터가 덮어씌워진다.


2. 지역 변수는 왜 안전할까? 🤔

그럼 지역 변수는 왜 문제가 없을까?
지역 변수(메서드 내에서 선언된 변수)는 각 쓰레드마다 별도의 스택 메모리에 할당되기 때문에 다른 쓰레드와 공유되지 않는다.


* 스레드가 생성될 때 각 스레드만의 스택 메모리가 만들어진다.

* 메서드 내에서 선언된 변수는 스택 프레임(Stack Frame)에 저장됨.

* 메서드가 실행을 마치면 스택 프레임이 사라지면서 해당 변수도 삭제됨.

    public void processUser() {
        String userData = "사용자 A"; // 지역 변수 (쓰레드마다 별도의 메모리 할당)
    }
    

 

즉, 지역 변수는 기본적으로 동시성 문제가 발생하지 않음.

 

📝 스택 메모리 / 힙 메모리

구분 스택(Stack) 메모리 힙(Heap) 메모리
메모리 할당 각 스레드마다 개별적인 공간이 할당됨 애플리케이션 전체에서 공유됨
저장 대상 지역 변수, 메서드 호출 스택 객체, 배열, static 변수
데이터 공유 각 스레드가 독립적으로 사용 (공유되지 않음) 모든 스레드가 공유 가능 (동기화 필요)
데이터 생명주기 메서드 실행이 끝나면 자동 제거 GC(Garbage Collector)가 관리 (자동 해제됨)
속도 빠름 (LIFO 구조, 직접 관리됨) 느림 (GC가 관리하며 비용 발생)
예제
public void method() {
    int x = 10; // 지역 변수 (스택 메모리)
}
                    
class Person {
    String name; // 힙 메모리에 저장
}
                    

3. 해결책: ThreadLocal을 활용하자 🔥

멀티스레드 환경에서 안전하게 데이터를 관리하려면 ThreadLocal을 사용하면 된다.

ThreadLocal은 쓰레드마다 독립적인 저장 공간을 제공하기 때문에 동시성 문제를 해결할 수 있다.

ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법) - 3. 해결책: ThreadLocal을 활용하자 🔥
ThreadLocal-1
ThreadLocal, 안 쓰면 큰일 나는 이유 (멀티스레드 동시성 문제 해결법) - 3. 해결책: ThreadLocal을 활용하자 🔥
ThreadLocal-2

🛠 ThreadLocal 사용 예제

    public class ThreadLocalExample {
        private static ThreadLocal threadLocalData = new ThreadLocal<>();

        public static void main(String[] args) {
            Thread t1 = new Thread(() -> {
                threadLocalData.set("Thread 1 데이터");
                System.out.println("Thread 1: " + threadLocalData.get());
            });

            Thread t2 = new Thread(() -> {
                threadLocalData.set("Thread 2 데이터");
                System.out.println("Thread 2: " + threadLocalData.get());
            });

            t1.start();
            t2.start();
        }
    }
    

위 코드를 실행하면 각 쓰레드가 독립적인 데이터를 저장하고 읽을 수 있다.


4. ThreadLocal의 동작 원리 🧐

ThreadLocal은 내부적으로 각 쓰레드의 고유한 공간을 사용해서 값을 저장한다.

즉, 각 쓰레드는 자신만의 데이터를 갖고, 다른 쓰레드와 공유되지 않는다.


5. ThreadLocal의 사용 후 반드시 remove() 해야 하는 이유 

쓰레드 풀(thread pool) 환경에서는 ThreadLocalremove()로 정리하지 않으면 메모리 누수가 발생할 수 있다.

💀 문제 상황

  • 쓰레드 풀에서 재사용된 쓰레드가 이전 데이터를 그대로 가지고 있을 수 있음
  • 잘못된 데이터가 다음 요청에도 영향을 줄 가능성이 있음
  • 메모리가 회수되지 않아서 점점 사용량이 증가할 수도 있음

✅ 해결 방법

    try {
        threadLocalData.set("사용자 데이터");
        // 로직 실행
    } finally {
        threadLocalData.remove(); // 반드시 제거!
    }
    

🎯 마무리

멀티스레드 환경에서 공유 자원을 안전하게 관리하려면 ThreadLocal을 적절히 활용해야 한다.

하지만, remove()를 반드시 호출해서 메모리 누수를 방지하는 것도 잊지 말도록..

💡 "멀티스레드 프로그래밍에서는 공유 자원이 문제의 핵심이다. 안전한 코드를 작성하자!"

 

'spring' 카테고리의 다른 글

Mockito  (0) 2025.04.07
싱글톤 Context에서 전략 패턴 사용 시 동시성 문제  (0) 2025.03.27
스프링 MVC 학습목록 (2)  (0) 2025.01.08
HandlerMapping & RequestMapping  (0) 2024.12.02
SpringMVC ArgumentResolver  (0) 2024.12.02

댓글