스프링 어플리케이션은 WAS의 스레드풀에서 요청마다 스레드를 할당하여 로직을 수행하게 된다. 그런데 잘못 설계를 하면 스레드마다 같은 변수에 접근하여 값을 공유할 수도 있다. 이런 경우를 방지하기 위해 Java에서는 ThreadLocal이라는 것을 제공해서 스레드마다 자신의 저장 공간을 가질 수 있도록 해준다.
ThreadLocal 미적용
아래는 요청을 처리하기 시작했을 때 1을 증가시키고, 끝날 때 다시 1을 감소시키는 간단한 로직이다.
@Getter
@Component
public class NumberManager {
private int num;
public void start() {
++num;
}
public void end() {
--num;
}
}
@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {
private final NumberManager numberManager;
@GetMapping("/test")
public String test() throws InterruptedException {
numberManager.start();
log.info("{}", numberManager.getNum());
Thread.sleep(1000);
numberManager.end();
log.info("{}", numberManager.getNum());
return "good";
}
}
실행 결과를 보면 원래는 1 증가하고 1 감소해서 결과가 0이어야 하지만 NumberManager를 스프링 빈으로 등록했기 때문에 같은 인스턴스를 공유해서 원하는 대로 동작하지 않는 것을 알 수 있다.
ThreadLocal 적용
@Getter
@Component
public class NumberManager {
private ThreadLocal<Integer> num = ThreadLocal.withInitial(() -> 0);
public void start() {
num.set(num.get() + 1);
}
public void end() {
num.set(num.get() - 1);
}
public void remove() {
num.remove();
}
}
@Slf4j
@RestController
@RequiredArgsConstructor
public class TestController {
private final NumberManager numberManager;
@GetMapping("/test")
public String test() throws InterruptedException {
numberManager.start();
log.info("{}", numberManager.getNum().get());
Thread.sleep(1000);
numberManager.end();
log.info("{}", numberManager.getNum().get());
numberManager.remove();
return "good";
}
}
ThreadLocal은 Optional처럼 값을 감싸고 있는 형태이기 때문에 set()으로 값을 설정하고 get()으로 값을 가져와야 한다. 위처럼 초기 값을 지정해도 되고 그냥 new ThreadLocal<>()로 생성한 뒤에 따로 값을 할당해 줘도 된다.
이제 실행해보면 1 증가하고 다시 0으로 감소하는 동작이 개별로 잘 동작하는 것을 알 수 있다. ThreadLocal은 이렇게 스레드 별 독립된 공간에 저장할 수 있도록 해준다는 것을 알 수 있었다.
주의할 점
ThreadLocal은 스레드 별로 독립된 공간을 제공하지만 문제가 있다. 그것은 WAS의 스레드풀 때문에 같은 스레드의 요청이 다시 올 수도 있다는 점이다. 그래서 같은 스레드 요청이 다시 오게 되면 이전에 처리했던 값은 어떻게 될까? 직접 end()와 remove()를 주석 처리하고 숫자를 더하는 로직만 남겨서 실행해 보자.
이런 식으로 처음에는 1로 잘 동작하지만...
나중에 같은 스레드로 요청이 오면 이전의 값에 1 더해진 값으로 처리되는 것을 알 수 있다. 처음 요청에서 수행했던 값이 남아있기 때문이다. 그렇기 때문에 해당 스레드에서 진행했던 값을 삭제하기 위해 로직을 수행한 뒤 마지막에 remove()를 통해서 해당 스레드 공간의 값을 지워버리는 것이다. 이렇게 해야 나중에 같은 스레드의 요청이 왔을 때 이전에 동작하던 대로 작동할 수 있다.
'공부 > Java' 카테고리의 다른 글
[Java] 퀵정렬 (QuickSort) 구현하기 (0) | 2024.09.24 |
---|---|
[Java] 동적 프록시 (Dynamic Proxy) (0) | 2024.07.23 |
[Java] 람다식 (Lambda expression) (0) | 2024.02.23 |
[Java] 생성자 (Constructor) (0) | 2024.02.22 |
[Java] 날짜 및 시간 관련 클래스 (Date, Calendar, Timestamp, LocalDate 등) (0) | 2023.12.25 |