스프링은 기본적으로 빈을 하나만 만들어서 모든 곳에서 공유합니다. 그런데 여러 요청이 동시에 들어오면, 하나의 객체를 공유해도 정말 괜찮은 걸까요?

개념 정의

Bean 스코프 는 스프링 컨테이너가 빈 인스턴스를 얼마나 오래, 몇 개나 유지할지를 결정하는 설정입니다. 기본값은 singleton 으로, 컨테이너 전체에서 단 하나의 인스턴스만 존재합니다.

왜 필요한가

모든 객체가 싱글톤이면 안 되는 상황이 있습니다.

  • **상태를 가진 객체 **: 사용자별로 다른 데이터를 담아야 하는 경우
  • **HTTP 요청별 데이터 **: 요청 컨텍스트 정보를 빈에 담아야 하는 경우
  • ** 매번 새 인스턴스가 필요한 경우 **: 사용할 때마다 초기 상태여야 하는 객체

스코프를 이해하지 못하면 "왜 다른 사용자의 데이터가 보이지?" 같은 심각한 버그를 만들 수 있습니다.

내부 동작

스코프 종류

스코프설명인스턴스 수
singleton컨테이너당 하나 (기본값)1개
prototype요청할 때마다 새로 생성N개
requestHTTP 요청당 하나요청 수만큼
sessionHTTP 세션당 하나세션 수만큼
applicationServletContext당 하나1개
websocketWebSocket 세션당 하나세션 수만큼

싱글톤이 기본인 이유

왜 매번 새 객체를 만들지 않고 하나만 만들어서 공유할까요?

  1. 대부분의 스프링 빈은 ** 상태를 갖지 않는 서비스 객체 **입니다.
  2. 상태가 없으면 여러 스레드가 동시에 사용해도 서로 영향을 주지 않습니다.
  3. 하나의 인스턴스를 재사용하면 객체 생성 비용과 GC 부담이 사라집니다.
  4. 따라서 기본 스코프를 singleton으로 설정하는 것이 합리적입니다.
JAVA
@Service
public class OrderService {
    private final OrderRepository orderRepository; // 상태 없음, 의존성만 보유

    public Order createOrder(OrderRequest request) {
        // 매개변수로 데이터를 받아서 처리 → 인스턴스 변수에 저장하지 않음
        return orderRepository.save(new Order(request));
    }
}

이런 서비스는 100개의 요청이 동시에 들어와도, 각자 자기 매개변수와 로컬 변수를 사용하기 때문에 하나의 인스턴스를 공유해도 안전합니다. 매번 새 객체를 만들면 메모리 낭비이고, GC 부담만 늘어납니다.

싱글톤에서 주의할 점

JAVA
@Service
public class BadService {
    private int count = 0; // 인스턴스 변수에 상태 저장 → 위험!

    public int incrementAndGet() {
        return ++count; // 여러 스레드가 동시에 접근하면 문제 발생
    }
}

싱글톤 빈에 가변 상태를 두면 동시성 문제가 발생합니다. 싱글톤 빈은 ** 무상태(stateless)**로 설계해야 합니다.

Prototype 스코프

JAVA
@Component
@Scope("prototype")
public class PrototypeBean {
    public PrototypeBean() {
        System.out.println("PrototypeBean 생성: " + this);
    }
}
JAVA
@Component
public class Client {
    @Autowired
    private ApplicationContext context;

    public void doSomething() {
        // 매번 새 인스턴스
        PrototypeBean bean1 = context.getBean(PrototypeBean.class);
        PrototypeBean bean2 = context.getBean(PrototypeBean.class);
        System.out.println(bean1 == bean2); // false
    }
}

중요한 점은 prototype 빈의 소멸 콜백(@PreDestroy)은 스프링이 호출하지 않는다 는 것입니다. 생성과 주입까지만 관리하고, 그 이후는 클라이언트 코드의 책임입니다.

싱글톤 + Prototype 조합 문제

이 부분이 실무에서 가장 많이 실수하는 지점입니다.

JAVA
@Component
@Scope("prototype")
public class PrototypeBean {
    private int count = 0;

    public int addAndGet() {
        return ++count;
    }
}

이어서 @Component을 적용한 나머지 구현부입니다.

JAVA
@Component
public class SingletonBean {
    private final PrototypeBean prototypeBean; // 주입 시점에 딱 한 번 생성됨

    public SingletonBean(PrototypeBean prototypeBean) {
        this.prototypeBean = prototypeBean;
    }

    public int logic() {
        return prototypeBean.addAndGet();
        // 매번 같은 인스턴스! count가 계속 증가함
    }
}

SingletonBean은 한 번만 생성되므로, 생성자에서 주입받은 PrototypeBean도 하나뿐입니다. prototype 스코프의 의미가 사라져버립니다.

