스프링 빈은 기본적으로 싱글톤 스코프라고 지금껏 배워왔다.
스코프는 빈이 존재할 수 있는 범위를 뜻한다.
스프링은 싱글톤 스코프를 포함해 여러 스코프를 지원한다.
싱글톤 스코프 : 기본 스코프로, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
웹 관련 스코프 :
싱글톤 스코프와 달리 항상 새로운 인스턴스를 생성해서 반환한다.
@Scope("prototype")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
System.out.println("prototypeBean1 = " + prototypeBean1);
System.out.println("prototypeBean2 = " + prototypeBean2);
=> 서로 다른 인스턴스를 가진 빈이 두개가 출력되는 것을 볼 수 있다.
스프링 컨테이너가 생성과 의존관계 주입까지만 실행되기 때문에 종료 메서드는 실행되지 않는다.
프로토타입 빈은 프로토타입 빈을 조회한 클라이언트가 관리해야 한다
싱글톤 빈과 함께 사용하면 의도한대로 잘 동작하지 않는다.
싱글톤 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이기 떄문에, 조회를 할 경우 빈이 새로 생기지 않는다.
싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지된다.
싱글톤 빈과 프로토타입을 함께 사용할 때, 항상 새로운 프로토타입 빈을 생성하는 방법
싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새롭게 요청을 한다.
의존관계를 외부에서 주입 받는 것이 아니라 의존관계를 찾는 것을 Dependency Lookup 의존관계 조회라고 한다.
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
}
항상 새로운 프로토타입 빈을 생성해서 조회를 하게 된다.
스프링에서는 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 있다.
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
}
실행해보면 prototypeBeanProvider.getObject()
을 통해서 항상 새로운 프로토타입 빈이 생성된다.
웹 스코프는 웹 환경에서만 동작한다.
프로토타입과는 달리 스프링이 해당 스코프의 종료시점까지 관리한다.
request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고, 관리된다.
session: HTTP Session과 동일한 생명주기를 가지는 스코프
application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
동시에 여러 Http 요청이 오게 되면 어떤 요청이 남긴 로그인지 알기가 어렵다.
이럴 때 사용하는 것이 Request 스코프이다.
웹환경에서만 동작하기 때문에 spring-boot-starter-web 라이브러리를 추가해준다.
implementation 'org.springframework.boot:spring-boot-starter-web'
이제 스프링 부트는 내장 톰켓 서버를 활용해서 웹 서버와 스프링을 함께 실행시킨다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public String getRequestURL() {
return 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 been create" + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] Request scope been closed" + this);
}
}
로그를 출력하기 위한 클래스이고 @Scope(value = "request")로 request 스코프로 지정했다.
이 빈은 http 요청 당 하나씩 생성이 되고, Http 요청이 끝나는 시점에 소멸이된다.
@PostConstruct 초기화 메서드를 이용해 uuid를 생성한다.
@PreDestroy 소멸 메서드를 이용해 이 빈이 소멸될 때 메시지를 남긴다.
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";
} }
로거가 잘 작동하는지 확인하는 테스트용 컨트롤러이다.
http://localhost:8080/log-demo 로 통해 요청 URL을 받아서 진행한다.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
하지만 에러가 나게 된다... 생성을 먼저 했지만, 요청이 없어서 오류가 난다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
가짜 프록시 클래스를 만들어두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.
MyLogger의 순수한 클래스가 아니라 다른 어떠한 객체가 대신 등록이 되어 버린다.
의존관계 주입도 이 가짜 프록시 객체가 주입된다.
가짜 프록시 객체는 요청이 오면 그 때 진짜 빈을 요청하는 로직이 있다.