9장. 빈 스코프(싱글톤/프로토타입/웹스코프)

이번 장에서는 스프링 빈의 생존 기간을 결정하는 개념, 즉 스코프(Scope)를 다룬다.

대부분의 스프링 애플리케이션은 싱글톤으로 돌아간다고 생각하기 쉽지만,

실무에서는 request, session, application 같은 웹 스코프나

prototype 스코프를 반드시 알아야 하는 순간이 온다.

  • “유저 요청마다 다른 객체가 필요할 때”
  • “세션마다 데이터를 저장하고 싶을 때”
  • “싱글톤 안에서 프로토타입 빈을 사용해야 할 때”
  • “AOP 프록시가 request scope와 어떻게 협력하는지”

이런 상황을 정확히 이해하면

스프링을 사용하는 수준이 초급 → 중급 → 상급으로 바로 올라간다.


1. 스프링 빈 스코프란?

(쉽게 이해하는 설명)

스프링은 빈을 하나 만들고 끝이 아니라,

“그 빈을 언제 만들고 언제 폐기할까?”를 결정해야 한다.

이걸 스프링에서는 스코프(Scope)라고 부른다.

스프링이 기본적으로 제공하는 스코프는 다음 5가지다:

스코프설명
singleton스프링 컨테이너 시작부터 종료까지 1개 유지
prototype호출할 때마다 새로운 빈 생성
requestHTTP 요청마다 새로운 빈 생성
sessionHTTP 세션마다 1개 유지
application서블릿 컨텍스트 전체에 1개 유지

이 중에서도

singleton, prototype, request가 가장 중요하다.


2. 싱글톤 스코프 – 스프링의 기본 전략

(쉬운 설명)

싱글톤은 스프링 애플리케이션에서 가장 많이 사용되는 기본 스코프다.

특징:

  • ApplicationContext 초기화 시점에 딱 1개 생성
  • 모든 요청에서 같은 객체 사용
  • @PreDestroy 시점에 소멸

코드 예:

@Scope("singleton")
@Component
public class SingletonBean {}

스프링 빈의 95%는 싱글톤이다.

이유는 간단하다. 성능이 가장 좋고 메모리 효율적이며

스프링 DI 구조와 가장 잘 맞기 때문이다.


3. 프로토타입 스코프 – 요청할 때마다 새로운 객체 생성

(쉬운 설명)

프로토타입 스코프는 이렇게 동작한다.

  1. getBean()을 호출할 때마다
  2. 새로운 객체를 새로 만들고
  3. 의존관계 주입까지만 수행
  4. 이후 빈 관리는 개발자 책임

코드 예:

@Scope("prototype")
@Component
public class ProtoBean {}

중요한 점:

프로토타입 빈은 @PreDestroy가 호출되지 않는다.

즉, 종료 관리 책임이 스프링이 아니라 개발자에게 있다.


4. 문제: 싱글톤 안에서 프로토타입 빈을 사용하면?

(쉬운 설명 → 기술 심층 연결)

아래처럼 작성했다고 해보자.

@Component
public class SingletonBean {

    @Autowired
    private ProtoBean protoBean;

    public int logic() {
        protoBean.addCount();
        return protoBean.getCount();
    }
}

문제:

  • 스프링은 DI를 초기 1회만 수행한다
  • 즉, SingletonBean 내부에 주입된 프로토타입 빈은 항상 같은 객체

즉, 프로토타입 의미가 사라진다.


5. 해결책: Provider 사용

(쉬운 설명)

Provider는 “필요할 때마다 새로운 빈을 가져오는 방법”이다.

예:

@Autowired
private ObjectProvider<ProtoBean> provider;

public int logic() {
    ProtoBean pb = provider.getObject();
    return pb.getCount();
}

이렇게 하면 매번 새로운 프로토타입 빈을 반환한다.


6. 기술 심층 Part ①

Provider의 내부 구현 구조 (ObjectProvider / JSR-330 Provider)

ObjectProvider 내부 동작

ObjectProvider는 사실상 다음과 같은 코드를 수행한다.

public T getObject() {
    return beanFactory.getBean(T.class);
}

즉, 매번 getBean()을 호출한다 → 매번 새 객체 생성

→ 프로토타입 스코프 규칙을 그대로 만족한다.


7. 웹 스코프 – request, session, application

(쉬운 설명)

