해당 포스팅은 인프런에서 제공하는 김영한 님의 '스프링 핵심원리 고급편'을 수강한 후 정리한 글입니다. 유료 강의를 정리한 내용이기에 제공되는 예제나 몇몇 내용들은 제외하였고, 정리한 내용을 바탕으로 글 작성자인 저의 언어로 다시 작성한 글이기에 서술이 부족하거나 잘못된 내용이 있을 수 있습니다. 그렇기에 해당 글은 개념에 대한 참고 정도만 해주시고, 강의를 통해 학습하시기를 추천합니다.
여러 쓰레드가 동시에 같은 인스턴스의 필드 값을 변경하면서 발생하는 문제를 동시성 문제라고 하며 쓰레드가 많아질수록, 즉 트래픽이 늘어날수록 자주 발생하게 된다. 지역 변수와 같은 쓰레드 별 메모리 영역이 할당되는 값은 해당되지 않으며 스프링 빈, static 객체와 같이 싱글톤 객체의 필드를 변경하며 사용할 때 발생한다. 이를 해결하기 위해 사용하는 것이 쓰레드 로컬(ThreadLocal)이다.
동시성 문제에 대한 java에서의 처리 방법이 궁금해져서 OpenAi의 ChatGPT에게 물어보았더니 아래와 같은 답변을 얻었다.
Synchronized 키워드 : Java에서 synchronized 키워드를 사용하여 메소드 또는 블록을 잠글 수 있습니다.
Lock 객체 : Java에서 Lock 객체를 사용하여 동시적으로 접근하려는 자원에 대한 접근을 제어할 수 있습니다.
Atomic 클래스 : Java에서 Atomic 클래스를 사용하여 동시성 문제가 발생하는 경우 자동적으로 락을 걸어주고 해제하여 동시성 문제를 해결할 수 있습니다.
Executor Framework : Java에서 Executor Framework를 사용하여 다중 스레드 환경에서 동시성 문제를 해결할 수 있습니다.
이 외에도 Java 5에서 추가된 Concurrent Package 등의 라이브러리를 사용하여 동시성 문제를 해결할 수 있습니다.
쓰레드 로컬은 쓰레드 별로 생성되는 별도의 내부 저장소를 지칭한며, 다음과 같은 기능을 제공한다.
ThreadLocal.set()
ThreadLocal.get()
ThreadLocal.remove()
@Slf4j
public class ThreadLocalService {
private ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore.get());
nameStore.set(name);
sleep(1000);
log.info("조회 nameStore={}",nameStore.get());
return nameStore.get();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Slf4j
public class ThreadLocalServiceTest {
private ThreadLocalService service = new ThreadLocalService();
@Test
void threadLocal() {
log.info("main start");
Runnable userA = () -> {
service.logic("userA");
};
Runnable userB = () -> {
service.logic("userB");
};
Thread threadA = new Thread(userA);
threadA.setName("thread-A");
Thread threadB = new Thread(userB);
threadB.setName("thread-B");
threadA.start();
sleep(100);
threadB.start();
sleep(2000);
log.info("main exit");
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
[Test worker] main start
[Thread-A] 저장 name=userA -> nameStore=null
[Thread-B] 저장 name=userB -> nameStore=null
[Thread-A] 조회 nameStore=userA
[Thread-B] 조회 nameStore=userB
[Test worker] main exit
쓰레드 로컬을 모두 사용 후 ThreadLocal.remove()
를 통해 쓰레드 로컬에 저장된 값을 제거해주어야 한다. 쓰레드의 생성 비용은 비싸기 때문에 WAS는 사용이 끝난 쓰레드를 제거하지 않고, 보통 쓰레드 풀을 통해 재사용한다. 그렇기에 사용 후 제거하지 않으면 심각한 문제가 발생할 수 있다.