개발 실장님께서 아래 코드에 버그가 있는데 무엇인지 알겠냐고 물어보셨다.
(회사 코드를 그대로 쓸 수 없어 클래스명, 변수명을 변경했다.)
🔍 이 코드에서 버그를 찾아보자
NaverLoginService, KakaoLoginService, GoogleLoginService 등
"소셜 로그인 제공자 + LoginService" 형식으로 네이밍된 싱글톤 빈들이 존재한다.
💥 버그: 요청마다 바뀌는 값을 싱글톤 빈의 필드에 저장했다
loginService가 멤버 변수로 선언되어 있다.
이 Service 클래스는 싱글톤 빈이므로, 모든 요청 스레드가 같은 인스턴스를 공유한다.
loginService)을 싱글톤 빈의 필드에 저장했다.여러 스레드가 같은 필드를 동시에 읽고 쓰면서 값이 덮어씌워진다.
✅ 1차 해결: 요청별 참조는 지역 변수로 둔다
지역 변수의 참조는 각 스레드의 스택 프레임에 저장된다.
참조하는 객체는 힙에 생성되지만, 그 참조가 다른 스레드에 공유되지 않으면 요청별로 안전하게 사용할 수 있다.
loginService를 지역 변수로 바꿔도,getBean()이 반환한 LoginService 자체가 싱글톤이라면init()이 내부 필드를 변경하는 구조일 경우 같은 문제가 다시 발생한다.예를 들어 NaverLoginService 안에
this.userInfo = userInfo; 같은 코드가 있으면,여러 스레드가 같은 NaverLoginService 인스턴스의 필드를 동시에 바꾸게 된다.
이런 경우 init 파라미터를 이후 메서드에 명시적으로 전달하거나,
요청별 상태 객체를 따로 만들어 처리해야 한다.
📐 왜 이런 일이 생기나 — 스택과 힙


🤔 왜 Thread-safe 하다고 착각했을까?
첫째, 멀티스레드 환경을 많이 겪어보지 않아서
막연히 "스프링이 알아서 해주겠지"라고 생각했다.
둘째, 대부분의 스프링 코드가 이미 Thread-safe한 패턴으로 작성되어 있었기 때문이다.
Repository, 다른 Service를 final 필드로 주입받는 패턴은 상태가 바뀌지 않으므로 안전하다.
그래서 "빈은 원래 안전하다"고 착각하기 쉽다.
📝 정리
스프링 싱글톤 빈은 여러 스레드가 동시에 사용할 수 있다.
스프링이 자동으로 Thread-safe하게 만들어주는 것은 아니다.
private final UserRepository userRepository;private final PaymentClient paymentClient;private final SomePolicy policy;불변 의존성은 값이 바뀌지 않으므로 안전
private RequestDto currentRequest;private LoginService loginService;private String userId;요청마다 바뀌는 mutable 상태는 위험
요청마다 값이 바뀌는 mutable state를 싱글톤 빈의 필드에 두는 것이 문제다.
의존성은
final 필드로 주입해도 괜찮다.요청별 데이터는 지역 변수나 메서드 파라미터로 전달해야 한다.
이 글이 싱글톤 빈에 mutable state를 두면 안 되는 이유라면,
아래 글은 @Scope(PROTOTYPE)을 붙였는데 왜 같은 객체가 공유됐는지를 다룬다.
함께 보면 스프링 빈 스코프와 Thread-safe 문제를 같이 이해할 수 있다.
'Programming > Java & Spring 관련 내용 정리' 카테고리의 다른 글
| [Spring] 멀티 쓰레드 환경에서 자원이 공유돼 버렸다. (0) | 2023.05.31 |
|---|---|
| [Spring] @Async는 왜 제대로 동작하지 못했는가악! (0) | 2023.03.23 |
| [Spring] 멀티모듈 구성 방법 (0) | 2022.10.28 |
| [Spring Batch] 우리 정산 시스템은 오류를 어떻게 처리하고 있을까 (0) | 2022.10.28 |
| [Spring Batch] 로그인한지 1년 지난 회원 -> 휴면회원으로 상태변경하는 배치구현 (0) | 2022.09.22 |