nathan_H

ThreadLocal 이란? 본문

Programming Laguage/Java

ThreadLocal 이란?

nathan_H 2020. 8. 29. 21:46

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을 사용하게 되면, 굳이 변수를 파라미터 같은 곳에 넣지 않아도 공유 및 사용이 가능하게 된다.
  • 그래서 앞서 언급한 SecurityContextHolderThreadLocal을 통해 파라미터로 Principal을 주입 받지 않더라도, 현재 SecurityContextHolderThreadLocal로 저장된 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 메소드를 활용하며ThreadThreadLocalMap 타입의 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에 적용 하다보면 유용하게 사용할 수도 있을 거 같다.

참고


Comments