BeanScopeAndProvider
스프링 빈이 존재할 수 있는 범위(스코프)에 대한 개념과 종류, 그리고 프로토타입 스코프를 활용할 때 발생하는 문제점과 해결책을 정리했습니다.
1. 빈 스코프란? 📌
정의 💡 스프링 빈이 생성되고 존재할 수 있는 범위를 의미합니다.
기본 스코프 ⚡ 기본적으로 모든 스프링 빈은 싱글톤 스코프로 생성됩니다.
2. 스프링이 지원하는 스코프 종류 🗂️
싱글톤(Singleton) 🏠
스프링 컨테이너 시작과 종료까지 유지되는 가장 넓은 범위
기본 스코프로 별도 설정 없이 사용됨
프로토타입(Prototype) 🧬
스프링 컨테이너는 빈 생성과 의존관계 주입까지만 관여
이후 관리는 클라이언트에 위임하는 매우 짧은 범위
웹 관련 스코프 🌐
request: 웹 요청이 들어오고 나갈 때까지 유지
session: 웹 세션이 생성되고 종료될 때까지 유지
application: 웹의 서블릿 컨텍스트와 같은 범위로 유지
3. 빈 스코프 지정 방법 📋
컴포넌트 스캔으로 자동 등록 🚀
@Scope("prototype")
@Component
public class HelloBean {}
설정 클래스에서 수동 등록 ⚙️
@Scope("prototype")
@Bean
PrototypeBean helloBean() {
return new HelloBean();
}
4. 싱글톤 스코프와 프로토타입 스코프 비교 ⚖️
싱글톤 스코프 특징 🏆
생명주기 🕰️
스프링 컨테이너 시작 시 생성
스프링 컨테이너 종료 시까지 유지
인스턴스 👤
항상 같은 인스턴스를 반환
상태를 공유하므로 stateless 설계 필요
동작 과정 🔄
스프링 컨테이너 생성 시 빈 초기화
컨테이너에서 빈 요청 시 항상 같은 객체 반환
컨테이너 종료 시 빈의 소멸 메서드(@PreDestroy) 실행
// 싱글톤 빈 테스트 예시
@Test
public void singletonBeanFind() {
// 컨테이너 생성
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(SingletonBean.class);
// 빈 두 번 조회
SingletonBean bean1 = ac.getBean(SingletonBean.class);
SingletonBean bean2 = ac.getBean(SingletonBean.class);
// 동일한 인스턴스인지 검증
assertThat(bean1).isSameAs(bean2);
ac.close(); // 종료
}
프로토타입 스코프 특징 🔄
생명주기 🕰️
스프링 컨테이너에서 빈 조회 시점에 생성
생성, 의존관계 주입, 초기화까지만 스프링이 관여
이후 관리는 클라이언트에 위임
인스턴스 👥
조회할 때마다 항상 새로운 인스턴스 생성
상태를 공유하지 않음
동작 과정 🔄
컨테이너에서 빈 요청 시 새 객체 생성
의존관계 주입 및 초기화
클라이언트에 반환 후 컨테이너는 더 이상 관리하지 않음
소멸 메서드(@PreDestroy) 실행되지 않음
// 프로토타입 빈 테스트 예시
@Test
public void prototypeBeanFind() {
// 컨테이너 생성
AnnotationConfigApplicationContext ac =
new AnnotationConfigApplicationContext(PrototypeBean.class);
// 빈 두 번 조회
PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
// 서로 다른 인스턴스인지 검증
assertThat(bean1).isNotSameAs(bean2);
ac.close(); // 종료 (소멸 메서드 실행 안됨)
}
5. 프로토타입 스코프의 문제점 ⚠️
싱글톤 빈과 함께 사용 시 문제 ⚡
의도치 않은 동작 🔍
싱글톤 빈이 프로토타입 빈을 주입받으면 최초 한 번만 주입됨
싱글톤 빈이 살아있는 동안 같은 프로토타입 빈을 계속 사용하게 됨
프로토타입 빈을 사용할 때마다 새 인스턴스를 받는 것이 아님!
// 문제 상황 예시
@Test
void singletonClientUsePrototype() {
// 설정
AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
// 첫 번째 클라이언트 빈 조회 및 로직 실행
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1); // 카운트는 1
// 두 번째 클라이언트 빈 조회 및 로직 실행
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2); // 카운트는 2 (같은 프로토타입 빈 사용)
}
// 싱글톤 클라이언트 빈
static class ClientBean {
private final PrototypeBean prototypeBean; // 프로토타입 빈을 주입받음
@Autowired
public ClientBean(PrototypeBean prototypeBean) {
this.prototypeBean = prototypeBean;
}
public int logic() {
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
원인 📝
싱글톤 빈은 생성 시점에만 의존관계 주입이 일어남
프로토타입 빈은 주입 시점에 새로 생성되지만, 이후에는 새로 생성되지 않음
결과적으로 싱글톤 빈 내부의 프로토타입 빈은 싱글톤처럼 동작
6. 프로토타입 스코프 문제 해결책 🛠️
1. ApplicationContext 직접 사용 📋
// ApplicationContext를 주입받아 직접 조회
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
// 사용할 때마다 스프링 컨테이너에 새로 요청
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
장점 ✅
매번 새로운 프로토타입 빈 생성 보장
단점 ❌
스프링 컨테이너에 종속적인 코드
단위 테스트 어려움
2. ObjectProvider/ObjectFactory 사용 🔄
// ObjectProvider 사용
static class ClientBean {
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
return prototypeBean.getCount();
}
}
특징 🔍
ObjectFactory: 기본 기능만 제공
ObjectProvider: ObjectFactory 상속, 편의 기능 추가
스프링 의존적이지만 기능이 단순해 테스트 용이
별도 라이브러리 필요 없음
3. JSR-330 Provider 사용 🧩
// JSR-330 Provider 사용 (스프링 부트 3.0 미만)
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
return prototypeBean.getCount();
}
// 스프링 부트 3.0 이상
// jakarta.inject:jakarta.inject-api:2.0.1 라이브러리 추가 필요
@Autowired
private jakarta.inject.Provider<PrototypeBean> provider;
특징 🔍
자바 표준(JSR-330)
기능이 단순(get() 메서드만 존재)
스프링이 아닌 다른 컨테이너에서도 사용 가능
별도 라이브러리 추가 필요
스프링 부트 3.0 미만: javax.inject:javax.inject:1
스프링 부트 3.0 이상: jakarta.inject:jakarta.inject-api:2.0.1
7. 실무 활용 가이드 💼
언제 프로토타입 스코프를 사용할까? 🤔
매번 사용할 때마다 새로운 객체가 필요한 경우
의존관계 주입이 완료된 새로운 객체가 필요한 경우
실무에서는? 💻
싱글톤으로 대부분의 문제 해결 가능
프로토타입 빈을 직접 사용하는 경우는 매우 드묾
Provider는 DL이 필요한 모든 경우에 사용 가능
선택 가이드 🧭
스프링 의존적 코드를 작성해도 괜찮다면 → ObjectProvider (편의성 높음)
다른 컨테이너에서도 코드를 사용해야 한다면 → JSR-330 Provider (자바 표준)
별도 기능이 필요 없고 스프링에 의존해도 된다면 → 스프링이 제공하는 기능 사용
8. 핵심 포인트 요약 📚
싱글톤 빈
컨테이너 시작부터 종료까지 하나의 인스턴스 유지
상태를 공유하므로 stateless 설계 필요
@PreDestroy 같은 종료 메서드 호출됨
프로토타입 빈
조회할 때마다 새로운 인스턴스 생성
스프링은 생성, 의존관계 주입, 초기화까지만 관여
@PreDestroy 같은 종료 메서드 호출되지 않음
클라이언트가 관리 책임을 가짐
문제 해결 방법
ApplicationContext 직접 사용 (비권장)
ObjectProvider/ObjectFactory 사용 (스프링 의존)
JSR-330 Provider 사용 (자바 표준)
DL (Dependency Lookup, 의존관계 조회)
의존관계를 외부 주입이 아닌 직접 필요한 의존관계를 찾는 방식
Provider는 DL 기능만 제공하는 가벼운 컨테이너
9. DL(Dependency Lookup)의 이해 🔍
9.1 DI와 DL의 차이점 ⚖️
DI (Dependency Injection) 💉
의존관계를 외부(스프링 컨테이너)에서 주입받는 방식
객체가 필요한 의존성을 직접 찾지 않고 수동적으로 받기만 함
생성자, 수정자, 필드 주입 등으로 구현
코드 예시:
@Service public class UserService { private final UserRepository userRepository; // 스프링이 UserRepository를 주입해줌 (DI) public UserService(UserRepository userRepository) { this.userRepository = userRepository; } }
DL (Dependency Lookup) 🔎
의존관계를 필요한 시점에 직접 컨테이너에 요청하여 찾는 방식
객체가 능동적으로 의존성을 찾음
지연 로딩(Lazy Loading) 구현에 유용
코드 예시:
@Service public class UserService { private final ApplicationContext context; public UserService(ApplicationContext context) { this.context = context; } public void process() { // 필요한 시점에 직접 빈을 찾음 (DL) UserRepository repository = context.getBean(UserRepository.class); repository.save(); } }
9.2 DL이 필요한 상황 🎯
프로토타입 스코프 빈
싱글톤 빈에서 매번 새로운 프로토타입 빈이 필요할 때
지연 초기화(Lazy Initialization)
무거운 리소스를 사용하는 빈을 실제 사용 시점까지 생성 지연
선택적 의존성(Optional Dependencies)
특정 조건에서만 의존성이 필요한 경우
순환 참조 해결
두 빈이 서로 의존하는 순환 참조 문제 회피
동적 빈 선택
런타임에 조건에 따라 다른 빈을 선택해야 할 때
9.3 DL 구현 방식의 진화 📈
ApplicationContext 직접 사용 (초기 방식)
@Autowired private ApplicationContext context; public void doSomething() { PrototypeBean bean = context.getBean(PrototypeBean.class); bean.process(); }
문제점: 컨테이너 전체를 주입받아 무겁고, 테스트하기 어려움
ObjectFactory 도입 (개선)
@Autowired private ObjectFactory<PrototypeBean> beanFactory; public void doSomething() { PrototypeBean bean = beanFactory.getObject(); bean.process(); }
특징: 단일 빈 타입만 조회하는 기능으로 한정
ObjectProvider 확장 (현재 권장)
@Autowired private ObjectProvider<PrototypeBean> beanProvider; public void doSomething() { PrototypeBean bean = beanProvider.getObject(); bean.process(); }
특징: 스트림 지원, 옵셔널 처리 등 편의 기능 추가
JSR-330 Provider (표준)
@Autowired private Provider<PrototypeBean> provider; public void doSomething() { PrototypeBean bean = provider.get(); bean.process(); }
특징: 자바 표준 API로 프레임워크 독립적
10. Provider의 내부 동작 원리 🧠
10.1 ObjectProvider의 동작 메커니즘 ⚙️
기본 구조 📊 ObjectProvider는 기본적으로 ObjectFactory를 상속한 인터페이스입니다.
public interface ObjectProvider<T> extends ObjectFactory<T>, Iterable<T> { // 추가 메서드들... }
핵심 동작 원리 🔄
ObjectProvider는 빈을 직접 가지고 있지 않고, 빈을 찾아올 수 있는 위임 로직만 포함
getObject() 호출 시 내부적으로 BeanFactory에 위임하여 요청한 빈을 찾음
스프링 컨테이너의 DefaultListableBeanFactory 클래스에서 실제 빈을 조회
// 내부 구현 예시 (실제 코드 간소화) public class DefaultObjectProvider<T> implements ObjectProvider<T> { private final BeanFactory beanFactory; private final String beanName; private final Class<T> type; @Override public T getObject() { // 실제 빈 팩토리에 빈 조회 요청을 위임 return beanFactory.getBean(beanName, type); } }
프로토타입 빈 처리 🔍 ObjectProvider가 프로토타입 빈을 요청할 때 컨테이너는:
요청 시점에 해당 빈의 새 인스턴스를 생성
의존관계 주입 및 초기화 메서드 실행
생성된 새 인스턴스를 반환
각 getObject() 호출마다 위 과정 반복
스코프 인식 기능 🌐
ObjectProvider는 스코프를 인식하여 각 스코프에 맞는 빈 인스턴스 제공
웹 request 스코프인 경우 현재 HTTP 요청에 맞는 빈 인스턴스 반환
10.2 JSR-330 Provider의 동작 원리 🧩
자바 표준 API 설계 📐
javax.inject 패키지(또는 jakarta.inject)의 Provider 인터페이스는 단순한 구조로 설계됨
public interface Provider<T> { T get(); }
스프링에서의 구현 🔄
스프링은 내부적으로 JSR-330 Provider를 위한 어댑터 클래스를 제공
이 어댑터는 실제 스프링의 BeanFactory를 통해 빈을 찾는 로직을 구현
// 내부 구현 예시 (실제 코드 간소화) class ProviderAdapter<T> implements Provider<T> { private final BeanFactory beanFactory; private final String beanName; private final Class<T> type; @Override public T get() { // 스프링 컨테이너에 빈 조회를 위임 return beanFactory.getBean(beanName, type); } }
생명주기와 스코프 관리 ⏱️
Provider.get() 호출 시 스프링은 해당 빈의 스코프에 맞게 인스턴스 관리
프로토타입 빈의 경우 매번 새로운 인스턴스 생성
웹 스코프 빈의 경우 해당 스코프에 맞는 인스턴스 제공
10.3 Provider의 확장 기능 🔋
ObjectProvider의 확장 기능
// 옵셔널 처리
PrototypeBean bean = beanProvider.getIfAvailable(() -> new PrototypeBean());
// 스트림 처리
beanProvider.stream()
.filter(bean -> bean.isEnabled())
.forEach(bean -> bean.process());
// 조건부 처리
beanProvider.ifAvailable(bean -> bean.process());
실제 사용 예시 - 전략 패턴 구현
@Service
public class PaymentService {
private final ObjectProvider<List<PaymentStrategy>> strategiesProvider;
public PaymentService(ObjectProvider<List<PaymentStrategy>> strategiesProvider) {
this.strategiesProvider = strategiesProvider;
}
public void processPayment(String type, int amount) {
// 필요한 시점에 모든 전략을 가져와서 적합한 것을 선택
PaymentStrategy strategy = strategiesProvider.getObject().stream()
.filter(s -> s.supports(type))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("지원하지 않는 결제 방식"));
strategy.pay(amount);
}
}
11. Provider 활용 고급 패턴 🚀
11.1 팩토리 패턴 구현 🏭
Provider를 사용하여 빈 팩토리 패턴을 간결하게 구현할 수 있습니다.
@Component
public class PrototypeBeanFactory {
private final Provider<PrototypeBean> prototypeBeanProvider;
public PrototypeBeanFactory(Provider<PrototypeBean> prototypeBeanProvider) {
this.prototypeBeanProvider = prototypeBeanProvider;
}
public PrototypeBean createBean(String data) {
PrototypeBean bean = prototypeBeanProvider.get();
bean.setData(data);
return bean;
}
}
11.2 지연 초기화 패턴 ⏲️
무거운 빈의 초기화를 실제 사용 시점까지 지연시키는 패턴입니다.
@Service
public class HeavyResourceService {
private final Provider<ExpensiveResource> resourceProvider;
private ExpensiveResource cachedResource;
public HeavyResourceService(Provider<ExpensiveResource> resourceProvider) {
this.resourceProvider = resourceProvider;
}
public void processData() {
// 필요한 시점에 한 번만 초기화
if (cachedResource == null) {
cachedResource = resourceProvider.get();
}
cachedResource.process();
}
}
11.3 조건부 의존성 패턴 🔀
특정 조건에 따라 다른 구현체를 사용하는 패턴입니다.
@Service
public class ConfigurableService {
private final Provider<List<MessageSender>> senderProvider;
private final Environment env;
public ConfigurableService(Provider<List<MessageSender>> senderProvider, Environment env) {
this.senderProvider = senderProvider;
this.env = env;
}
public void sendMessage(String msg) {
// 환경에 따라 다른 구현체 선택
String profile = env.getActiveProfiles()[0];
MessageSender sender = senderProvider.get().stream()
.filter(s -> s.supportsProfile(profile))
.findFirst()
.orElseGet(() -> senderProvider.get().get(0)); // 기본 구현체
sender.send(msg);
}
}
11.4 프록시 패턴과의 결합 🔄
Provider를 프록시 패턴과 결합하여 유연한 확장이 가능합니다.
@Component
public class TransactionalPrototypeProxy {
private final Provider<PrototypeBean> beanProvider;
private final TransactionManager txManager;
public TransactionalPrototypeProxy(Provider<PrototypeBean> beanProvider,
TransactionManager txManager) {
this.beanProvider = beanProvider;
this.txManager = txManager;
}
public void executeWithTx() {
// 매번 새로운 빈을 트랜잭션 컨텍스트에서 실행
txManager.begin();
try {
PrototypeBean bean = beanProvider.get();
bean.execute();
txManager.commit();
} catch (Exception e) {
txManager.rollback();
throw e;
}
}
}
Last updated