[Spring] 빈 스코프
본 포스트는 Inflearn
의 김영한님 강의를 바탕으로 작성했습니다.
Bean Scope
- 스프링은 default로 빈을 싱글톤 스코프로 생성한다. 싱글톤 스코프로 생성된 빈은 스프링 컨테이너 시작 시점에 생성되어 스프링 컨테이너 종료까지 유지된다.
- 스프링은 싱글톤 스코프 말고도 프로토타입 스코프, 웹 스코프(request, session, application)를 지원한다. 하나씩 살펴보자
프로토타입 스코프
- 프로토타입 스코프를 잘 이해하기 위해 싱글톤 스코프부터 복습해보자. 싱글톤 스코프로 생성된 빈은 조회를 하면 항상 같은 instance의 스프링 빈이 반환된다. (이미지 참고)
- 이미지와 같이 요청할 때마다 새로운 객체가 반환되는 것이 아니라 동일한 객체가 반환되는 것을 확인할 수 있다.
- 그렇다면 프로토타입 스코프로 생성된 빈은 어떨까?
- 클라이언트에서 빈을 찾을 때에 생성되고 의존관계도 주입되서 클라이언트로 뿌린다.
- 여기서 주목해야 할 점은 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 해주고 더 이상 관리를 안해준다는 것이다.
-
때문에 @PreDestroy가 붙은 메서드도 호출되지 않는다. 클라이언트쪽에서 필요하다면 직접 호출해야 한다.
- 프로토타입 빈 정리
- 프로토타입 빈은 스프링 컨테이너에 요청할 때마다 새로 생성된다.
- 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더 이상 관리하지 않는다. (이후에 클라이언트쪽에서 알아서 관리해야 한다.)
- 종료 메서드가 호출되지 않기 때문에 필요하다면 클라이언트쪽에서 직접 호출해야 한다.
프로토타입 빈의 문제점
- 프로토타입빈을 싱글톤 빈과 같이 사용할 때 문제가 발생할 수 있다. 아래 그림을 보자.
- 싱글톤 스코프로 생성되는 빈은 스프링 컨테이너 시작 시점에 생성되면서 이 때 의존관계를 주입받는다. 그런데 주입받는 객체가 프로토타입이면 어떨까?
- 프로토타입 스코프를 사용하는 빈의 사용 목적은 호출할 때마다 다른 빈이 생성되서 나오도록 하는 것이다. 그러나 싱글톤 빈 내부에 가지고 있는 프로토타입 빈은 스프링 컨테이너 생성 시점에 주입이 끝난 빈일 것이다. 때문에 호출할 때마다 다른 빈이 생성되서 나오는 것이 아니라 계속 같은 빈이 나올 것이다.
- 이제 이 문제를 해결하는 여러 방법에 대해 살펴보고 어떤 방법이 가장 좋을지 고민해보자!
해결방법1 : 스프링 컨테이너에 직접 요청
- 가장 단순하고 무식한 방법이다. 싱글톤 빈 내부에 스프링 컨테이너 의존성을 주입받고 프로토타입 빈을 사용할 때 마다 스프링 컨테이너에 요청하는 방식이다.
static class ClientBean {
@Autowired
private ApplicationContext ac;
public int logic() {
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
}
- 이런식으로 의존관계를 외부에서 주입받는 것이 아니라 필요한 의존관계를 직접 찾는 것을 Dependency Lookup(DL, 의존관계 조회) 라고 한다.
- 하지만 위 코드와 같이 스프링 컨테이너 전체를 주입 받으면 스프링 컨테이너에 종속적이게 되고, 단위 테스트도 어렵다.
- 그리고 여기서 필요한 기능은 DL뿐인데 ApplicationContext는 너무 다양한 기능을 제공한다.
- DL만 해주는 무언가가 있으면 좋을텐데?? -> 스프링에 이미 있다. ObjectProvider
해결방법2 : ObjectProvider
- ObjectProvider는 스프링에서 만든 것으로 DL 기능을 제공한다. 사용방법은 아래 코드를 보자!
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
해결방법3 : Provider
- 자바 표준에서 제공하는 것으로 스프링 컨테이너가 아닌 다른 컨테이너에서도 사용할 수 있다.
- 사용방법은 ObjectProvider와 거의 같다. (코드 참고)
@Autowired
private Provider<PrototypeBean> provider;
public int logic() {
PrototypeBean prototypeBean = provider.get();
prototypeBean.addCount();
int count = prototypeBean.getCount();
return count;
}
프로토타입 빈 정리
- 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용하면 된다.
- 실제 실무에서 웹개발을 하면 대부분 싱글톤 빈으로 해결할 수 있기 때문에 사용할 일은 거의 없다고 보면 된다.
- 다만 ObjectProvider, Provider 같은 DL을 지원하는 것들은 알아두면 좋다.
웹 스코프
- 웹 스코프는 웹 환경에서만 동작하며 프로토타입 스코프랑 다르게 해당 웹 스코프 종료 시점까지 스프링 컨테이너가 관리해준다.
- 그러면 request 스코프에 대해서 알아보자!
- request 스코프는 HTTP request 요청마다 생성 및 할당된다. => 생성 시점이 HTTP request가 들어오는 시점이다.
- 위 그림에서 클라이언트A의 요청과 클라이언트B의 요청이 있는데 이 둘은 다른 HTTP request 이므로 다른 객체가 할당된다.
request 스코프는 언제 사용될까?
- 로그 처리
- 동시에 여러 HTTP request가 들어오면 어떤 요청에 의해 생성된 로그인지 구분하기 어려울 수 있다. 이럴때 사용하면 좋다.
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
...
}
@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";
}
}
- 위 코드를 실행해보면 서버가 제대로 뜨지 않는다.
- LogDemoController는 싱글톤 빈이어서 스프링 컨테이너가 뜨는 시점에 생성되고 의존관계가 주입된다. 그런데 이 시점에는 아직 HTTP request가 들어온 것이 없다.
- 아직 스프링 컨테이너에 MyLogger라는 request 스코프를 가지는 빈은 생성되지 않은 것이다.
- 없는 것을 주입하려고 하니까 당연히 에러가 발생한다.
해결방법1 : Provider
- ObjectProvider를 사용해서 실제 HTTP reqeust가 들어올 때 request 스코프 빈을 생성하면 된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private 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";
}
}
- ObjectProvider를 통해 getObject()를 호출하는 시점까지 request 스코프 빈의 생성을 지연할 수 있다.
- ObjectProvider를 호출할 때, 같은 HTTP 요청에 대해서는 같은 스프링 빈이 반환된다.
해결방법2 : 프록시
- 두번째 방법은 가짜 객체(proxy)를 주입해놓고 실제 호출 시점에 request 스코프 빈을 생성하는 방법을 사용하는 것이다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
...
}
- 적용할 대상이 구현체면 TARGET_CLASS, 인터페이스면 INTERFACES를 넣어주면 된다.
- 프록시 객체는 내부에 실제 객체를 찾는 방법을 가지고 있다.
- 실제 필요한 시점까지 생성을 미뤄뒀다가 필요할 때 생성해서 실제 객체를 가져온다.
핵심
- 프록시 방법, Provider 방법의 핵심은 실제 필요할 때까지 객체의 생성을 미룬다는 점이다.
출처
- https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8