Spring ‐ Thread Local(쓰레드 로컬) - dnwls16071/Backend_Study_TIL GitHub Wiki
- 여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제
- 동시성 문제는 여러 쓰레드가 같은 인스턴스 필드에 접근하기 때문에 트래픽이 적은 상황에서는 확률상 잘 나타나지 않고, 트래픽이 점점 많아질수록 자주 발생한다.
- 스프링 빈과 같이 싱글톤 컨테이너로 관리되는 경우 동시성 문제를 조심해야 한다.
- 쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다.
@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();
}
}
}
- 자바는 언어 차원에서 쓰레드 로컬을 지원하기 위한 ThreadLocal 클래스를 제공한다.
- 해당 쓰레드가 쓰레드 로컬을 사용하고 나면 반드시 쓰레드 로컬에 저장된 값을 제거해주어야 한다.
- 동시성 문제가 발생하는 이유
- 여러 쓰레드가 힙 영역에 있는 인스턴스의 필드에 접근(싱글톤 객체)
- 클래스 영역에 static 으로 붙은 공용 필드에 접근
- 스택 영역은 쓰레드 별로 생성이 되기 때문에 지역 변수에서는 동시성 문제가 발생하지 않는다.
❗쓰레드 로컬 주의사항
- 쓰레드 로컬의 값을 사용 후 제거하지 않으면 WAS처럼 쓰레드 풀을 사용하는 경우 심각한 문제가 발생한다.
- 사용자가 저장 HTTP를 요청한다.
- WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
- 쓰레드가 할당된다.
- 쓰레드는 사용자A의 데이터를 쓰레드 로컬에 저장한다.
- 쓰레드 로컬의 전용 보관소에 사용자A의 데이터를 보관한다.
- 사용자A의 HTTP 요청이 끝난다.
- WAS는 사용이 끝난 쓰레드를 쓰레드 풀에 반납한다. 쓰레드를 생성하는 비용은 비싸기 때문에 제거하지 않고 재사용한다.
- 사용자A의 쓰레드는 결국 쓰레드 풀에 살아있다. 따라서 쓰레드 로컬에 있는 사용자A가 사용한 쓰레드의 데이터 역시 살아있게 된다.
- 사용자B가 조회를 위한 새로운 HTTP 요청을 한다.
- WAS는 쓰레드 풀에서 쓰레드를 하나 조회한다.
- 사용자B에게 사용자A가 사용했던 쓰레드가 할당된다.(항상 그렇다는 것은 아니다. 물론 다른 쓰레드가 할당될 수 있다.)
- 쓰레드는 쓰레드 로컬에서 데이터를 조회한다.
- 쓰레드는 쓰레드 로컬 전용 보관소에 있는 사용자A 관련 값을 반환한다.
- 결과적으로 사용자A와 관련한 값이 반환된다.
- 사용자B는 사용자A의 정보를 조회하게 된다.
❗Spring Security 프레임워크에서 사용하는 어노테이션인 @AuthenticationPrincipal 사용
- 요청이 들어오면 JWT 토큰을 검증하고 Authentication 객체를 생성한다.
- SecurityContextHolder.getContext()에 Authentication을 저장하는데 이 때, 쓰레드 로컬을 사용한다.
- @AuthenticationPrincipal을 통해 컨트롤러에서 Principal에 접근한다.
- 스프링 시큐리티의 인증 / 인가 처리는 여러 필터 체인으로 구성이 되는데 그 중 FilterChainProxy 클래스의 코드를 보면 위와 같다.
- 여기서 요청 처리 후 clearContext()를 통해 쓰레드 로컬의 값을 자동으로 클리어하기 때문에 안전하게 사용할 수 있는 것이다.