Notice
Recent Posts
Recent Comments
Link
nathan_H
ThreadLocal 이란? 본문
Intro
- Spring Security를 사용하다 보면, 인증이 된 유저는
UsetDetailsService
에서 리턴을 한 후UserDetails
타입의 객체가 바로Principal
로 변환되어 사용하게 된다. 즉 인증이 된 유저는 Principal이 되고 인증이 필요한 로직에 사용되게 된다.- 가령 아래와 같은 코드에서 현재 인증이 처리된 유저의 클라이언트 등록 목록을 조회하는URI 요청이 들어올 시 파라미터로 현재 인증이 처리 된 Principal이 들어오게 되면서 해당 Principal의 name (username)으로 클라이언트 등록 조회 목록을 조회할 수 있게 된다.
@GetMapping("/info")
public ModelAndView registrationInfo(ModelAndView mv, Principal principal) {
String username = principal.getName();
List<ClientInfoDto> clients = registrationClientInfoService.getRegistrationInfo(username);
mv.addObject("clientInfo", clients);
mv.setViewName("client/info");
return mv;
}
- 또한 파라미터로 Pricipal 객체를 받지 않더라도
SecurityContextHolder
을 활용해 현재 인증이 처리된 유저의 username을 꺼내 올 수 있게 된다.
SecurityContextHolder.getContext().getAuthentication().getName();
- 인증된 유저에 대한 데이터 혹은 객체를 사용하기 위해서는 메소드의 파라미터 혹은 클래스의 변수를 통해 가져오게 되는데
SecurityContextHolder
은 과연 어떻게 현재 인증된 유저에 대한 정보를 계속 가지고 있게 되는 것일까??** - 그 이유는 바로
SecurityContextHolder
기본적으로ThreadLocal
전략을 사용하고 있기 때문이다.**
Thread Local
- ThreadLocal은 thread-local 변수를 제공하는 클래스이다.
- 여기서 thread-local 변수란 말그대로 thread 내부에서 사용되는 지역변수를 의미한다.
- 가령 메소드에서 사용하는 변수나 데이터는 파라미터 혹은 메소드 scope 내에서 정의하고 사용하게 되는데, Thread Local을 사용하게 되면, 굳이 변수를 파라미터 같은 곳에 넣지 않아도 공유 및 사용이 가능하게 된다.
- 그래서 앞서 언급한
SecurityContextHolder
은 ThreadLocal을 통해 파라미터로 Principal을 주입 받지 않더라도, 현재SecurityContextHolder
에ThreadLocal
로 저장된Principal
을 꺼내와 사용할 수 있게 되는 것이다.
ThreadLocal 클래스
ThreadLocal 클래스는 thread-local 변수들을 제공한다.
이 변수들은 get 또는 set 메소드를 통해 접근하는 각 스레드가 독립적으로 변수의 초기화 된 사본을 가지고 있다는 점에서 다르다. ThreadLocal 인스턴스들은 보통 스레드와 상태를 연결하려고 하는 클래스들의 private static 필드들이다.
(예를 들어, 유저 ID 또는 트랜잭션 ID)
사용법
- 직접 인증된 유저를 ThreadLocal에 저장해 사용해보자.
// UserAccount를 ThreadLocal에 저장해 사용하는 Custom한 UserContext
public class UserContext {
// UserAccount를 담는 ThreadLocal 정의
private static ThreadLocal<UserAccount> THREAD_LOCAL_ACCOUNT = new ThreadLocal<UserAccount>();
public static UserAccount getUserAccount() {
return THREAD_LOCAL_ACCOUNT.get();
}
// 인증된 유저를 ThreadLocal에 담기.
public static void setUserAccount(UserAccount userAccount) {
THREAD_LOCAL_ACCOUNT.set(userAccount);;
}
}
- ThreadLocal에 userAccount 객체 저장
- 인증된 username으로 userAccount를 조회해 UserContext에 저장.
@GetMapping("/info")
public ModelAndView registrationInfo(ModelAndView mv, Principal principal) {
String username = principal.getName();
UserContext.setUserAccount(userAccountRepository.findByUsername(username));
List<ClientInfoDto> clients = registrationClientInfoService.getRegistrationInfo(username);
mv.addObject("clientInfo", clients);
mv.setViewName("client/info");
return mv;
}
- ThreadLocal에 저장된 userAccount 객체 사용
@PostMapping("/info/delete")
public ModelAndView registrationInfo(ModelAndView mv) {
UserAccount user = UserContext.getUserAccount();
registrationClientInfoService.delete(user.getUsername);
mv.setViewName("client/info");
return mv;
}
- 등록된 client 조회와 같이 Principal을 따로 메소드 파라미터로 받지 않더라도 현재 UserContext에 저장된 인증 처리된 UserAccount를 가져와 사용할 수 있음.
ThreadLocal 좀 더 알아보기
- ThreadLocal 동작 방식을 보기 위해 ThreadLocal 내부를 들어가 보자.
ThreadLocal 일부를 가져온 코드
public class ThreadLocal {
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
}
ThreadLocal
의 값을 설정하거나 조회할 때는 각각 set, get 메소드를 활용하며 각Thread
는ThreadLocalMap
타입의threadLocals
라는 필드를 가지고 있다.ThreadLocalMap
에는Entry
타입 배열인 table이,Entry
에는ThreadLocal
과 우리가 저장 하고자하는 값이 포함되어 있다.- 여기서
threadLocals
는 여러Thread
에서도 독립적으로 존재할 수 있다. - 또한
**threadLocals
의 요소들은ThreadLocal
로 구분할 수 있기 때문에 하나의Thread
안에 여러ThreadLocal
을 정의해도 할 수 있다. (참고로, threadLocals의 크기보다 많은 ThreadLocal을 정의하거나 해싱 값(i)이 겹치면 문제가 된다고 한다.)** - ThreadLocal 사용 주의 사항
- ThreadLocal은 static으로 정의를 해야 한다.
- 가령, 아래와 같이 static으로 정의하지 않으면 하나의 Thread안에서 값이 다르게 나오게 된다.
public class NathanThreadLocal {
private ThreadLocal<Double> local = ThreadLocal.withInitial(Math::random);
public Double get() {
return local.get();
}
}
class Main {
public static void main(String[] args) {
new Thread(() -> {
ThreadLocalTest t1 = new ThreadLocalTest();
System.out.println("first Object " + t1.get());
ThreadLocalTest t2 = new ThreadLocalTest();
System.out.println("second object " + t2.get());
}).start();
}
}
결과
first Object 0.28367256981460687
second object 0.625707046878209
- 그 이유는 앞서 언급했듯이
**threadLocals
의 값들은ThreadLocal
객체로 구분되는데 위의 코드에서는NathanThreadLocal
객체가 생성될 때마다ThreadLocal
객체가 생성**되고,Thread
는 둘을 각각threadLocals
에 저장하기 때문이다.
마무리
- 주로 ThreadLocal은 서버에서 클라이언트 요청들에 대해 각 쓰레드에서 처리하게 될 경우, 해당 유저의 인증 및 세션 정보나 참조 데이터를 저장하는 데 사용한다고 한다. 초반에 언급한 Spring Security가 그 중 하나이다.
- 그 외에도 Spring 등에서는
Interceptor
를 이용해서ThreadLocal
의 작업을 제어하는 경우가 많다고 한다. - 경험이 좀 쌓이고, 다양한 Use Case에 적용 하다보면 유용하게 사용할 수도 있을 거 같다.
참고
'Programming Laguage > Java' 카테고리의 다른 글
인터페이스의 개념과 추상 클래스와의 차이점 (0) | 2020.06.02 |
---|---|
[Java] 바이트코드 조작 (0) | 2020.05.08 |
[Java] Java 애플리케이션에서 JVM 구조와 실행 과정 (0) | 2020.05.05 |
[Java] 상속 핵심 개념과 추상 클래스 (0) | 2020.05.01 |
[Java] 빠르게 정리하는 자바 클래스 (0) | 2020.04.28 |
Comments