자바에서 싱글톤 패턴과 static 변수를 사용할 때, 멀티스레드 환경에서는 동시성 문제가 발생할 수 있다.
특히 웹 애플리케이션에서 이런 문제가 생기면 예상치 못한 버그나 데이터 오염이 일어날 수도 있다.
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가 관리하며 비용 발생) |
예제 |
|
|
3. 해결책: ThreadLocal을 활용하자 🔥
멀티스레드 환경에서 안전하게 데이터를 관리하려면 ThreadLocal
을 사용하면 된다.
ThreadLocal은 쓰레드마다 독립적인 저장 공간을 제공하기 때문에 동시성 문제를 해결할 수 있다.


🛠 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) 환경에서는 ThreadLocal
을 remove()로 정리하지 않으면 메모리 누수가 발생할 수 있다.
💀 문제 상황
- 쓰레드 풀에서 재사용된 쓰레드가 이전 데이터를 그대로 가지고 있을 수 있음
- 잘못된 데이터가 다음 요청에도 영향을 줄 가능성이 있음
- 메모리가 회수되지 않아서 점점 사용량이 증가할 수도 있음
✅ 해결 방법
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 |
댓글