Spring Bean의 스코프(Scope) 종류를 설명해주세요.
힌트
singleton, prototype, request, session을 생각해보세요.
정답 및 해설
Spring Bean의 스코프(Scope) 종류를 설명해주세요.
Spring Bean 스코프는 Spring IoC 컨테이너가 Bean 인스턴스를 어떻게 생성하고 관리할지를 결정하는 설정입니다. 스코프에 따라 Bean의 생명 주기와 인스턴스 공유 방식이 달라집니다. 올바른 스코프 선택은 애플리케이션의 메모리 효율성, 스레드 안전성, 성능에 직접적인 영향을 미칩니다.
Bean 스코프 종류
Spring은 기본적으로 6가지 스코프를 제공합니다. 이 중 singleton과 prototype은 모든 Spring 애플리케이션에서 사용 가능하며, 나머지 4가지는 웹 기반 애플리케이션에서만 사용 가능합니다.
| 스코프 | 설명 | 사용 가능 환경 |
|---|---|---|
| singleton | 컨테이너당 하나의 인스턴스 | 모든 환경 |
| prototype | 요청마다 새 인스턴스 | 모든 환경 |
| request | HTTP 요청마다 새 인스턴스 | 웹 환경 |
| session | HTTP 세션마다 새 인스턴스 | 웹 환경 |
| application | ServletContext당 하나 | 웹 환경 |
| websocket | WebSocket 세션당 하나 | 웹 환경 |
1. Singleton 스코프 (기본값)
개요
Singleton은 Spring Bean의 기본 스코프입니다. Spring IoC 컨테이너당 하나의 Bean 인스턴스만 생성되며, 이후 해당 Bean을 요청하는 모든 곳에서 동일한 인스턴스를 공유합니다.
@Component // 기본 스코프: singleton
public class UserService {
// 모든 요청에서 동일한 인스턴스 사용
}
// 명시적으로 singleton 지정
@Component
@Scope("singleton")
public class UserService {
}
// 또는 Java Config에서
@Configuration
public class AppConfig {
@Bean
@Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
public UserService userService() {
return new UserService();
}
}
동작 원리
Spring Container 시작
|
v
UserService 인스턴스 생성 (1개)
|
v
Bean Registry에 등록
|
┌────┴────┐
v v
Request1 Request2 → 동일한 인스턴스 반환
주의사항: 상태(State) 관리
Singleton Bean은 여러 스레드가 공유하므로 상태(인스턴스 변수)를 가지면 스레드 안전 문제가 발생합니다.
// 잘못된 예 - 상태를 가진 Singleton Bean (스레드 안전 문제)
@Service
public class OrderService {
private int orderCount = 0; // 위험! 여러 스레드가 공유
public void createOrder() {
orderCount++; // 레이스 컨디션 발생 가능
}
}
// 올바른 예 - 상태 없는 Singleton Bean
@Service
public class OrderService {
private final OrderRepository orderRepository;
public OrderService(OrderRepository orderRepository) {
this.orderRepository = orderRepository;
}
public void createOrder(OrderDto dto) {
// 로컬 변수만 사용, 인스턴스 상태 없음
Order order = new Order(dto.getProductId(), dto.getQuantity());
orderRepository.save(order);
}
}
적합한 사용 사례
- Service, Repository, Controller 등 비즈니스 로직 처리 컴포넌트
- 설정 정보를 보유한 Configuration 클래스
- 상태를 갖지 않는 유틸리티 컴포넌트
2. Prototype 스코프
개요
Prototype 스코프는 Bean을 요청할 때마다 새로운 인스턴스를 생성합니다. 상태를 갖는 Bean에 적합하며, Spring 컨테이너는 인스턴스 생성 및 초기화까지만 관여하고, 이후 소멸은 관여하지 않습니다.
@Component
@Scope("prototype")
public class ShoppingCart {
private List<CartItem> items = new ArrayList<>();
public void addItem(CartItem item) {
items.add(item);
}
public List<CartItem> getItems() {
return Collections.unmodifiableList(items);
}
}
중요한 특징: 소멸 콜백 미호출
@Component
@Scope("prototype")
public class PrototypeBean implements DisposableBean {
@PostConstruct
public void init() {
System.out.println("초기화 호출됨"); // 호출됨
}
@Override
public void destroy() {
System.out.println("소멸 호출됨"); // 호출되지 않음!
}
}
Prototype Bean은 GC(Garbage Collector)에 의해 소멸되므로, @PreDestroy나 DisposableBean.destroy()가 호출되지 않습니다. 리소스 정리가 필요한 경우 직접 관리해야 합니다.
적합한 사용 사례
- 사용자별로 독립적인 상태가 필요한 객체 (장바구니, 폼 데이터)
- 매번 새로운 인스턴스가 필요한 명령(Command) 패턴
- 스레드 안전을 위해 공유하면 안 되는 객체
3. Request 스코프
개요
HTTP 요청마다 새로운 Bean 인스턴스를 생성하고, 요청이 종료되면 소멸합니다. 웹 애플리케이션에서만 사용 가능합니다.
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
private String requestId;
private long startTime;
@PostConstruct
public void init() {
this.requestId = UUID.randomUUID().toString();
this.startTime = System.currentTimeMillis();
}
public String getRequestId() {
return requestId;
}
}
proxyMode 설정의 필요성
Singleton Bean에서 Request Scoped Bean을 주입받을 때, Singleton은 애플리케이션 시작 시 초기화되지만 Request Bean은 요청이 들어올 때 생성되어야 하므로 프록시(Proxy) 를 사용해야 합니다.
// proxyMode 없이 사용 시 문제
@Service
public class MyService {
@Autowired
private RequestScopedBean requestBean; // 주입 시점에 Request Bean이 없음 → 오류
}
// proxyMode 설정으로 해결
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
// ...
}
4. Session 스코프
개요
HTTP 세션마다 하나의 Bean 인스턴스를 생성합니다. 사용자의 세션이 유지되는 동안 동일한 인스턴스를 사용하며, 세션이 만료되면 소멸합니다.
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class UserSession {
private String userId;
private String username;
private List<String> recentlyViewed = new ArrayList<>();
// getter/setter
public void addRecentlyViewed(String productId) {
recentlyViewed.add(productId);
if (recentlyViewed.size() > 10) {
recentlyViewed.remove(0);
}
}
}
적합한 사용 사례
- 로그인한 사용자 정보 관리
- 사용자별 장바구니 데이터
- 사용자 선호 설정 (테마, 언어 등)
5. Application 스코프
개요
ServletContext당 하나의 인스턴스를 생성합니다. Singleton과 유사하지만, 여러 Spring ApplicationContext가 있는 경우 ServletContext를 공유하는 범위에서 단 하나의 인스턴스를 보장합니다.
@Component
@Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
public class AppConfiguration {
private Map<String, String> globalConfig = new ConcurrentHashMap<>();
public void setConfig(String key, String value) {
globalConfig.put(key, value);
}
public String getConfig(String key) {
return globalConfig.get(key);
}
}
Singleton Bean에서 Prototype Bean 주입 문제
문제 상황
Singleton Bean이 Prototype Bean을 주입받으면, Singleton이 초기화될 때 단 한 번만 Prototype Bean이 생성되어 이후 항상 같은 인스턴스를 사용하게 됩니다. 이는 Prototype의 의도와 다릅니다.
@Component
@Scope("prototype")
public class PrototypeBean {
private final String id = UUID.randomUUID().toString();
public String getId() {
return id;
}
}
@Service
public class SingletonService {
@Autowired
private PrototypeBean prototypeBean; // 문제! 항상 같은 인스턴스
public String getPrototypeId() {
return prototypeBean.getId(); // 항상 동일한 ID 반환
}
}
해결 방법 1: ObjectProvider 사용 (권장)
@Service
public class SingletonService {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public String getPrototypeId() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
return prototypeBean.getId(); // 매번 새로운 인스턴스
}
}
해결 방법 2: ApplicationContext 사용
@Service
public class SingletonService implements ApplicationContextAware {
private ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public String getPrototypeId() {
PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
return prototypeBean.getId(); // 매번 새로운 인스턴스
}
}
해결 방법 3: @Lookup 어노테이션
@Service
public abstract class SingletonService {
@Lookup
public abstract PrototypeBean getPrototypeBean(); // Spring이 구현체를 동적 생성
public String getPrototypeId() {
return getPrototypeBean().getId(); // 매번 새로운 인스턴스
}
}
스코프 선택 가이드
상태가 없는 컴포넌트?
→ YES: Singleton (기본값)
→ NO:
웹 환경인가?
→ NO: Prototype
→ YES:
요청마다 독립적? → Request Scope
세션 동안 유지? → Session Scope
애플리케이션 전역? → Application Scope
요약 표
| 스코프 | 인스턴스 수 | 생명 주기 | 소멸 콜백 | 주요 사용 사례 |
|---|---|---|---|---|
| singleton | 컨테이너당 1개 | 컨테이너 시작~종료 | 호출됨 | Service, Repository, Controller |
| prototype | 요청마다 새 인스턴스 | 요청~GC 수집 | 호출 안됨 | 상태 있는 객체, Command 패턴 |
| request | HTTP 요청마다 1개 | 요청 시작~종료 | 호출됨 | 요청별 컨텍스트, 로그 추적 |
| session | HTTP 세션마다 1개 | 세션 시작~만료 | 호출됨 | 사용자 세션 데이터 |
| application | ServletContext당 1개 | 애플리케이션 시작~종료 | 호출됨 | 전역 설정 |
| websocket | WebSocket 세션당 1개 | 소켓 연결~종료 | 호출됨 | WebSocket 채널 데이터 |