Spring Bean Scope
Bean Scope란 Spring Container가 Bean 인스턴스를 언제, 얼마나 생성하고 언제까지 유지할지를 정의하는 규칙이다.
스코프 종류 개요
| 스코프 | 적용 범위 | 생성 시점 | 소멸 시점 |
|---|---|---|---|
singleton |
ApplicationContext 전체 | Container 초기화 시 | Container 종료 시 |
prototype |
주입 요청 시마다 | getBean() 호출 시마다 |
Spring이 관리하지 않음 |
request |
HTTP 요청 1건 | 요청 시작 | 요청 종료 |
session |
HTTP 세션 1건 | 세션 시작 | 세션 만료 |
application |
ServletContext 전체 | 서버 시작 | 서버 종료 |
📒
request,session,application은 웹 환경(WebApplicationContext)에서만 사용 가능하다.
Singleton 스코프
개념
Spring의 기본 스코프다. @Bean 어노테이션에 별도 스코프를 명시하지 않으면 자동으로 Singleton이 적용된다.
ApplicationContext 하나당 Bean 인스턴스가 단 하나만 생성되며, 이후 모든 의존성 주입 요청은 동일한 인스턴스를 참조한다.
@Bean
public UserService userService() {
return new UserService(); // 딱 한 번만 생성됨
}
생명주기
ApplicationContext 초기화
→ Bean 인스턴스 1회 생성
→ 모든 주입 요청에 동일 인스턴스 반환
→ ApplicationContext 종료 시 @PreDestroy 호출 후 소멸
특징
- 메모리 효율적: 인스턴스를 1개만 유지한다.
- 상태 공유 주의: 모든 스레드가 같은 인스턴스를 사용하므로, 인스턴스 변수에 상태를 저장하면 동시성 문제가 발생한다.
- Spring이 생명주기 전체를 관리:
@PostConstruct,@PreDestroy가 정상 동작한다.
적합한 사용 사례
- 상태 없는(stateless) 서비스 객체 (대부분의
@Service,@Repository) - 공유 설정 객체
Prototype 스코프
개념
getBean() 호출 또는 의존성 주입이 발생할 때마다 새로운 인스턴스를 생성해서 반환한다.
@Bean
@Scope("prototype")
public HttpSecurity httpSecurity() {
return new HttpSecurity(...); // 주입될 때마다 새 인스턴스 반환
}
생명주기
주입 요청 발생
→ Bean 인스턴스 새로 생성
→ 요청한 쪽에 반환
→ Spring Container는 더 이상 해당 인스턴스를 추적하지 않음
→ 소멸은 요청한 쪽(호출자)의 책임
특징
- 독립성 보장: 각 주입 요청이 서로 다른 인스턴스를 받으므로 상태를 공유하지 않는다.
- Spring이 소멸을 관리하지 않음:
@PreDestroy가 호출되지 않는다. 자원 해제가 필요하다면 호출자가 직접 처리해야 한다. - 메모리 사용량 증가 가능: 요청마다 생성되므로, 많은 요청이 있을 경우 GC 압력이 높아질 수 있다.
적합한 사용 사례
- 상태 있는(stateful) 객체
- 요청마다 독립적인 설정이 필요한 빌더 패턴 객체 (
HttpSecurity등) - 멀티스레드 환경에서 공유하면 안 되는 객체
Singleton vs Prototype 핵심 비교
Singleton
ApplicationContext
└─ UserService (인스턴스 #1)
↑ ↑ ↑
Controller A Controller B Controller C
(모두 같은 인스턴스 참조)
Prototype
ApplicationContext
├─ HttpSecurity (인스턴스 #1) → SecurityConfig #1 에 주입
├─ HttpSecurity (인스턴스 #2) → SecurityConfig #2 에 주입
└─ HttpSecurity (인스턴스 #3) → SecurityConfig #3 에 주입
| 항목 | Singleton | Prototype |
|---|---|---|
| 인스턴스 수 | 1개 | 요청마다 새로 생성 |
| 상태 공유 | 공유됨 | 독립적 |
| Spring 소멸 관리 | O (@PreDestroy 호출) |
X (호출자 책임) |
| 기본값 여부 | O | X |
| 메모리 효율 | 높음 | 낮을 수 있음 |
웹 전용 스코프
웹 전용 스코프를 사용하려면 spring-web 의존성이 필요하고, WebApplicationContext 환경이어야 한다.
Request 스코프
HTTP 요청 1건과 생명주기를 같이한다. 요청이 시작되면 생성되고, 응답이 완료되면 소멸된다.
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestLogger {
private String requestId = UUID.randomUUID().toString();
}
🔥 Singleton Bean에서 Request-scoped Bean을 주입받으려면
proxyMode = ScopedProxyMode.TARGET_CLASS가 필요하다. 없으면 주입 시점에 요청 컨텍스트가 없어서 오류가 발생한다.
Session 스코프
HTTP 세션 1건과 생명주기를 같이한다. 세션이 생성되면 Bean이 만들어지고, 세션이 만료되면 소멸된다.
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ShoppingCart {
private List<Item> items = new ArrayList<>();
}
장바구니, 사용자 설정처럼 한 사용자의 세션 동안 유지되어야 하는 데이터에 적합하다.
Application 스코프
ServletContext와 생명주기를 같이한다. 사실상 Singleton과 동일해 보이지만, Singleton은 ApplicationContext 단위이고 Application 스코프는 ServletContext 단위다. 여러 ApplicationContext가 하나의 ServletContext를 공유하는 경우에 의미가 있다.
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AppCounter {
private AtomicInteger count = new AtomicInteger(0);
}
스코프 지정 방법
어노테이션 방식 (권장)
// 방법 1: 문자열 직접 지정
@Bean
@Scope("prototype")
public SomeBean someBean() { ... }
// 방법 2: 상수 사용 (타입 안전)
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public SomeBean someBean() { ... }
// 방법 3: @Component에 직접 적용
@Component
@Scope("prototype")
public class SomeComponent { ... }
// 방법 4: 웹 스코프 상수 사용
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean { ... }
XML 방식 (레거시)
<bean id="someBean" class="com.example.SomeBean" scope="prototype"/>
Singleton에 Prototype을 주입할 때 발생하는 문제
문제 상황
@Component
public class SingletonService {
@Autowired
private PrototypeBean prototypeBean; // 문제 발생 지점
// Singleton이 초기화될 때 딱 한 번만 주입됨
// 이후 prototypeBean은 항상 같은 인스턴스를 참조함
// → Prototype의 의미가 사라짐
}
SingletonService가 초기화되는 시점에 PrototypeBean이 딱 한 번 생성되어 주입되고, 이후 SingletonService가 살아있는 동안 해당 인스턴스가 계속 재사용된다.
해결 방법 1: ApplicationContext에서 직접 꺼내기
@Component
public class SingletonService implements ApplicationContextAware {
private ApplicationContext context;
public void doSomething() {
PrototypeBean prototypeBean = context.getBean(PrototypeBean.class);
// 호출할 때마다 새 인스턴스
}
@Override
public void setApplicationContext(ApplicationContext context) {
this.context = context;
}
}
해결 방법 2: ObjectProvider 사용 (Spring 4.3+, 권장)
@Component
public class SingletonService {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public void doSomething() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
// 호출할 때마다 새 인스턴스
}
}
해결 방법 3: Scoped Proxy 사용
@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class PrototypeBean { ... }
프록시 객체가 주입되고, 실제 메서드 호출 시점마다 새 인스턴스를 만들어 위임한다. 코드 변경이 가장 적지만, 프록시 메커니즘에 대한 이해가 필요하다.
Spring Security에서 Prototype을 쓰는 이유
HttpSecurityConfiguration에서 HttpSecurity를 Prototype으로 제공하는 이유는 다음과 같다.
@Bean
@Scope("prototype")
HttpSecurity httpSecurity() throws Exception { ... }
SecurityFilterChain은 여러 개를 동시에 등록할 수 있다.
@Bean
@Order(1)
SecurityFilterChain apiChain(HttpSecurity http) throws Exception {
// API 전용 보안 설정
return http.build();
}
@Bean
@Order(2)
SecurityFilterChain defaultChain(HttpSecurity http) throws Exception {
// 일반 웹 보안 설정
return http.build();
}
각 SecurityFilterChain Bean 메서드는 HttpSecurity를 파라미터로 주입받는다. 만약 HttpSecurity가 Singleton이라면 apiChain과 defaultChain이 동일한 HttpSecurity 인스턴스를 공유하게 되고, 서로의 설정이 뒤섞여 버린다.
Prototype으로 선언함으로써 각 SecurityFilterChain 메서드가 독립된 HttpSecurity 인스턴스를 받아 서로 영향을 주지 않는 독립적인 보안 설정을 구성할 수 있다.
Spring Boot 3.x / Spring Framework 6.x 기준