ThreadLocal은 쓰레드마다 해당 쓰레드만 접근할 수 있는 저장 공간입니다.
아래처럼 여러 요청을 받아도 Java에서 제공하는 쓰레드 로컬을 사용해서 값을 저장하면 각 요청(쓰레드) 마다 쓰레드 자기 자신만 접근할 수 있는 공간에 값이 저장되고 해당 값을 안전하게 사용할 수 있습니다.
@Service
@Slf4j
public class SaveService {
private String StoredName;
public String save(String name) {
log.info("name을 StoredName 에 저장 name={} -> StoredName={}", name, StoredName);
StoredName = name;
sleep(3000);
log.info("조회 StoredName={}",StoredName);
return StoredName;
}
@Controller
@RequiredArgsConstructor
public class HomeController {
private final SaveService saveService;
@GetMapping("/{name}")
@ResponseBody
public String saveName(@PathVariable String name){
//이름을 저장하고 저장된 이름을 출력하는 함수
saveService.save(name);
return "ok";
}
}
그럼 이제 아래와 같이 브라우저로 요청을 보내겠습니다
PONY 를 전달하는 요청을 보내고, 바로 JINY를 전달하는 요청을 보냅니다
[결과]
Service객체는 싱글톤 빈으로 등록되었고, 결국 StoredName 변수도 하나의 공간만 생깁니다.
그래서 StoredName에 PONY가 먼저 저장되었지만, 바로 뒤에 오는 JINY 요청이 덮어 쓰게 되는 것입니다. 흐름을 그림으로 본다면 다음과 같습니다
선언
ThreadLocal<"저장할 데이터의 타입"> threadLocal = new ThreadLocal<>();
ThreadLocal에 값 저장하기
threadLocal.set(데이터);
ThreadLocal에 저장한 값 불러오기
threadLocal.get()
@Service
@Slf4j
public class SaveService {
private ThreadLocal<String> StoredName = new ThreadLocal<>();
public String save(String name) {
log.info("name을 StoredName 에 저장 name={} -> StoredName={}", name, StoredName);
StoredName.set(name);
sleep(3000);
log.info("조회 StoredName={}",StoredName.get());
return StoredName.get();
}
그리고 다시 PONY 요청을 보내고 JINY 요청을 보냅니다
[결과]
요청(쓰레드) 마다 쓰레드로컬이 생겨 안전하게 값을 출력할 수 있게 되었습니다.
ThreadLocal을 통해 멀티쓰레드를 사용해도 Thread-safe 한 환경을 구성할 수 있다.
자바에서는 synchronized 예약어를 통해서도 Thread-safe 한 환경을 구성할 수는 있지만 둘은 해당 데이터를 같은 변수에 저장하냐, 다른 변수에 저장하냐에 차이가 있다.
쓰레드로컬은 쓰레드 별로 데이터를 다른 변수에 저장하지만,
synchronized 방식은 쓰레드 별로 같은 변수를 사용하되, 동일한 변수에 접근할 때 나머지 쓰레드들이 접근 못하도록 블락킹하는 방식으로 동작합니다.
그래서 멀티스레드 환경에서 오히려 성능 저하를 유발할 수 있습니다.
스프링부트 프로젝트를 하면 쓰레드는 보통 커넥션 풀을 사용해서 재사용하는 방식으로 작동한다.
그래서 쓰레드 로컬의 값을 사용 후 제거하지 않으면 커넥션 풀에서 스레드를 꺼냈을 때 스레드 로컬에 값이 남아 있을 수 있다. (아래 그림 참고)
반드시 ThreadLocal.remove() 을 통해 저장한 값을 제거해줘야 한다.