빈 스코프
- 빈이 존재할 수 있는 범위를 뜻함
- 대표적으로 싱글톤과 프로토타입이 있다. 프로토타입은 빈의 생성과 의존관계 주입만 관여하고 그 후에는 스프링 컨테이너가 관리하지 않는 매우 짧은 범위의 스코프. 항상 새로운 인스턴스를 생성해서 반환함.
- 웹 관련 스코프는 request, session, application이 있다.
- request: 웹 요청이 들어오고 나갈 때까지 유지되는 스코프(ex. 요청별 트랜잭션 ID 저장)
- session: 웹 세션이 생성되고 종료될 때까지 유지되는 스코프(ex. 로그인 사용자 정보 저장)
- application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프(ex. 애플리케이션 설정 정보 저장)
public class SingletonTest {
@Test
void singletonBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
assertThat(singletonBean1).isSameAs(singletonBean2);
ac.close();
}
@Scope("singleton")
static class SingletonBean {
@PostConstruct
public void init() {
System.out.println("SingletonBean.init");
}
// 자동 호출됨.
@PreDestroy
public void destroy() {
System.out.println("SingletonBean.destroy");
}
}
}
// ------------------------------------------------------------------
public class PrototypeTest {
@Test
void prototypeBeanFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
// 클라이언트에서 직접 호출 해줘야함.
prototypeBean1.destroy();
prototypeBean2.destroy();
ac.close();
}
@Scope("prototype")
static class PrototypeBean {
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init");
}
// 스프링 컨테이너가 닫힐 때 자동으로 호출 안됨
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
- 싱글톤 스코프 빈이 프로토타입 스코프 빈을 주입받으면, 그 프로토타입 스코프 빈은 그대로 유지된다. 그래서 다음의 문제가 발생함.
싱글톤 스코프 빈이 프로토타입 스코프 빈을 주입받으면 생기는 문제
public class SingletonWithPrototypeTest1 {
@Test
void prototypeFind() {
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(Prototypebean.class);
Prototypebean prototypeBean1 = ac.getBean(Prototypebean.class);
prototypeBean1.addCount();
assertThat(prototypeBean1.getCount()).isEqualTo(1);
Prototypebean prototypeBean2 = ac.getBean(Prototypebean.class);
prototypeBean2.addCount();
assertThat(prototypeBean2.getCount()).isEqualTo(1);
}
@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);
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(2);
}
@Scope("singleton")
static class ClientBean {
private final Prototypebean prototypebean; // 생성 시점에 주입돼서, 똑같은 프로토타입 빈이 계속 쓰임. ClientBean은 싱글톤이기 때문
@Autowired
public ClientBean(Prototypebean prototypebean) {
this.prototypebean = prototypebean;
}
public int logic() {
prototypebean.addCount();
int count = prototypebean.getCount();
return count;
}
}
@Scope("prototype")
static class Prototypebean {
private int count = 0;
public void addCount() {
++count;
}
public int getCount() {
return count;
}
@PostConstruct
public void init() {
System.out.println("PrototypeBean.init " + this);
}
@PreDestroy
public void destroy() {
System.out.println("PrototypeBean.destroy");
}
}
}
싱글톤 스코프 빈과 프로토타입 스코프 빈 동시 사용 해결책들
- 의존관계를 주입받는 것이 아니라, 직접 찾는 것을 DL(Dependency Lookup)이라고 함.
- ObjectFactory는 getObject()만 제공해서 가벼움. 스프링에 의존
- ObjectProvider는 ObjectFactory를 상속해서 여러 다른 편의 기능을 제공. 스프링에 의존
- JSR-330 Provider는 get()만 제공함. 자바 표준이고, 지금 딱 필요한 DL 정도만 제공. 별도 라이브러리 가져와야 함.
- 프로토타입 스코프 빈은 거의 사용하지 않긴 한다. 이런게 있다 정도로 알고 가자.
웹 스코프
- 웹 환경에서만 동작. 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출됨.
- request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴 스가 생성되고, 관리된다.
- session: HTTP Session과 동일한 생명주기를 가지는 스코프
- application: 서블릿 컨텍스트(ServletContext)와 동일한 생명주기를 가지는 스코프
- websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private 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 create: " + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "]" + "request scope bean close: " + this);
}
}
// --------------------------------------------------------------------
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id: " + id);
}
}
// -------------------------------------------------------------------------
@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.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
- 이러면 MyLogger가 request 스코프라, 오류가 발생한다. 요청이 들어와야 생성되는데, 요청이 안들어 왔으니 스프링 컨테이너가 없어서 주입을 못해주는 것이다.
스코프와 Provider
- ObjectProvider.getObject()를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있음.
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider;
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.log("service id: " + id);
}
}
// ------------------------------------------------------------
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) {
MyLogger myLogger = myLoggerProvider.getObject();
String requestURL = request.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
클라이언트 마다 별도 빈이 생성되고, 다른 데서 접근해도 동일한 스프링 빈을 접근하게 됨.
스코프와 프록시
- 적용 대상이 클래스면, proxyMode를 TARGET_CLASS, 인터페이스면 TARGET_INTERFACE로 선택.
- 실제 MyLogger가 아니라, 가짜 MyLogger를 넣어둠. 그러다가 요청이 들어오면 그 때 생성해서 진짜를 주입해주는 것.
- 가짜 프록시 객체의 log()가 호출되고, 가짜 프록시 객체가 실제 request 스코프의 myLogger의 log()를 호출해줌.
- 덕분에 싱글톤 스코프의 빈을 사용하는 것처럼 편하게 request 스코프 빈을 사용할 수 있다.
- 핵심 원리는, Provider든 프록시든 실제 요청이 들어올 때까지(진짜 객체 조회를 꼭 필요한 시점까지) 생성을 지연한다는 것임.
- 마치 싱글톤을 사용하는 것 같아서 주의해서 사용해야 함. 즉, 꼭 필요한 곳에만 최소화해서 사용해야 유지보수에 용이하다.
프록시 myLogger가 실제 myLogger.log() 호출
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
private String uuid;
private 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 create: " + this);
}
@PreDestroy
public void close() {
System.out.println("[" + uuid + "]" + "request scope bean close: " + this);
}
}
// --------------------------------------------------------------------------------
@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.getRequestURI().toString();
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
// --------------------------------------------------------------------------------------
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id: " + id);
}
}