개발 학습 실습실

이 사이트의 실습은 자바스크립트를 켜야 움직입니다. 아래는 같은 내용을 글로 적은 것입니다.

공부 › Spring › IoC 컨테이너와 DI

싱글톤 빈과 상태

내 주문에 다른 사람 이름이 찍혔다

스프링 빈은 기본적으로 하나만 만들어져 모든 요청이 함께 씁니다. 그 빈의 필드에 요청마다 달라지는 값을 저장하면, 동시에 들어온 다른 요청이 그 값을 덮어씁니다.

실행으로 대조 · JDK 21에서 실행해 확인

이 실습에 나오는 개념

싱글톤 빈과 무상태(Stateless)
싱글톤 빈은 하나의 인스턴스를 모든 요청 스레드가 함께 사용한다. 따라서 요청마다 달라지는 데이터를 인스턴스 필드에 저장하면 여러 요청의 데이터가 섞인다. 싱글톤 빈은 요청별 상태를 필드에 두지 않고 메서드의 지역 변수와 매개변수로만 다루는 무상태로 설계해야 한다.
빈 스코프
빈 인스턴스가 만들어지고 유지되는 범위다. 기본값인 singleton은 컨테이너에 하나만 만들어 모두가 공유한다. prototype은 요청할 때마다 새로 만든다. 웹 환경에는 HTTP 요청마다 만드는 request, 세션마다 만드는 session 스코프가 있다.
ThreadLocal
스레드마다 독립된 값을 저장할 수 있게 하는 변수다. 같은 ThreadLocal 변수라도 스레드별로 다른 값을 가지므로 동기화 없이 스레드 고유의 정보를 보관할 수 있다. 스프링은 현재 트랜잭션과 인증 정보를 이 방식으로 보관한다. 스레드 풀에서는 스레드가 재사용되므로 사용 후 반드시 remove()로 지워야 한다.
스레드 안전(Thread-safe)
여러 스레드가 동시에 사용해도 올바르게 동작하는 성질이다. 공유하는 상태가 없거나, 상태가 불변이거나, 접근이 적절히 동기화되어 있으면 스레드 안전하다. 스프링의 싱글톤 빈은 여러 요청 스레드가 함께 사용하므로 스레드 안전하게(보통 상태를 갖지 않게) 만들어야 한다.
경쟁 상태(Race Condition)
둘 이상의 스레드가 같은 데이터를 동시에 읽고 쓸 때 실행 순서에 따라 결과가 달라지는 상황이다. count++는 값을 읽고, 1을 더하고, 다시 쓰는 세 단계로 이뤄지는데 두 스레드의 단계가 섞이면 한쪽의 증가가 사라진다. 테스트에서는 잘 드러나지 않다가 부하가 걸릴 때 나타나는 것이 특징이다.

이렇게 바꿔 보세요

처음 설정으로 실행하면

  1. OrderService 빈은 컨테이너에 하나만 있습니다. 철수의 요청은 스레드 1이, 영희의 요청은 스레드 2가 맡아 거의 동시에 같은 객체의 order()를 호출합니다.
  2. 스레드 1(철수): 필드 currentUser에 "철수"를 저장합니다.
  3. 스레드 2(영희): 같은 필드에 "영희"를 저장합니다. 필드는 하나뿐이므로 "철수"가 덮어써집니다.
  4. 스레드 1(철수): 재고 확인을 마치고 필드를 다시 읽어 주문을 만듭니다. 필드에는 "영희"가 들어 있습니다. 철수의 주문이 영희의 이름으로 만들어졌습니다.
  5. 스레드 2(영희): 필드를 읽어 주문을 만듭니다. "영희"입니다.

실행 결과

코드

@Service
class OrderService {                 // 인스턴스는 하나뿐
    private String currentUser;      // 모든 요청이 함께 쓰는 필드

    public Order order(String user, Item item) {
        this.currentUser = user;     // (1) 저장
        checkStock(item);            // 잠깐 다른 일
        return new Order(currentUser, item);   // (2) 다시 읽음
    }
}

이 모형의 전제

두 요청이 가장 나쁘게 겹치는 순서(둘 다 저장한 뒤에 읽는 순서)를 가정했습니다. 실제로는 겹칠 때만 드러나므로 재현이 어렵습니다. 스프링 빈의 기본 스코프가 싱글톤이라는 점과 ThreadLocal이 스레드별로 값을 따로 가진다는 점은 스프링과 자바 공식 문서의 설명을 따랐습니다. 이 PC의 JDK 21에서 실제로 실행해 확인했습니다. 객체 하나의 필드에 두 스레드가 값을 쓰게 하니 두 주문 모두 나중에 쓴 이름이 됐고, 지역 변수로는 섞이지 않았습니다. 스레드 하나짜리 풀에서 ThreadLocal을 지우지 않으면 다음 작업이 이전 값을 읽었고, remove한 경우에는 null이 나왔습니다.

앞 실습: 빈 생성과 주입 · 다음 실습: 빈 스코프

이 실습에 나오는 개념

이 실습에서 보는 것

설정

코드


      
    

그림

실행 비교

이렇게 바꿔 보세요