![[Spring 핵심원리 - 고급] 쓰레드 로컬(Thread Local)](https://img1.daumcdn.net/thumb/R750x0/?scode=mtistory2&fname=https%3A%2F%2Fblog.kakaocdn.net%2Fdn%2FbrYFzu%2FbtsNfwC8WZm%2FsQKvnhxZIanRKLRAa3zztK%2Fimg.png)
[Spring 핵심원리 - 고급] 쓰레드 로컬(Thread Local)Back-End/Spring2025. 4. 9. 15:36
Table of Contents
이 글은 인프런 김영한님의 Spring 강의를 바탕으로 개인적인 정리를 위해 작성한 글입니다.
동시성 이슈
- 동시성 이슈란, 하나의 자원에 여러 스레드가 동시에 접근하여 값을 수정하거나 조회하는 과정에서, 개발자가 의도한 대로 동작하지 않는 현상을 말한다.
- 스프링에서 빈은 기본적으로 싱글톤 스코프이므로, 여러 요청에서 동일한 인스턴스를 공유하게 된다.
- 이때, 싱글톤 빈 내부에 상태를 저장하는 필드가 있다면, 동시에 여러 요청이 들어올 경우 서로의 요청 데이터가 충돌하면서 동시성 이슈가 발생할 수 있다.
- 이러한 상황에서는 ThreadLocal을 사용하여 각 쓰레드마다 독립적인 저장 공간을 제공함으로써 문제를 해결할 수 있다.
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class FieldService {
private String nameStore;
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore);
nameStore = name;
sleep(1000);
log.info("조회 nameStore={}", nameStore);
return nameStore;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
import hello.advanced.trace.threadlocal.code.FieldService;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
@Slf4j
public class FieldServiceTest {
private FieldService fieldService = new FieldService();
@Test
void field() {
log.info("main start");
Runnable userA = () -> {
fieldService.logic("userA");
};
Runnable userB = () -> {
fieldService.logic("userB");
};
Thread threadA = new Thread(userA);
threadA.setName("thread-A");
Thread threadB = new Thread(userB);
threadB.setName("thread-B");
threadA.start();
// sleep(2000); //동시성 문제 발생X
sleep(100); //동시성 문제 발생O
threadB.start();
sleep(3000); //메인 쓰레드 종료 대기
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
위 예제에서 스레드 A가 작업을 시작하고 약간의 시간 뒤에 스레드 B가 실행되면 동시성 이슈가 발생하지 않지만, 짧은 시간 뒤에 스레드 B가 실행되면 동시성 이슈가 발생한다.
이는 fieldService가 싱글톤 객체이기 때문이다.
따라서 필드를 공통으로 여러 스레드가 사용하는 경우 동시성 이슈가 발생한다.
참고
- 이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다.
- 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다.
- 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생한다.
스레드 로컬
동시성 이슈 해결
스레드 로컬을 사용하면 각 스레드마다 별도의 내부 저장소를 제공한다.
다라서 같은 인스턴스의 스레드 로컬 필드에 접근해도 문제 없다.
- thread-A 가 userA 라는 값을 저장하면 쓰레드 로컬은 thread-A 전용 보관소에 데이터를 안전하게 보관한다.
- thread-B 가 userB 라는 값을 저장하면 쓰레드 로컬은 thread-B 전용 보관소에 데이터를 안전하게 보관한다.
- 쓰레드 로컬을 통해서 데이터를 조회할 때도 thread-A 가 조회하면 쓰레드 로컬은 thread-A 전용 보관소에서 userA 데이터를 반환해준다.
- 물론 thread-B 가 조회하면 thread-B 전용 보관소에서 userB 데이터를 반환해준다.
- 자바는 언어차원에서 쓰레드 로컬을 지원하기 위한 java.lang.ThreadLocal 클래스를 제공한다.
스레드 로컬을 적용한 코드는 아래와 같다.
@Slf4j
public class ThreadLocalService {
private ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore.get());
nameStore.set(name);
sleep(1000);
log.info("조회 nameStore={}", nameStore.get());
return nameStore.get();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
기존에 있던 FieldService 와 거의 같은 코드인데, nameStore 필드가 일반 String 타입에서 ThreadLocal 을 사용하도록 변경되었다.
ThreadLocal 사용법
- 값 저장: ThreadLocal.set(xxx)
- 값 조회: ThreadLocal.get()
- 값 제거: ThreadLocal.remove()
해당 쓰레드가 쓰레드 로컬을 모두 사용하고 나면 ThreadLocal.remove() 를 호출해서 쓰레드 로컬에 저장 된 값을 제거해주어야 한다.
주의 사항
스레드 로컬을 사용하고 나서 ThreadLocal.remove()를 호출하지 않으면 심각한 문제가 발생할 수 있다.
메모리 누수
- ThreadLocal 객체는 GC 대상이 될 수 있지만, 그와 연결된 값(Value)은 강한 참조로 남아 있다.
- 우리가 ThreadLocal.remove()를 호출하지 않으면, GC가 ThreadLocal을 수거한 후에도 값은 계속 메모리에 남아 있게 된다.
- 즉, 삭제되지 않은 값이 해당 쓰레드의 생명 주기 동안 계속 메모리를 차지하게 된다.
잘못된 데이터 공유
스레드 로컬을 사용하면 각 스레드마다 별도의 내부 저장소를 제공한다.
이 말은 스레드 풀을 사용하는 환경에서 다른 요청, 같은 스레드를 사용한다면 잘못된 정보가 공유될 수 있다는 뜻이다.
- 톰캣 등 대부분의 WAS는 요청당 스레드를 새로 생성하는 것이 아니라, 재사용한다. (스레드풀 구조)
- 이때 이전 요청에서 ThreadLocal에 저장한 값이 삭제되지 않으면, 다음 요청에도 남아 있게 된다.
- 요청 A → userId를 ThreadLocal에 저장
- 요청 A 처리 완료 → remove() 호출 안 함
- 같은 스레드에서 요청 B 처리 시작
- 요청 B에서 ThreadLocal.get() 호출 → 요청 A의 사용자 정보가 반환됨
'Back-End > Spring' 카테고리의 다른 글
[Spring 핵심원리 - 고급] 전략 패턴(디자인 패턴) (1) | 2025.04.10 |
---|---|
[Spring 핵심원리 - 고급] 템플릿 메서드 패턴(디자인 패턴) (0) | 2025.04.10 |
[Spring] 스프링 컨테이너(IoC, DI 컨테이너) (0) | 2025.04.02 |
[Spring] Spring Boot + Swagger (0) | 2025.03.28 |
[Spring] GeoIP를 이용한 해외 IP 차단 (0) | 2025.01.16 |