빈스코프 : 웹 스코프
웹스코프의 특징은 웹환경에서만 동작한다는 것입니다. 또 스프링이 종료시점까지 관리하기때문에 종료메서드가 호출됩니다. 웹 스코프는 아래와 같이 여러 종류가 있지만 가장 익숙한 request에 대한 동작을 살펴보겠습니다
예제 : 사용자의 요청마다 로그남기기
웹 스코프는 웹 환경에서만 돌아가기 때문에 웹환경을 만들어주기위해 라이브러리를 추가해줍니다.
build.gradle
//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
이 라이브러리를 추가해주므로써 스프링 부트는 톰켓서버를 활용해 웹서버와 스프링을 함께 실행합니다. 또한 라이브러리를 추가하기전에는 AnnotationConfigAppicationContext를 기반으로 구동시켰다면 이제는 AnnotationConfigServletWebServerApplicationContext 를 기반으로 웹을 구동하게 됩니다.
이제부터 [UUID][requestURL] {message}를 포맷으로 갖는 로그를 생성해보도록 하겠습니다. 먼저 로그를 생성하는 request스코프 스프링 빈 MyLogger부터 만들도록 하겠습니다. uuid는 HTTP요청마다 달라야하기때문에 빈이 초기화 될때 uuid를 넣어줍니다! requestURL은 외부에서 넣어줄 수 있도록 setter를 만들어 줬습니다
@Component
@Scope(value = "request")
public class MyLogger {
private String uuid;
private String requestURL;
public void setRequestURL(String url){
this.requestURL = url;
}
public void log(String message) {
System.out.println("[" + uuid + "]" + "[" + requestURL + "] " + message);
}
@PostConstruct
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);
}
이어서 컨트롤러를 만들어서 MyLogger를 사용해보겠습니다. request를 받아와서 URL을 빼낸뒤 setter로 넣어줍니다. 그런뒤에 log함수로 로그를 남깁니다. @RequiredArgsConstructor는 롬북덕분에 생성자주입을 편하게 할 수 있는 어노테이션입니다.
@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";
}
}
이번에는 비즈니스 로직이 있는 계층에서 로그를 남겨보겠습니다. 여기서도 myLogger를 가져와서 그 로거의 log를 호출합니다. 이때 MyLogger는 request스코프 빈이기 때문에 같은 요청에 대해서는 같은 스프링 빈이 반환됩니다. 따라서 Controller에서 넣어줬던 requestURL이 myLogger의 멤버변수로 잘 전달되어왔을 것입니다. 그렇지 않으면 파라미터로 URL같은 정보들을 서비스계층까지 넘겨줘야하는 상황이 생겨 지저분해집니다. 웹 스코프덕분에 웹정보는 Controller단에서만 쓰이게됩니다 👍
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final MyLogger myLogger;
public void logic(String id) {
myLogger.log("service id = " + id);
}
}
그런데 이렇게 실행하면 에러가 납니다. 그 이유는 스프링 컨테이너가 빈을 등록하는 과정에서 request 스코프 빈이 아직 없기 때문입니다..!! 싱글톤 스프링빈과는 다르게 request 스코프는 실제고객의 요청이 올때 만들어집니다. 어떻게 해결해야할까요..🧐🧐🧐🧐
해결
첫번째 해결방법은 Provider입니다! 바로 스프링 어플리케이션이 돌아갈 때 싱글톤이 MyLogger를 바로 필요로하지 않도록 해주는 것입니다. 아래 두 싱글톤 스프링 빈들이 MyLogger를 찾기 때문에 문제인 것입니다. 따라서 이부분을 Provider로 처리해줍시다!
public class LogDemoController {
private final MyLogger myLogger;
}
public class LogDemoService {
private final MyLogger myLogger;
}
사용자의 요청이 온 뒤에야 DL을 통해 빈을 직접 조회하는 것이죠🍒
ObjectProvider.getObject()를 실행하는 시점까지 빈의 생성을 미룰 수 있습니다
public class LogDemoController {
private final LogDemoService logDemoService;
private final ObjectProvider<MyLogger> myLoggerProvider; // provider!
@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";
}
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
private final ObjectProvider<MyLogger> myLoggerProvider; // provider!
public void logic(String id) { // 요청 들어오고 실행되는 함수
MyLogger myLogger = myLoggerProvider.getObject(); // 빈 조회
myLogger.log("service id = " + id);
}
}
프록시는 쉽게 말해서 실제를 흉내내는 가짜라고 보시면 됩니다. 따라서 MyLogger 자체를 ProxyMode로 설정해 놓으면 빈이 주입되야하는 시점에 실제로 생성되지 않았더라도 가짜 MyLogger가 들어가기때문에 오류를 내지 않습니다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
프록시모드에서의 동작원리는 먼저 코드는 처음과 똑같이 써도 됩니다. 생성자주입을 받을 때 가짜 프록시 객체 MyLogger가 들어가게됩니다. 이 가짜 객체는 진짜 MyLogger를 찾는 방법을 알고있습니다. 따라서 myLogger.setRequestURL과 같이 실제로 객체가 사용되는 시점에 진짜 객체에 접근해 그 함수를 실행합니다. 따라서 실제로 myLogger가 다 user의 요청이 있고나서 쓰이기 때문에 문제없이 돌아갑니다. 이때 프록시객체는 요청마다 생길 필요는 없기 때문에 싱글톤처럼 동작합니다.
@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";
}
}
두 방법 모두 빈의 생성에서 지연을 허락한다는 점!!이 핵심원리입니다~⭐️⭐️