김영한님의 강의를 듣고 정리한다.
Spring의 Controller, Service.. 등등 대부분의 클래스들은 Spring Container에 Bean으로 등록되어 사용된다. 스프링 빈은 싱글톤으로 등록된다. 이 인스턴스가 애플리케이션에 딱 1개 존재한다는 뜻이다. 이렇게 하나만 있는 인스턴에 전역변수를 선언하여 사용하면 어떤 문제가 발생할까?
해당 빈의 전역변수를 여러개의 쓰레드가 동시에 접근하여 사용하기 때문에 중간에 데이터가 바뀌거나 유실되는 문제가 발생한다. 이를 동시성 문제라고한다.
이런 동시성 문제는 지역 변수에서는 발생하지 않는다. 지역 변수는 쓰레드마다 각각 다른 메모리 영역이 할당된다. 동시성 문제가 발생하는 곳은 같은 인스턴스의 필드(주로 싱글톤에서 자주 발생), 또는 static 같은 공용 필드에 접근할 때 발생한다. 동시성 문제는 값을 읽기만 하면 발생하지 않는다. 어디선가 값을 변경하기 때문에 발생하는 문제다.
ThreadLocal java.lang.ThreadLocal
쓰레드 로컬은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 의미한다.
아래 그림처럼 쓰레드 로컬을 사용하면 각 쓰레드마다 각자의 저장소를 제공한다. 따라서 같은 인스턴스의 쓰레드 로컬 필드에 접근해도 문제가 없다. 일반적인 변수 필드는 같은 인스턴스의 다른 쓰레드가 접근하여 값은 변경하면 다른 쓰레드가 저장한 변수는 사라진다.
문제가 되는 코드
@Slf4j
public class FieldService {
private String nameStore;
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore);
nameStore = name;
sleep(1000); // 비지니스 로직이 실행된다는 가정(쿼리 조회 등)
log.info("조회 nameStore={}", nameStore);
return nameStore;
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Slf4j
public class FieldService Test {
private FieldService fieldService = new FieldService();
@Test
void field() {
Runnable userA = () -> {
fieldService.logic("userA");
});
Runnable userB = () -> {
fieldService.logic("userB");
});
Thread threadA = new Thread(userA);
threadA.setName("thread-A");
Thread threadB = new Thread(userB);
threadA.setName("thread-B");
threadA.start();
sleep(200);
threadB.start();
}
}
23:00:33.242 [thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService - 저장 name=userA -> nameStore=null
23:00:33.351 [thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService - 저장 name=userB -> nameStore=userA
23:00:34.254 [thread-A] INFO hello.advanced.trace.threadlocal.code.FieldService - 조회 nameStore=userB
23:00:34.361 [thread-B] INFO hello.advanced.trace.threadlocal.code.FieldService - 조회 nameStore=userB
threadA가 fieldService.logic("userA")을 실행하고 실행이 끝나지 않은 상황에서 threadB가 fieldService.logic("userB")을 실행이 되어서 threadA에서 리턴되는 nameStore는 threadB가 수정한 userB이다.
@Slf4j
public class FieldService {
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();
}
}
}
FieldService의 일반 변수였던 nameStore를 ThreadLocal로 변경하면 이러한 동시성 문제가 발생하지 않는다.
주의해야할 점
ThreadLocal과 쓰레드풀을 함께 사용할 때는 각별히 주의해야 한다.
이처럼 쓰레드풀을 사용하는 경우 쓰레드로컬의 데이터를 재사용하는 결과가 일어날 수 있다. 따라서 쓰레드 로컬을 사용할 때는 쓰레드 로컬의 사용이 끝날 때 ThreadLocal.remove()를 통해서 꼭 제거해줘야 한다.