코드 예제

ObjectProvider로 해결하기

JAVA
@Component
public class SingletonBean {
    private final ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public SingletonBean(ObjectProvider<PrototypeBean> prototypeBeanProvider) {
        this.prototypeBeanProvider = prototypeBeanProvider;
    }

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        // 매번 새 인스턴스 생성
        return prototypeBean.addAndGet(); // 항상 1 반환
    }
}

ObjectProvider는 빈 조회를 지연시켜서, getObject()를 호출할 때마다 컨테이너에 빈을 요청합니다. prototype 스코프이므로 매번 새 인스턴스를 받습니다.

JSR-330 Provider

JAVA
@Component
public class SingletonBean {
    private final Provider<PrototypeBean> provider; // jakarta.inject.Provider

    public SingletonBean(Provider<PrototypeBean> provider) {
        this.provider = provider;
    }

    public int logic() {
        PrototypeBean prototypeBean = provider.get(); // 매번 새 인스턴스
        return prototypeBean.addAndGet();
    }
}

Provider는 자바 표준이라 스프링에 덜 의존적이지만, 기능은 ObjectProvider가 더 풍부합니다(스트림 처리, Optional 지원 등).

Scoped Proxy로 해결하기

JAVA
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean {
    private int count = 0;

    public int addAndGet() {
        return ++count;
    }
}

이렇게 하면 싱글톤 빈에 주입되는 것은 CGLIB 프록시이고, 메서드를 호출할 때마다 프록시가 컨테이너에서 새 prototype 인스턴스를 가져와 위임합니다.

Request 스코프

JAVA
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestContext {
    private String requestId;
    private LocalDateTime requestTime;

    @PostConstruct
    public void init() {
        this.requestId = UUID.randomUUID().toString();
        this.requestTime = LocalDateTime.now();
    }

    // getter, setter
}
JAVA
@RestController
public class OrderController {
    private final RequestContext requestContext; // 프록시 주입

    public OrderController(RequestContext requestContext) {
        this.requestContext = requestContext;
    }

    @GetMapping("/order")
    public String order() {
        // 각 HTTP 요청마다 다른 RequestContext 인스턴스 사용
        return "Request ID: " + requestContext.getRequestId();
    }
}

request 스코프 빈은 proxyMode를 지정해야 합니다. 싱글톤인 컨트롤러가 생성될 때는 아직 HTTP 요청이 없으므로, 프록시를 넣어두고 실제 요청이 올 때 진짜 빈을 연결합니다.

스코프 선택 기준

PLAINTEXT
빈에 상태가 있는가?
├── 없다 → singleton (기본값)
└── 있다
    ├── 요청마다 다른 상태? → request
    ├── 세션마다 다른 상태? → session
    └── 매번 새 인스턴스 필요? → prototype

주의할 점

1. 싱글톤 빈에 가변 인스턴스 변수를 두면 동시성 버그가 발생한다

싱글톤 빈은 모든 스레드가 공유합니다. private int count = 0 같은 가변 필드를 두면 여러 스레드가 동시에 접근하여 레이스 컨디션이 발생합니다. "다른 사용자의 데이터가 보인다"는 류의 버그가 이 원인인 경우가 많습니다. 싱글톤 빈은 반드시 무상태(stateless)로 설계하고, 요청별 데이터는 매개변수나 로컬 변수로 처리해야 합니다.

2. 싱글톤에 prototype을 주입하면 prototype이 싱글톤처럼 동작한다

싱글톤 빈은 한 번만 생성되므로, 생성자에서 주입받은 prototype 빈도 단 한 번만 생성됩니다. prototype의 "매번 새 인스턴스" 의미가 완전히 사라집니다. ObjectProvider<T>를 사용하여 getObject() 호출 시점마다 새 인스턴스를 받거나, proxyMode = ScopedProxyMode.TARGET_CLASS를 지정해야 합니다.

3. request 스코프 빈을 proxyMode 없이 싱글톤에 주입하면 시작 시 에러가 난다

request 스코프 빈을 싱글톤 컨트롤러에 직접 주입하면, 애플리케이션 시작 시 아직 HTTP 요청이 없어 BeanCreationException이 발생합니다. @Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)로 프록시를 통해 주입해야 실제 요청 시점에 올바른 빈이 연결됩니다.

정리

항목설명
기본 스코프singleton — 컨테이너당 하나. 대부분 이것으로 충분
싱글톤 핵심 규칙가변 인스턴스 변수 금지 (동시성 버그)
prototype 함정싱글톤에 주입하면 사실상 싱글톤으로 동작
해결법ObjectProvider<T> 또는 proxyMode = TARGET_CLASS
request/session반드시 proxyMode 설정 필요 (싱글톤 빈에서 사용 시)
댓글 로딩 중...