본문 바로가기

Programming Language/Java

[Java] ThreadLocal에 대해서.... 그리고 주의점

ThreadLocal은 Java에서 각 쓰레드별로 독립적으로 변수를 관리하기 위한 객체 이다. 

 

Java의 쓰레드 class 정의를 보면 아래와 같이 쓰레드 별로 ThreadLocals 변수를 가지고 있는것을 볼 수 있다.

java.lang.Thread

public class Thread implements Runnable {
    /* Make sure registerNatives is the first thing <clinit> does. */
    private static native void registerNatives();
    static {
        registerNatives();
    }

...

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

 

 

ThreadLocal의 Get 메소드를 잠시 살펴보면 현재 currentThread 객체를 가져와서 해당 객체에 대한 값을 끄집어 내게 되어있다. 쓰레드별로 key를 관리하면서 독립적인 변수 설정이 되는 원리이다.

java.lang.Thread

public class ThreadLocal<T> {

   ...

    /**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    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();
    }

 

 

사용법은 다음과 같다 ContextHolder 객체를 싱글턴으로 관리하면서 사용하면 된다.

/**
 *  description : Context Holder
 */
public class ThreadLocalContextHolder {

    private static final ThreadLocal<ThreadLocalContext> contextHolder = new ThreadLocal<>();

    /**
     * @name : setContext
     * @param : context
     * @description : ThreadLocalContextHolder를 셋팅한다.
     */
    public static void setContext(ThreadLocalContext context) {
        contextHolder.set(context);
    }

    /**
     * @name : getContext
     * @return : ThreadLocalContextHolder
     * @description : ThreadLocalContextHolder를 얻는다.
     */
    public static ThreadLocalContext getContext() {
        ThreadLocalContext context = contextHolder.get();
        if (context == null) {
            context = new ThreadLocalContext();
            contextHolder.set(context);
        }
        return context;
    }

    /**
     * @name : clearContext
     * @description : context를 clear한다.
     */
    public static void clearContext() {
        contextHolder.remove();
    }
}

 

ThreadLocal 사용시 주의점

 

사용법은 간단하지만 꼭 주의해야할 점이 있다. ThreadLocalContext를 사용후에는 반납시 자동으로 값이 초기화되지 않기 때문에 Thread Pool 처럼 전체 Pool안에서 재활용하며 application이 구동될 경우 신규 context 생성 시 이전 쓰레기값이 채워진채로 재사용될 수가 있다. 

 

ThreadLocal 해제시 clear를 안했을 경우에 어떻게 되는지 쉽게 확인해 볼 수 있다.

 

우선 재현을 쉽게 하기 위해 아래와 같이 Spring boot로 백엔드 구동시 tomcat의 threads max를 작게 설정한다. (아래는 3개로 설정)

src/main/resources/application-local_auth.yml

server:
  port: 8091
  address: xxx.30.1.30
  servlet:
    session:
      tracking-modes: cookie
      cookie:
        secure: true
  nfs: c:/Workspace/nfs
  tomcat:
    basedir: c:/Workspace/tomcat
    threads:
      max: 3
      min-spare: 3

 

 

간단하게 위 ThreadLocal 변수를 생성할 Contoller api를 만들어본다.

   @RequestMapping(value = "/test/session", method = RequestMethod.POST, produces = "application/json")
   public ResponseEntity<ResultDVO> createTestSession(HttpServletRequest request) throws Exception{

        HttpSession session = request.getSession(true);
        session.setAttribute("TEST_DATA", System.currentTimeMillis());

        ThreadLocalContext context = ThreadLocalContextHolder.getContext();
        UserDVO user = new UserDVO();
        user.setLoginId("test");
        user.setUserId("test");
        context.setUser(user);

        return new ResponseEntity<>(new ResultDVO(), HttpStatus.OK);
    }

 

Swagger-ui나 Postman 등의 Tool로 해당 API를 연속 호출해 본다. Thread pool이 3개 이므로 3번까지는 context에 값이 null인채로 할당이 된다. 그러나 4번째부터는  쓰레드 생성시 context에 이전 쓰레기 값이 채워져서 리턴되는것을 볼 수 있다.

3번째 호출까지는 내부 값이 null로 설정되어 있다.

 

4번째 호출부터는 이전에 쓰던값 그대로 리턴이된다.

 

ThreadLocal 변수 clear 방법

 

변수 clear 방법은 간단한데 interceptor의 afterCompletion 메소드를 override하고 그안에서 clearContext()를 불러주어 해당 Thread에서 할당한 값들을 해제하여 주면 된다.

 

public class CommInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        ThreadLocalContextHolder.clearContext();
    }
}

 

위와 같이 처리를 한 후에는 api를 계속 호출해봐도 쓰레기 값이 채워져서 return되지 않음을 볼 수 있다.

 

-- The End --