싱글톤 컨테이너를 할때, 무상태 설계와 더불어 해결방법으로 ThreadLocal을 사용하라고 했다.
강의에서 자세한 내용은 알려주지 않아. 따로 찾아보고 정리를 해본다.
우선 이걸 왜쓰냐?
만약 public class orderService{
private int price;
}
만약 userA가 주문을 했을때 이 주문금액을 필드에 저장해놓는다면,
orderService가 싱글톤 컨테이너에 있다면 이건 공유를 하게되는 인스턴스이고,
이상태에서 만약 userB가 주문을 다시한다면 userA의 price가 덮어씌어지는 현상을 겪게 될것이다.
이를 방지하기 위해 threadLocal은 각각의 쓰레드 별로 별도의 저장공간을 제공하는 컨테이너이다.
참고로 threadLocal이 사용되는 환경은 해당 컨테이너를 가지고있는 서비스가 싱글톤 객체로 공유되는 객체이다. 이러면 각 스레드는 동일한 서비스객체=>orderService에 접근하게 되고, 동일한 threadLocal에 접근한다.
ThreadLocal을 사용하지 않는경우
public class ServiceA {
//사용자 인증 정보
private Authentication authentication;
private UserRepository userRepository;
private final ServiceA instance = new ServiceA();
public static ServiceA getInstance(){
return instance;
}
public boolean login(LoginForm form) {
User user = userRepository.findById(form.getId()).orElseThrow(NoSuchException::new);
if(PasswordEncoder.matches(user.getPassword(), form.getPassword())){
authentication = Authentication.of(form.getId(), form.getPassword, ...);
}
}
public boolean hasPrincipal(){
return !authentication == null;
}
}
이러한 상태에서 만약 userA가 로그인한다고 치자, 그러면 Service에서 getId,getPassword로 Authenticaion.of로 확인을 할것이고 만약 userA가 있다면
userA의 아이디와 password가 authentication에 들어가고, hasPrincipal이 null이 아니게 될것이다.
문제는, 그다음 userB가 요청을 하는 것이다.
서비스에서는 hasPrincipal()메서드를 통해서 인증정보가 있는지 확인을 하는데, Thread A를 통해 저장된 인증정보가 있기에 Thread B의 요청이 접근 허가 되는것이다.
이 이유가 무었이냐? 바로 ServiceA class가 싱글톤 컨테이너에 등록되어 공유된 인스턴스를 사용한다. 이말은 여러 스레드가 이 인스턴스 포함 내부 자원들도 전부 공유하고 있기 때문에, 다른 쓰레드의 자원을 모두가 공유하게 되는것이다.
그래서 여기서 필드로 private Authetication authentication 설정해놓은것도, userA가 인증을 해놨지만, userB도 인증된 내부 자원까지 전부 쓰는것이다.
그래서 각각의 쓰레드가 공통된 객체 내부에서도 다른 상태를 가지길 바래서 나온 개념이 ThreadLocal이다.
그러면 이제 어떻게 ThreadLocal이 각각 쓰레드 별로 변수를 할당해서 사용하게 할까?
그림으로 봐보자,
각 쓰레드 객체별로 threadLocals라는 인스턴스 변수를 가지고 있다.
그리고 이 threadLocals를 이용해 threadLocal 내부의 ThreadLocalMap이라는 클래스로 key/value형식으로 데이터를 보관한다.
그리고 threadLocal의 get,set메서드도 결국, 현재 수행중인 thread를 currentTread()메서드로 꺼낸다음에
이 Thread에서 threadLocalMap을 찾아 활용하는 것이었다.
그렇다면 LocalThread의 대표적인 사용처 하나만 알아보자
Spring Security
스프링 시큐리티의 핵심은 SecurityContextHolder, SecurityContext, 그리고 Authentication 객체에 있다.
여기서 SecurityContextHolder는 SecurityContext를 저장한다.
이 SecurityCOntext안에는 Authentication이 들어있어 사용자의 정보를 담고있다.
이 SecurityContextHolder에는 SecurityContext를 저장하는 컨테이너 역할을 하는데 저장 방식을 MODE_THREADLOCAL 전략으로 저장을 한다.
이 전략은 스프링 시큐리티가 SerurityCOntext를 저장하는 방식인데, 이방식은 ThreadLocal를 사용하여 각 쓰레드마다 별도의 SecurityContext를 보유하게 한다.
아까 위의 사진처럼 각 쓰레드별로 threadLocalMap을 사용하여 ID,비번을 저장해놓는다.
이를 통해 동시에 처리되는 여러 요청 간에 인증정보가 서로 영향을 주지 않게 한다.
주의점
WAS는 API를 요청하고 반환하면서 Thread를 사용한 뒤에도 Thread를 종료시키지 않는다.
이말이 뭐냐 만약 일정수 200개라고 가정하면 이 200개의 Thread를 생성해 스레드 풀로 관리하다가, 클라이언트의 요청마다 이 스레드 풀에서 여유 Thread를 발급하여 사용하고 반환받아서 다시 관리하는 식으로 사용을 한다.
그렇기에 이전 사용자가 Thread를 사용하며 ThreadLocal에 데이터를 저장해 놓았을때, 다른 사용자가 이 Thread를 받게 되면 안의 내용물을 위/변조 할수 있다.
그렇기에 Thread의 사용이 끝나는 시점에 ThreadPool에 반환하기 전에 반드시 ThreadLocal을 초기화 시켜줘야한다.
스프링 시큐리티에서는 clearContext()라는 메서드를 통해 정보를 초기화해주고
RequestContextHolder에서는 resetRequestAttributes()라는 메서드로 초기화해준다.