프로토타입 빈을 언제 사용할까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
ObjectProvider
, JSR330 Provider
등은 프로토타입 뿐만 아니라 DL이 필요한 경우는 언제든지 사용할 수 있다.
싱글톤 빈이 매번 일회용 빈을 생성하는 상황은 스프링 프레임워크와 같은 의존성 주입 환경에서 종종 발생합니다.
이러한 패턴은 특정 비즈니스 요구 사항을 충족시키기 위해 사용될 수 있습니다.
1.웹 요청 처리: 웹 애플리케이션에서 싱글톤 컨트롤러는 각 요청마다 새로운 요청 데이터 객체를 생성할 수 있습니다. 예를 들어, 사용자의 웹 요청을 처리하는 싱글톤 서비스가 있을 때, 각 요청마다 사용자의 세션 정보나 요청 데이터를 담은 일회용 빈을 생성할 수 있습니다. 이렇게 하면 각 요청이 독립적으로 처리될 수 있으며, 서비스의 상태를 공유하지 않아도 됩니다.
2.사용자 맞춤형 서비스 제공: 싱글톤 서비스가 사용자별 맞춤 설정을 관리하는 경우, 각 사용자 세션에 대해 일회용 빈을 생성하여 개별 사용자의 선호도나 설정을 관리할 수 있습니다. 예를 들어, 사용자별 콘텐츠 추천을 제공하는 서비스에서는 각 사용자 세션마다 다른 추천 알고리즘 인스턴스를 생성할 수 있습니다.
의존관계를 외부에서 주입(DI) 받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup (DL) 의존관계 조회(탐색) 이라한다. = 필요한 시점에 늦게 호출가능 = 빈의 생성 지연 기능
...
@Autowired
private ApplicationContext ac;
...
public int logic() {
// DI를 통해 PrototypeBean을 가져오지 않고
// PrototyBean.class 를 직접 찾아서 PrototypeBean을 가져왔다. --> DL
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); --> DL
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
실행해보면 ac.getBean()
을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.
지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가 가 있으면 된다.
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider
이다. 참고로 과거에는 ObjectFactory
가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider
가 만들어졌다.
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider; --> DL 서비스
...
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); -->DL!
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
실행해보면 prototypeBeanProvider.getObject()
을 통해서 항상 새로운 프로토타입 빈이 생성되는 것 을 확인할 수 있다.
ObjectProvider
의 getObject()
를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환 한다. (DL)
스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.
ObjectProvider` 는 지금 딱 필요한 DL 정도의 기능만 제공한다.
특징
- ObjectFactory: 기능이 단순, 별도의 라이브러리 필요 없음, 스프링에 의존
- ObjectProvider: ObjectFactory 상속, 옵션, 스트림 처리등 편의 기능이 많고, 별도의 라이브러리 필요 없음, 스프링에 의존
마지막 방법은 javax.inject.Provider
라는 JSR-330 자바 표준을 사용하는 방법이다.
jakarta.inject:jakarta.inject-api:2.0.1
라이브러리를 gradle에 추가해야 한다.
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
실행해보면 provider.get()
을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다. provider
의 get()
을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다. (DL)자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다. Provider
는 지금 딱 필요한 DL 정도의 기능만 제공한다.
특징
- get()` 메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
여기가 핵심이다. proxyMode = ScopedProxyMode.TARGET_CLASS
를 추가해주자. 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS
를 선택
적용 대상이 인터페이스면 INTERFACES
를 선택
이렇게 하면 MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
@Scope
의 proxyMode = ScopedProxyMode.TARGET_CLASS)
를 설정하면 스프링 컨테이너는 CGLIB
라는 바이트코드를 조작하는 라이브러리를 사용해서, MyLogger를 상속받은 가짜 프록시 객체를 생성한다. 결과를 확인해보면 우리가 등록한 순수한 MyLogger 클래스가 아니라 `MyLogger$
$EnhancerBySpringCGLIB이라는 클래스로 만들어진 객체가 대신 등록된 것을 확인할 수 있다. 그리고 스프링 컨테이너에 "myLogger"라는 이름으로 진짜 대신에 이 가짜 프록시 객체를 등록한다.
ac.getBean("myLogger", MyLogger.class)` 로 조회해도 프록시 객체가 조회되는 것을 확인할 수 있 다.
그래서 의존관계 주입도 이 가짜 프록시 객체가 주입된다.
가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
가짜 프록시 객체는 내부에 진짜 myLogger
를 찾는 방법을 알고 있다.
클라이언트가 myLogger.log()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다.
가짜 프록시 객체는 request 스코프의 진짜 myLogger.log()
를 호출한다.
가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사 실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다(다형성)
동작 정리
CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
이 가짜 프록시 객체는 실제 요청이 오면 그때 내부에서 실제 빈을 요청하는 위임 로직이 들어있다.
가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱 글톤 처럼 동작한다.
특징 정리
프록시 객체 덕분에 클라이언트는 마치 싱글톤 빈을 사용하듯이 편리하게 request scope를 사용할 수 있다. 사실 Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 이것이 바로 다형성과 DI 컨테이너 가 가진 큰 강점이다.
꼭 웹 스코프가 아니어도 프록시는 사용할 수 있다.
주의점
마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.
예를들어
@Component
@Scope(value = "request")
public class MyLogger { ... }
------------------------------------------------
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; --> DL !
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject(); --> DL !
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) -->DL!
public class MyLogger {
}
----------------------
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}