기본 지식
다음과 같은 Service Class에 특정 객체를 save하는 로직이 있다고 가정한다.
( @Service는 테스트 코드라 적지 않음. -> 해당 객체는 Singleton )
@Slf4j
public class FieldService {
private String nameStore;
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore);
// 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();
}
}
}
해당 FieldService는 인스턴스 변수로 nameStore을 가지고 있다.
@Slf4j
public class FieldServiceTest {
private final FieldService fieldService = new FieldService();
@Test
void field() {
log.info("main start");
Runnable userA = () -> fieldService.logic("userA");
Runnable userB = () -> fieldService.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(200); // Main Thread 대기
log.info("main exit");
}
private void sleep(int i) {
try {
Thread.sleep(i);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
해당 순서되로 진행되므로 기대 결과는 다음과 같다.
1. userA가 저장> nameStore는 초기화 전이므로 null, userA저장
2. userA가 조회> nameStore에 저장되어 있는 userA 표기
3. userB가 저장=> nameStore에 저장되어 있는 userA 표기, userB저장
4. userB가 조회 => nameStore에 userB저장
기대 결과
main start
저장 name=userA -> nameStore=null
조회 nameStore=userA
저장 name=userB -> nameStore=userA
조회 nameStore=userB
main exit
실제 결과
main start
저장 name=userA -> nameStore=null
저장 name=userB -> nameStore=userA
조회 nameStore=userB
조회 nameStore=userB
main exit
이유
-> Spring Bean으로 등록되었다는 가정하에
위에서 살펴본 동시성 문제를 해결하기 위해 Thread 단위로 로컬 변수를 할당하는 기능을 사용할 수 있다.
이를 간단하게 사용할 수 있는 Java의 Thread Local Class가 있다.
Thread Class
public class Thread implements Runnable {
// ...
ThreadLocal.ThreadLocalMap threadLocals = null;
Thread Class에는 다음과 같이 ThreadLocal.ThreadLocalMap을 가지고 있다.
여기서 ThreadLocalMap이란, Thread의 주소값을 Key로 하는 Map이다.
해당 threadLocals 변수를 ThreadLocal Class를 통해 간단하게 제어할 수 있다.
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();
}
예시
@Slf4j
public class ThreadLocalService {
private ThreadLocal<String> nameStore = new ThreadLocal<>();
public String logic(String name) {
log.info("저장 name={} -> nameStore={}", name, nameStore.get());
// Set nameStore
nameStore.set(name);
sleep(1000);
log.info("조회 nameStore={}", nameStore.get());
// Get nameStore
return nameStore.get();
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
ThreadLocal을 사용하면, 각 Thread 마다 자신만 접근 가능한 변수에 접근하므로 동시성 문제를 해결할 수 있다.
Thread Local을 사용하면, 해당 Thread만 접근할 수 있는 로컬 변수가 생기는 셈이다.
그러나, Thread는 Tomcat에서 Thread Pool에 미리 만들어 놓은 Thread를 이용하기 때문에
remove()를 수행하지 않으면 이전에 저장된 ThreadLocal 변수에
새로운 사용자가 접근해 잘못된 결과가 발생할 수 있다.