스프링은 스프링 컨테이너의 시작과 동시에 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지되는 싱글톤 스코프 뿐만 아니라 다양한 스코프를 지원한다.
프로토타입 스코프는 싱글톤 스코프와는 다르게 요청이 올 때마다 항상 새로운 인스턴스를 생성하여 반환한다. 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 관리하고 그 후에 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 따라서 빈을 관리할 책임은 클라이언트에 있으며, @PreDestroy
같은 종료 메서드는 호출되지 않는다.
프로토타입 스코프로 빈을 등록하는 예시이다. @Scope("prototype")
를 사용하였다. 초기화 이후에는 스프링 컨테이너가 관리하지 않기에, @PreDestroy
메서드는 호출되지 않는다.
@Scope("prototype")
public class PrototypeBean{
@PostConstruct
public void init(){
System.out.println("SingletonBean.init");
}
}
싱글톤 빈에서 프로토타입 빈을 주입받아 사용할 경우를 생각해보자. 이 경우 문제가 발생할 수 있다. 싱글톤 빈은 컨테이너에 등록될 때에 프로토타입 빈을 딱 한 번 주입받아 컨테이너에 등록한다. 그 후 싱글톤 빈을 요청할 때마다 등록될 때 생성된 빈을 사용하는데, 여기서 문제가 발생한다. 항상 같은 싱글톤 빈을 사용하기 때문에 요청할 때마다 프로토타입 빈이 생성되어 주입되는 것이 아니고, 프로토타입 빈도 딱 한번만 생성되어 주입되게 된다.
지정한 빈을 컨테이너에서 찾아주는 DL(Dependency Lookup) 기능을 제공하는 ObjectFactory와 ObjectProvider가 있다.
@Scope("singleton")
static class ClientBean{
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
private int logic(){
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
ObjectProvider<PrototypeBean>
: PrototypeBean 프로토타입 클래스를 주입받는다. prototypeBeanProvider.getObject()
: getObject()
메서드를 호출할 때마다 새로운 인스턴스를 생성하여 반환한다.ObjectFactory는 getObject() 메서드를 제공하며, ObjectProvider는 ObjectFactory를 상속받아 더 많은 기능을 제공한다.
자바 표준을 사용하는 방법도 있다.
@Scope("singleton")
static class ClientBean{
@Autowired
private Provider<PrototypeBean> provider;
private int logic(){
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
private Provider<PrototypeBean>
: javax.inject.Provider를 사용한다.provider.get()
: 프로토타입 빈을 생성해 반환한다.get() 메서드 하나로 기능이 매우 단순하며, 자바 표준을 준수하는 방법이다.
웹 스코프는 웹 환경에서만 동작하며, 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리하기에 종료 메서드가 호출된다.
웹 스코프를 사용하기 위해 스프링 웹 라이브러리를 bulid.gradle에 추가한다. 이제 내장 톰켓 서버를 통해 웹 서버와 스프링을 함께 실행시킨다.
request스코프 빈은 고객 요청이 왔을 때 생성되기 때문에 스프링 애플리케이션 실행과 동시에 주입을 시도하면 오류가 발생한다. 이에 Provider를 사용해 해결할 수 있다.
@Component
@Scope(value = "request")
public class MyLogger {
public String uuid;
public String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created:" + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
@Scope(value = "request")
를 통해 고객 요청이 왔을 때 생성되는 request 스코프로 사용된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
public final LogDemoService logDemoService;
public final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
public final ObjectProvider<MyLogger>
: provider를 사용한다.myLoggerProvider.getObject()
: MyLogger 빈을 반환한다. 이 때 MyLogger 빈이 생성되게 된다.@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id = " + id);
}
}
myLoggerProvider.getObject()
: 컨트롤러에서 생성된 MyLogger 빈을 반환한다.
ObjectProvider
를 사용하면 컨트롤러, 서비스가 컨테이너에 등록될 때 MyLogger를 주입하는 것이 아닌 DL(Dependency Lookup)를 할 수 있는 객체가 주입된다. 프록시 방식을 사용하면 마치 싱글톤을 사용하는 것처럼 편리하게 request scope를 사용할 수 있다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
public String uuid;
public String requestURL;
public void setRequestURL(String requestURL) {
this.requestURL = requestURL;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
public void init(){
uuid = UUID.randomUUID().toString();
System.out.println("[" + uuid + "] request scope bean created:" + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close:" + this);
}
}
proxyMode = ScopedProxyMode.TARGET_CLASS
: MyLogger의 가짜 프록시 클래스를 만들어 다른 빈에 주입된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
public final LogDemoService logDemoService;
public 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";
}
}
실제 myLogger가 아닌 프록시 myLogger가 주입된다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
를 사용하면 실제 빈을 등록하는 것이 아닌, 이를 상속한 가짜 프록시 객체를 생성해 스프링 컨테이너에 등록한다. (CGLIB에 의한 바이트코드 조작)본 포스팅은 김영한 강사의 스프링 핵심 원리 강의를 수강하고 요약한 것으로, 해당 강의의 영상 및 강의자료를 참고하였습니다.