이번 장에서는 스프링 빈의 생존 기간을 결정하는 개념, 즉 스코프(Scope)를 다룬다.
대부분의 스프링 애플리케이션은 싱글톤으로 돌아간다고 생각하기 쉽지만,
실무에서는 request, session, application 같은 웹 스코프나
prototype 스코프를 반드시 알아야 하는 순간이 온다.
- “유저 요청마다 다른 객체가 필요할 때”
- “세션마다 데이터를 저장하고 싶을 때”
- “싱글톤 안에서 프로토타입 빈을 사용해야 할 때”
- “AOP 프록시가 request scope와 어떻게 협력하는지”
이런 상황을 정확히 이해하면
스프링을 사용하는 수준이 초급 → 중급 → 상급으로 바로 올라간다.
1. 스프링 빈 스코프란?
(쉽게 이해하는 설명)
스프링은 빈을 하나 만들고 끝이 아니라,
“그 빈을 언제 만들고 언제 폐기할까?”를 결정해야 한다.
이걸 스프링에서는 스코프(Scope)라고 부른다.
스프링이 기본적으로 제공하는 스코프는 다음 5가지다:
| 스코프 | 설명 |
|---|---|
| singleton | 스프링 컨테이너 시작부터 종료까지 1개 유지 |
| prototype | 호출할 때마다 새로운 빈 생성 |
| request | HTTP 요청마다 새로운 빈 생성 |
| session | HTTP 세션마다 1개 유지 |
| application | 서블릿 컨텍스트 전체에 1개 유지 |
이 중에서도
singleton, prototype, request가 가장 중요하다.
2. 싱글톤 스코프 – 스프링의 기본 전략
(쉬운 설명)
싱글톤은 스프링 애플리케이션에서 가장 많이 사용되는 기본 스코프다.
특징:
- ApplicationContext 초기화 시점에 딱 1개 생성
- 모든 요청에서 같은 객체 사용
- @PreDestroy 시점에 소멸
코드 예:
@Scope("singleton")
@Component
public class SingletonBean {}
스프링 빈의 95%는 싱글톤이다.
이유는 간단하다. 성능이 가장 좋고 메모리 효율적이며
스프링 DI 구조와 가장 잘 맞기 때문이다.
3. 프로토타입 스코프 – 요청할 때마다 새로운 객체 생성
(쉬운 설명)
프로토타입 스코프는 이렇게 동작한다.
- getBean()을 호출할 때마다
- 새로운 객체를 새로 만들고
- 의존관계 주입까지만 수행
- 이후 빈 관리는 개발자 책임
코드 예:
@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)
- 유저별 데이터
- 요청별 컨텍스트 정보
그래서 스프링은 특별한 스코프를 제공한다:
| 스코프 | 생성 시점 | 종료 시점 |
|---|---|---|
| request | HTTP 요청 시작 | 응답 완료 |
| 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 스코프 빈이 필요해지는 순간에만
실제 객체를 찾아 호출한다.
동작 방식은 이렇게 된다:
- 빈 주입 시점 → 프록시 객체가 싱글톤 빈에 주입됨
- 실제 로직 실행 시점 → 프록시가 현재 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 인터페이스가 핵심 기술 요소
이해하면 스프링이 “왜 이렇게 설계되었는지”
전체 구조가 보이기 시작한다.