쓰레드 로컬

동시성 문제

수도코드는 다음과 같다.

@Controller
public class MyController {
    private final MyService myService;

    @Get("/hello-number")
    public String hello(@RequestParam long num) {
        myService.doService(num);
    }
}

@Slf4j
@Service
public class MyService {
    
    private Long beanVariable;

    public void doService(Long num) {
        beanVariable = num;
        sleep(5000); // 임의로 넣은 스레드 슬립 시간.
        log.info("current Number value is : {}", beanVariable);
    }
}

흔-한 웹서비스 컨트롤러 + 서비스 구조.

서비스에 classVariable 라는 변수가 있다.

저 구조에서 /hello-number 를 슬립 시간인 5초 내에 연속으로 따딱 요렇게 때리면 어떻게 될까? (임의로 슬립을 5초나 준 이유)

  • /hello-number?num=1
  • /hello-number?num=2
  • /hello-number?num=3

(수도코드라 안해봤지만) 로그는 요렇게 떨어진다.

2022.07.07. 00:00:00 [Thread - 1] current Number value is : 3
2022.07.07. 00:00:02 [Thread - 2] current Number value is : 3
2022.07.07. 00:00:04 [Thread - 3] current Number value is : 3

왜 그럴까

동시성 문제때문이다.

동시성 문제가 무슨 소리냐 하면…

  • 기본적으로 최초 수도코드의 구조로 스프링 어플리케이션을 띄우면,
    컨트롤러도, 서비스도 컴포넌트 어노테이션(@Controller, @Service)을 달았기 때문에 Bean이 생성되고,
    이 Bean들은 스프링 컨테이너에 의해서 관리된다.
  • 개발자가 어노테이션을 통해 컴포넌트라고 등록하고, 스프링 컨테이너가 관리하는 Bean은 기본적으로 ‘싱글톤’이다.
  • 그리고 스프링 부트에서 사용하는 톰캣은, 하나의 요청을, ‘프로세스’ 가 아닌 ‘스레드’ 를 통해서 처리를 한다
    • 톰캣 구동시에 설정(안했으면 기본설정)한 스레드 풀에서 스레드를 꺼내와서 실행.
    • 프로세스로 띄우거나 하는 미친 WAS가 있긴 할랑가..

그래서, 스프링 어플리케이션을 띄우게 되면

  • MyService Bean이 한개.
    • 따라서 MyService.beanVariable 도 (당연히)한개.
  • 짧은 시간(??)에 한꺼번에 받은 수많은 요청을 멀티스레드로 처리하여,
    결국엔 MyService.beanVariable 변수에 접근을 하는데, 여기서 일종의 race condition 이 발생하여 ‘공용 자원을 동시에 접근하는’ 불상사가 발생…
    • 사용자로부터 요청은 각각 1,2,3 으로 받았으나, 정작 처리는 3,3,3 으로 처리



해결방법

개인적으로는 애초에 그냥 저 beanVariable같은걸 사용하지 않게 컴포넌트 자체에 클래스 변수를 사용하지 않게 코딩을 하는게 3억배 낫다고 생각한다.

내가 식견이 짧고 해왔던 일만 해서 그런가, 사실 bean에 종속된 저런 전역성 변수를 사용하는 케이스도 잘 못봤다.

근데, 그럼에도 불구하고 언젠가 사용해야 할 상황이 온다면….

ThreadLocal 을 사용하면 된다.

딱 봐도 네이밍이 ‘스레드 로컬’… 로컬 스레드에만 할당되는 저장소라는 느낌이 팍팍 들지 않는가..

사용법은 초 간단하다… 그냥 예제만으로도 설명이 될 지경임

@Controller
public class MyController {
    private final MyService myService;

    @Get("/hello-number")
    public String hello(@RequestParam long num) {
        myService.doService(num);
    }
}

@Slf4j
@Service
public class MyService {
    
    private ThreadLocal<Long> threadLocalVariable = new ThreadLocal<>(); // 스레드로컬 생성!!

    public void doService(Long num) {
        threadLocalVariable.set(num); // 셋!!!!!!!!!!!
        sleep(5000);
        log.info("current Number value is : {}", threadLocalVariable.get()); // 겟!!!!!!!!!!!
    }

}

이제, 아까와 똑같이 /hello-number 를 연속으로 따딱 때리면 어떻게 될까?

  • /hello-number?num=1
  • /hello-number?num=2
  • /hello-number?num=3

(요것도 일종의 수도코드라 안해봤지만) 로그는 요렇게 떨어진다.

2022.07.07. 00:01:00 [Thread - 1] current Number value is : 1
2022.07.07. 00:01:02 [Thread - 2] current Number value is : 2
2022.07.07. 00:01:04 [Thread - 3] current Number value is : 3

Leave a Comment