[SPRING] 스코프와 Provider, 프록시
스프링 애플리케이션 실행 시 아래 오류가 발생하는 경우가 있다.
Error creating bean with name 'myLogger': Scope 'request' is not active for the current thread;
consider defining a scoped proxy for this bean if you intend to refer to it from a singleton;
스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않는다.
이 빈은 실제 고객의 요청이 와야 생성할 수 있다!
Provider와 프록시를 통해 이 문제를 해결해보자.
Provider
Provider를 통해 request scope 빈의 생성을 지연할 수 있다.
사용 예제
MyLogger.java
@Component
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 // 이 빈은 http 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 http 요청과 구분할 수 있다.
public void init(){
uuid = UUID.randomUUID().toString(); // 유니크한 랜덤 uuid 생성
System.out.println("[" + uuid + "] request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close: " + this);
}
}
LogDemoController.java
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; // 🌟
@RequestMapping("log-demo")
@ResponseBody // view 화면 없이 문자 반환
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString(); // http request 정보를 받을 수 있다.
MyLogger myLogger = myLoggerProvider.getObject(); // 🌟 Provider -> http request가 들어왔을 때 호출
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService.java
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider; // 🌟
public void logic(String id) {
MyLogger myLogger = myLoggerProvider.getObject(); // 🌟 Provider -> http request가 들어왔을 때 호출
myLogger.log("service id = " + id);
}
}
ObjectProvider
덕분에ObjectProvider.getObject()
를 호출하는 시점까지 request scope 빈의 생성을 지연할 수 있다.ObjectProvider.getObject()
를 호출하는 시점에는 http 요청이 진행중이므로 request scope 빈의 생성이 정상 처리된다.ObjectProvider.getObject()
를LogDemoController
,LogDemoService
에서 각각 한번씩 따로 호출해도 같은 http 요청이면 같은 스프링 빈이 반환된다!
프록시
이번에는 프록시 객체를 사용해보자.
사용 예제
MyLogger.java
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // 🌟 MyLogger의 가짜 프록시 클래스를 만들어두고 http 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 // 이 빈은 http 요청 당 하나씩 생성되므로, uuid를 저장해두면 다른 http 요청과 구분할 수 있다.
public void init(){
uuid = UUID.randomUUID().toString(); // 유니크한 랜덤 uuid 생성
System.out.println("[" + uuid + "] request scope bean create: " + this);
}
@PreDestroy
public void close(){
System.out.println("[" + uuid + "] request scope bean close: " + this);
}
}
LogDemoController.java
@Controller
@RequiredArgsConstructor
public class LogDemoController {
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody // view 화면 없이 문자 반환
public String logDemo(HttpServletRequest request){
String requestURL = request.getRequestURL().toString(); // http request 정보를 받을 수 있다.
System.out.println("myLogger = " + myLogger.getClass()); // 가짜 프록시 객체(myLogger) 출력
myLogger.setRequestURL(requestURL);
myLogger.log("controller test");
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService.java
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger; // 의존관계를 주입받는다.
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
- 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 👍
- CGLIB라는 라이브러리로 내 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
- 가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 클라이언트가
myLogger.logic()
을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다. - 가짜 프록시 객체는 request 스코프의 진짜
myLogger.logic()
을 호출한다. - 가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다.(다형성)
- 가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고 싱글톤처럼 동작한다.
주의점
- 마치 싱글톤을 사용하는 것 같지만 다르게 동작하기 때문에 결국 주의해서 사용해야 한다.
- 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다. 🚨
정리
- Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
💛 개인 공부 기록용 블로그입니다. 👻