[spring] 빈스코프 : 웹스코프 (스프링 기본편 by 김영한)

su_y2on·2022년 2월 4일
0

Spring

목록 보기
26/30
post-thumbnail

빈스코프 : 웹 스코프


웹스코프의 특징은 웹환경에서만 동작한다는 것입니다. 또 스프링이 종료시점까지 관리하기때문에 종료메서드가 호출됩니다. 웹 스코프는 아래와 같이 여러 종류가 있지만 가장 익숙한 request에 대한 동작을 살펴보겠습니다

  • request: HTTP 요청 하나가 들어오고 나갈 때 까지 유지되는 스코프, 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리됩니다.
  • session: HTTP Session과 동일한 생명주기를 가지는 스코프
  • application: 서블릿 컨텍스트( ServletContext )와 동일한 생명주기를 가지는 스코프
  • websocket: 웹 소켓과 동일한 생명주기를 가지는 스코프



예제 : 사용자의 요청마다 로그남기기

웹 스코프는 웹 환경에서만 돌아가기 때문에 웹환경을 만들어주기위해 라이브러리를 추가해줍니다.

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 스코프는 실제고객의 요청이 올때 만들어집니다. 어떻게 해결해야할까요..🧐🧐🧐🧐





해결


1. Provider

첫번째 해결방법은 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);
	}
}




2. 프록시

프록시는 쉽게 말해서 실제를 흉내내는 가짜라고 보시면 됩니다. 따라서 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";
	} 
}

두 방법 모두 빈의 생성에서 지연을 허락한다는 점!!이 핵심원리입니다~⭐️⭐️

0개의 댓글