스프링 컨테이너는 등록된 스프링 빈들을 모두 싱글톤으로 관리한다. 따라서 스프링 빈으로 등록된 객체의 인스턴스는 애플리케이션에 딱 하나만 존재하게 된다. 싱글톤으로 관리해서 메모리 낭비를 방지할 수 있지만 조심해야하는 점이 있다. 그것은 필드 동시성 문제이다.
만약 싱글톤으로 관리하는 인스턴스의 필드를 여러 쓰레드가 동시에 접근하여 필드 값을 변경하면서 발생하는 문제이다.
@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 FieldServiceTest {
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);
threadB.setName("thread-B");
threadA.start();
threadB.start();
sleep(3000);
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
자바에서 필드 동시성 문제를 해결하기 위해 ThreadLocal 클래스를 제공한다!!
ThreadLocal은 해당 쓰레드만 접근할 수 있는 특별한 저장소를 말한다. 한마디로 해당 공유 필드에 대해서 쓰레드단위로 로컬 변수처럼 사용할 수 있게끔하는 것이다.
@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 field() {
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();
threadB.start();
sleep(3000); // 메인 쓰레드 종료 대기
}
private void sleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
}
만약 쓰레드 로컬의 값을 사용 후 제거하지 않으면 쓰레드 풀을 사용하는 경우 심각한 문제가 발생할 수 있다.
톰캣과 같이 쓰레드 풀을 사용하여 thread를 재활용하는 경우, 이전에 저장했던 ThreadLocal의 값이 남아있어 원치않는 동작을 할 수 있다. 따라서 쓰레드 풀을 사용하는 경우 반드시 모두 사용 후 ThreadLocal의 값을 remove 메서드를 사용하여 값을 제거해주어야 한다.
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
해당 코드는 Spring Security에서 사용자 인증 정보를 가져올 때 사용하는 코드이다. getContext() 메서드 내부가 ThreadLocal로 구현되어 있어, Thread 별로 인증정보를 다르게 가지고 있도록 할 수 있다.
https://www.inflearn.com/course/스프링-핵심-원리-고급편/dashboard
https://sabarada.tistory.com/163