웹 환경에서는 요청마다 객체를 달리해야 하는 상황이 많다.

예:

  • 요청 ID
  • 로그인 정보
  • 추적 로거(logging trace)
  • 유저별 데이터
  • 요청별 컨텍스트 정보

그래서 스프링은 특별한 스코프를 제공한다:

스코프생성 시점종료 시점
requestHTTP 요청 시작응답 완료
session세션 생성세션 만료
application서블릿 컨텍스트 생성웹 애플리케이션 종료

예를 들어 request 스코프 빈:

@Component
@Scope(value = "request")
public class MyLogger {}

하지만 여기서 “심각한 문제가 하나 발생한다.”


8. 문제: request 스코프 빈을 싱글톤 빈에 주입하면?

당연히 싱글톤 빈이 먼저 생성되는데,

request 스코프 빈은 HTTP 요청 시점에 만 생성된다.

즉, request 빈을 사용할 시점에 객체가 존재하지 않는다.

스프링은 이 문제를 해결하기 위해 “프록시”를 사용한다.


9. 기술 심층 Part ②

Scoped Proxy(스코프 프록시) 내부 구조 완전 이해

여기서 기술적으로 매우 중요한 개념이 등장한다.

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)

이렇게 작성하면

스프링은 request 스코프 빈 대신 CGLIB 기반 프록시 객체를 주입한다.

이 프록시는 단순한 가짜 객체다.


프록시 객체의 동작 원리

주입되는 객체는 실제 MyLogger가 아니라

“가짜 프록시 클래스(MyLogger$$EnhancerByCGLIB)”다.

프록시는 실제 request 스코프 빈이 필요해지는 순간에만

실제 객체를 찾아 호출한다.

동작 방식은 이렇게 된다:

  1. 빈 주입 시점 → 프록시 객체가 싱글톤 빈에 주입됨
  2. 실제 로직 실행 시점 → 프록시가 현재 RequestContextHolder에서 request 스코프 객체 조회 → 실제 객체를 찾아서 호출(delegate)

RequestContextHolder + ThreadLocal 기반 구조

스프링은 요청별 객체를 구분하기 위해

ThreadLocal을 사용한다.

구조:

  • 요청 1개 = 스레드 1개
  • ThreadLocal에 request 스코프 저장
  • 프록시가 현재 스레드에서 “현재 요청 대상 객체”를 가져옴
  • 실제 객체 호출

이 때문에 request 스코프 빈은 멀티스레드 환경에서도 안전하다.


10. 기술 심층 Part ③

Scope 구현체의 실제 구조

스프링의 모든 스코프는

org.springframework.beans.factory.config.Scope 인터페이스로 관리된다.

핵심 메서드 4개:

Object get(String name, ObjectFactory<?> objectFactory);
Object remove(String name);
void registerDestructionCallback(String name, Runnable callback);
String getConversationId();

예: RequestScope는

  • 요청 시작 시 Map<String, Object> 생성
  • get()에서 map.getOrDefault → 없으면 objectFactory.getObject()로 새 객체 생성
  • 응답 종료 시 callback 실행(@PreDestroy 포함)

11. 정리: 각 스코프의 실제 활용 구간

싱글톤

  • 서비스 계층
  • 리포지토리
  • 정책 객체
  • 대부분의 빈
  • 무상태(stateless) 객체

프로토타입

  • 매번 새로운 상태가 필요한 특정 로직
  • 멀티스레드 작업 객체
  • Builder 역할 객체

request

  • HTTP 요청별 추적 ID(log trace)
  • 요청별 컨텍스트
  • API 호출 정보

session

  • 로그인 사용자 정보

12. 결론

스코프는 스프링을 깊게 이해하는 핵심 요소다

정리하면:

  • 스코프는 빈의 생존 범위를 결정한다
  • 싱글톤은 기본이며 가장 많이 사용
  • 프로토타입은 요청마다 새로운 객체 생성
  • 웹 스코프는 request/session별로 객체 관리
  • 프록시(scoped proxy) 덕분에 웹 스코프 빈도 싱글톤 빈처럼 쉽게 주입 가능
  • Provider는 프로토타입과 request 스코프를 동적으로 생성할 때 핵심
  • 내부적으로는 ThreadLocal, CGLIB, Scope 인터페이스가 핵심 기술 요소

이해하면 스프링이 “왜 이렇게 설계되었는지”

전체 구조가 보이기 시작한다.