빈 스코프 - 웹 스코프 사용 시 문제해결
-> 이전에 request 스코프 코드를 만들고 실행했을 때 HTTP 요청이 들어오지 않아서 request 스코프 빈이 생성되지 않아 에러가 발생했다. Provider, 프록시를 사용하여 에러를 해결한다.
◎ Provider 사용
LogDemoController
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import practice.core.common.MyLogger;
import javax.servlet.http.HttpServletRequest;
@Controller
@RequiredArgsConstructor
public class LogDemoController { // 로거가 작동하는지 확인하는 테스트용 컨트롤러
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) { // HttpServletRequest를 통해 요청 URL(http://localhost:8080/log-demo)을 받는다.
String requestURL = request.getRequestURL().toString();
MyLogger myLogger = myLoggerProvider.getObject();
myLogger.setRequestURL(requestURL); // requestURL 값을 myLogger에 저장
myLogger.log("controller test"); // 로그를 남긴다.
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;
import practice.core.common.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);
}
}
-> 결과
- 애플리케이션을 실행시키고 localhost:8080/log-demo에 접속하면 정상적으로 작동한다.
- 또한 로그도 정상적으로 출력되었다.
- ObjectProvider를 통해 ObjectProvider.getObject()를 호출하는 시점까지 request 스코프 빈의 생성을 지연할 수 있다.
- ObjectProvider.getObject()를 호출하는 시점에는 HTTP 요청이 들어와서 진행 중이기 때문에 request 스코프 빈이 정상적으로 생성된다.
- ObjectProvider.getObject()를 LogDemoController, LogDemoService에서 각각 한 번씩 따로 호출하여도 같은 HTTP 요청이면 동일한 스프링 빈이 반환된다.
◎ 프록시 사용
-> MyLogger 클래스에 proxyMode = ScopedProxyMode.TARGET_CLASS를 추가한다.
-> 적용대상이 클래스면 TARGET_CLASS를 선택하고, 인터페이스라면 INTERFACE를 선택한다.
-> proxyMode = ScopedProxyMode.TARGET_CLASS로 클래스를 지정하면 MyLogger의 가짜 프록시 객체를 만들고 HTTP request와 관계 없이 가짜 프록시 객체를 다른 빈에 미리 주입할 수 있다.
-> LogDemoController와 LogDemoService 클래스를 Provider 적용 이전으로 수정한다.
LogDemoController
@Controller
@RequiredArgsConstructor
public class LogDemoController { // 로거가 작동하는지 확인하는 테스트용 컨트롤러
private final LogDemoService logDemoService;
private final MyLogger myLogger;
@RequestMapping("log-demo")
@ResponseBody
public String logDemo(HttpServletRequest request) { // HttpServletRequest를 통해 요청 URL(http://localhost:8080/log-demo)을 받는다.
String requestURL = request.getRequestURL().toString();
System.out.println("myLogger = " + myLogger.getClass()); // 주입된 MyLogger 확인
myLogger.setRequestURL(requestURL); // requestURL 값을 myLogger에 저장
myLogger.log("controller test"); // 로그를 남긴다.
logDemoService.logic("testId");
return "OK";
}
}
LogDemoService
@Service
@RequiredArgsConstructor
public class LogDemoService { // 서비스 계층
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
-> 결과
- 실행해보면 MyLogger는 CGLIB라는 라이브러리로 클래스를 상속받은 가짜 프록시 객체를 만들어서 주입된다.
- Scope의 'proxyMode = ScopedProxyMode.TARGET_CLASS'를 대상 클래스에 지정하면 스프링 컨테이너는 CGLIB라는 바이트 코드를 조작하는 라이브러리를 사용하여 MyLogger클래스를 상속받은 가짜 프록시 객체를 생성한다.
- 따라서 애플리케이션을 실행했을 때 myLogger를 확인해보면 순수 MyLogger 클래스가 아니라 CGLIB가 붙어서 등록된 객체가 가짜 프록시 객체라는 것을 확인할 수 있다.
- MyLogger의 기능을 호출하는 시점에 진짜 MyLogger를 찾아 동작한다.
- 가짜 프록시 객체는 해당 객체에 있는 기능에 대한 요청이 들어오면 내부에서 진짜 빈을 요청하는 위임 로직이 들어있다.
- 가짜 프록시 객체는 진짜 객체를 찾는 방법을 알고 있다.
- 클라이언트가 myLogger.log()를 호출하면 가짜 프록시 객체의 메서드를 호출한다.
- 프록시 객체는 request 스코프의 진짜 myLogger.log()를 호출한다.
- 프록시 객체는 진짜 객체를 상속받아 만들어진 객체기 때문에 해당 객체를 사용하는 클라이언트 입장에서는 프록시 객체인지 진짜 객체인지 모르는 상태로 마치 프록시 객체가 진짜 객체인 것 처럼 사용한다.
- 프록시 객체를 사용하여 마치 싱글톤 빈을 사용하는 것처럼 request 스코프 빈을 사용한다.
- 싱글톤 빈을 사용하는 것처럼 request 스코프 빈을 사용하지만 실제 싱글톤 빈은 아니므로 주의해서 사용해야 한다.
- request 스코프와 같은 특수한 스코프는 필요한 곳에 최소화하여 사용해야 한다. 무분별하게 사용 시 코드의 유지보수가 어려워진다.
※ Provider와 프록시 객체 사용의 핵심
- Provider나 프록시 객체를 사용하는 방식의 핵심은 진짜 객체 조회를 필요한 시점까지 지연처리하는 것이다.
☆ 참고