어제 우아한 형제들 루터회관 14층에서 SLiPP 스터디 중간 세미나가 있었고, 우아한 테크 코스 크루 자격으로 참석할 수 있었다. 첫번째 세션에서 김문수님의 게임 재화 및 재화 로그 시스템 개발기
를 들었는데, ThreadLocal이란 용어를 처음 접하게 되었다. 궁금증이 생겨서 찾아보고 이해한 결과를 정리하고, 구현해본 코드를 공유해보려고 한다.
오라클 Docs 에서 ThreadLocal 클래스를 다음과 같이 설명한다.
ThreadLocal 클래스는 thread-local 변수들을 제공한다. 이 변수들은 get 또는 set 메소드를 통해 접근하는 각 스레드가 독립적으로 변수의 초기화 된 사본을 가지고 있다는 점에서 다르다. ThreadLocal 인스턴스들은 보통 스레드와 상태를 연결하려고 하는 클래스들의 private static 필드들이다. (예를들어, 유저 ID 또는 트랜잭션 ID)
좀 더 쉽게 말하자면, ThreadLocal 변수를 선언하면 멀티 스레드 환경에서 각 스레드마다 독립적인 변수를 가지고, get(), set() 메소드를 통해 값에 대해 접근할 수 있다. 간단한 코드로 예를 들어보자.
private static final ThreadLocal<Integer> threadLocalId = new ThreadLocal<>();
위와 같이 threadLocalId 변수를 선언하고, threadLocalId.set(1)
으로 값을 지정해주면, 지정해준 스레드 안에서 threadLocalId.get()
을 호출했을 때 지정해준 값인 1을 리턴한다.
각 스레드는 스레드가 종료되기 전까지 ThreadLocal 인스턴스를 참조한다.
우아한 테크 코스 레벨 3의 첫번째 미션인 WAS 구현 미션 코드의 일부로 ThreadLocal의 예를 들겠다. Session의 생성, 저장, 조회, 삭제를 담당하는 SessionManager 클래스가 있다.
public class SessionManager {
private static Map<String, HttpSession> sessionMap = new HashMap<>();
public static HttpSession createEmptySession() {
return new HttpSession();
}
public static String addSession(HttpSession session) {
sessionMap.put(session.getId(), session);
return session.getId();
}
public static HttpSession findSession(String key) {
return sessionMap.get(key);
}
public static void deleteSession(String key) {
sessionMap.remove(key);
}
}
만약 코드의 여러 부분에서 세션을 사용해야한다고 할 때 기존의 코드에서는 매번 다음과 같은 과정을 통해 세션을 불러와야 한다.
이 방법의 문제는 세션을 가져오기 위해서는 HttpRequest을 항상 필요로 하고, 세션을 관리하는 Map에서 불필요하게 반복하여 검색하는 과정을 거친다.
하지만 ThreadLocal을 사용하면 처음에 요청이 들어올 때 인스턴스에 세션을 할당해주기만 하면 세션을 인스턴스로부터 계속 꺼내쓸 수 있다.
public class SessionThreadLocal {
private static final ThreadLocal<HttpSession> threadLocal;
static {
threadLocal = new ThreadLocal<>();
}
public static void set(HttpSession session) {
threadLocal.set(session);
}
public static void unset() {
threadLocal.remove();
}
public static HttpSession get() {
return threadLocal.get();
}
}
public class SessionInitiator {
public void handle(HttpRequest request, HttpResponse response) {
if (request.getHeader(COOKIE) == null || request.getSessionId() == null) {
HttpSession session = SessionManager.createEmptySession();
SessionManager.addSession(session);
Cookie cookie = new Cookie("SESSIONID", session.getId());
response.addCookie(cookie);
SessionThreadLocal.set(session);
return;
}
HttpSession session = SessionManager.findSession(request.getSessionId());
SessionThreadLocal.set(session);
}
}
public class LoginController extends AbstractController {
@Override
public View doPost(HttpRequest httpRequest, HttpResponse httpResponse) {
User user = DataBase.findUserById(httpRequest.getRequestBody("userId")).orElse(null);
if (user != null && user.matchPassword(httpRequest.getRequestBody("password"))) {
HttpSession session = SessionThreadLocal.get();
session.setAttributes("user", user);
Cookie cookie = new Cookie("logined", "true");
httpResponse.addCookie(cookie);
return new RedirectView("index.html");
}
Cookie cookie = new Cookie("logined", "false");
httpResponse.addCookie(cookie);
return new RedirectView("user/login_failed.html");
}
}
public void doDispatch(HttpRequest httpRequest, HttpResponse httpResponse) throws IOException {
try {
if (httpRequest.isStaticRequest()) {
handleStaticRequest(httpRequest, httpResponse);
return;
}
Controller controller = HandlerMapping.handle(httpRequest);
View view = controller.service(httpRequest, httpResponse);
view.render(httpRequest, httpResponse);
} catch (AbstractHttpException e) {
log.error(e.getMessage());
httpResponse.sendError(e);
} catch (URISyntaxException e) {
log.error(e.getMessage());
} finally {
SessionThreadLocal.unset();
}
}
만약 ThreadPool을 사용할 때, ThreadLocal 인스턴스를 초기화해주지 않으면 어떻게 될까?
간단한 테스트를 해보겠다.
public class ExecutorsTest {
private static final Logger logger = LoggerFactory.getLogger(ExecutorsTest.class);
private static AtomicInteger counter = new AtomicInteger(0);
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws Exception {
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
es.execute(() -> {
int idx = counter.addAndGet(1);
threadLocal.set(idx);
logger.info("Thread {}", threadLocal.get());
});
}
try {
Thread.sleep(1000);
System.out.println();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (int i = 0; i < 15; i++) {
es.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("Thread {}", threadLocal.get());
});
}
es.shutdown();
es.awaitTermination(100, TimeUnit.SECONDS);
}
}
스레드 5개를 가지고 있는 스레드 풀을 만든 후, 1에서 5까지 각각의 번호를 각 스레드의 ThreadLocal 인스턴스에 할당해줬다. 1초를 기다리고 나서 스레드에게 가지고 있는 ThreadLocal 값을 출력하는 코드이다. 결과값은 다음과 같이 나왔다.
19:27:34.824 [INFO ] [pool-1-thread-1] [webserver.ExecutorsTest] - Thread 1
19:27:34.824 [INFO ] [pool-1-thread-4] [webserver.ExecutorsTest] - Thread 4
19:27:34.824 [INFO ] [pool-1-thread-3] [webserver.ExecutorsTest] - Thread 3
19:27:34.824 [INFO ] [pool-1-thread-2] [webserver.ExecutorsTest] - Thread 2
19:27:34.824 [INFO ] [pool-1-thread-5] [webserver.ExecutorsTest] - Thread 5
19:27:36.828 [INFO ] [pool-1-thread-2] [webserver.ExecutorsTest] - Thread 2
19:27:36.828 [INFO ] [pool-1-thread-3] [webserver.ExecutorsTest] - Thread 3
19:27:36.828 [INFO ] [pool-1-thread-5] [webserver.ExecutorsTest] - Thread 5
19:27:36.828 [INFO ] [pool-1-thread-1] [webserver.ExecutorsTest] - Thread 1
19:27:36.828 [INFO ] [pool-1-thread-4] [webserver.ExecutorsTest] - Thread 4
19:27:37.829 [INFO ] [pool-1-thread-4] [webserver.ExecutorsTest] - Thread 4
19:27:37.829 [INFO ] [pool-1-thread-2] [webserver.ExecutorsTest] - Thread 2
19:27:37.829 [INFO ] [pool-1-thread-3] [webserver.ExecutorsTest] - Thread 3
19:27:37.829 [INFO ] [pool-1-thread-5] [webserver.ExecutorsTest] - Thread 5
19:27:37.829 [INFO ] [pool-1-thread-1] [webserver.ExecutorsTest] - Thread 1
19:27:38.830 [INFO ] [pool-1-thread-1] [webserver.ExecutorsTest] - Thread 1
19:27:38.831 [INFO ] [pool-1-thread-2] [webserver.ExecutorsTest] - Thread 2
19:27:38.831 [INFO ] [pool-1-thread-5] [webserver.ExecutorsTest] - Thread 5
19:27:38.830 [INFO ] [pool-1-thread-3] [webserver.ExecutorsTest] - Thread 3
19:27:38.830 [INFO ] [pool-1-thread-4] [webserver.ExecutorsTest] - Thread 4
각 스레드가 종료되지 않고, 사용되기 때문에 처음에 할당해준 값들이 반복해서 출력된다.