들어가며

  • 이전 포스트까지 싱글톤 스코프와 프로토타입 스코프에 대해 알아보았다.
    • 싱글톤 스코프는 스프링 DI 컨테이너와 생명 주기를 같게 하는 기본적인 스코프였다.
    • 프로토타입 스코프는 스프링 DI 컨테이너가 빈의 생성과 의존관계 주입까지만 수행하고 넘기는, 호출할 때 마다 새로운 인스턴스가 반환되는 스코프였다.
  • 이번 포스트부터는 웹 스코프에 대해 알아본다.
  • 웹 스코프는 웹 관련 라이브러리가 필요하다. 따라서 부득이하게 Spring Web MVC 기술을 사용할 것이다.

 

 

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작하는 스코프이다.
  • 프로토타입 스코프와는 다르게 웹 스코프는 스프링 프레임워크가 해당 스코프의 종료 시점까지 관리한다. 따라서 프로토타입 스코프에서 호출되지 않는 @PreDestroy등의 메서드등이 호출된다.
  • 웹 스코프의 종류는 4가지가 존재한다.
    • request 스코프
    • session 스코프
    • application 스코프
    • websocket 스코프
  • 여기서는 request 스코프를 예시로 들어 웹 스코프에 대해 설명한다.
    • 이는 나머지 스코프들도 request 스코프와 거의 동일하게 동작하며 범위에 대한 차이만 존재해서 그렇다.

  • request 스코프에 속한 빈들은 클라이언트에 대한 HTTP 요청 메시지가 들어오면 해당 빈이 생성된다. 이후 HTTP 요청 메시지가 끝나면(처리가 끝나면) 소멸된다.
  • 그리고 주목해야 할 특징 중 하나는 각 클라이언트마다 새로운 빈 인스턴스를 생성해서 반환한다는 것이다. 이는 이후에 저 Service 부분에서 클라이언트 A에 대한 request 스코프 빈을 사용해야 할 때 A 전용의 request 스코프 빈이 들어온다는 것을 의미한다.

 

 

request 스코프 예제

  • request 스코프는 위에서 설명한 각 HTTP reqeust마다 새로운 인스턴스를 만들어 반환하는 특성을 이용하여 어떤 요청이 로그를 남겼는지를 기록하게 할 수 있다.
  • 우리가 원하는 것은 다음과 같은 것이다.
[UUID][requestURL]{message}
  • 위와 같은 포멧의 로그를 남길 수 있도록 해볼 것이다.
@Component
@Scope(value = "request")
public class MyLogger{
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String url){
        requestURL = url;
    }
    
    public void log(String message){
        System.out.println("[" + this.uuid + "] [" + this.requestURL + "]" + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean created: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
  • 위와 같은 포맷의 로그를 남기는 기능을 수행하는 MyLogger 클래스이다.
  • @Scope를 사용하여 request스코프로 설정하였다.
  • UUID는 자바에서 지원하는 UUID인데 randomUUID()를 사용하면 고유한 ID를 반환받을 수 있다.
  • url의 경우 해당 클래스가 생성되는 시점에서는 requestURL을 알 수 없기 떄문에 생성자에서 초기화하기에는 무리가 있고, Setter를 통하여 설정한다.
  • @PostConstruct와 @PreDestory는 빈의 생명주기 포스트를 참고하자.
@Controller
public class LogDemoController{
    private final LogDemoService logDemoService;
    private final MyLogger myLogger;
    
    @Autowired
    LogDemoController(LogDemoService logDemoService , MyLogger myLogger){
        this.logDemoService = logDemoService;
        this.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";
    }
}
  • 지금까지 보지 못한 몇몇 어노테이션들이 보인다. 이 어노테이션들은 이후 Spring MVC에서 다룰 예정이다.
  • 간단하게 설명하자면 @RequestMapping은 URL애서 localhost:8080/ 이후에 log-demo를 넣고 진입할 시 해당 메서드를 실행하게 하는 어노테이션이다.
  • @ResponseBody의 경우 HTTP 응답 메시지의 본문(Body)에 해당 데이터를 직접 담아서 보내겠다는 어노테이션이다.
    • 즉 해당 요청이 정상 처리되고 난 이후에 클라이언트가 받은 응답 HTTP 메시지의 본문에 저 OK가 적혀 있는 것이다.
  • LogDemoService는 바로 이후에 있는 서비스 코드이다.
@Service
public class LogDemoService{
    private final MyLogger myLogger;
    
    @Autowired
    LogDemoService(MyLogger myLogger){
        this.myLogger = myLogger;
    }
    
    public void logic(String id){
         myLogger.log("serviceId = " + id);
    }
}
  • 이 Service는 웹 요청이 들어오고 난 뒤에 실행되는 메인 로직이다. 즉 웹 요청을 받아 처리하는 것은 Controller가 수행하고 Service는 Controller에서 여러 처리를 수행한 데이터들을 사용하여 어떤 비즈니스 로직(실 기능)을 수행한다.
  • 여기서 왜 우리가 MyLogger를 request scope로 지정했는지를 알 수 있다.
    • 만약 MyLogger를 request scope로 지정하지 않았다면, MyLogger는 싱글톤 스코프로 관리된다. 즉 해당 요청 전용이 아니게 된다.
    • 각 요청에 대한 전용 객체가 아니게 되었기 때문에 각 요청에 대한 정보를 가져올 수 없다. 이는 서비스 계층에서 컨트롤러 계층에 존재하는 requestURL을 가져오게 해야 한다는 것을 의미한다.
    • 바로 위에서 말했듯이 서비스는 오직 순수한 비즈니스 로직에 대해서 처리하는것만 집중해야 한다. 웹과 관련된 처리는 컨트롤러에서 해야하는데 서비스 계층까지 내려와버린 것이다. 이는 서비스 계층이 특정 웹 기술에 종속되는 것으로 권장되지 않는 방법이다.
    • 그런데 우리가 request scope으로 지정함으로서 requestURL 같은 웹 계층의 데이터를 넘기지 않고 MyLogger에 저장함으로서 깔끔하게 유지할 수 있다.
  • 그런데 여기서 문제가 생긴다.

 

 

request 예제의 문제점

  • 위의 예제를 적절하게 수행시키면 어플리케이션이 실행되지 않는다.
  • 이는 다음의 이유로 인하여 발생한다.

  • LogDemoController, LogDemoService는 따로 스코프를 지정하지 않았기 때문에 싱글톤 스코프로 설정된다. 이는 스프링 컨테이너가 실행될 때 객체가 생성되며 MyLogger를 주입받아야 한다는 것을 의미한다.
  • 따라서 스프링 컨테이너는 MyLogger를 생성시키려고 한다. 그런데 우리가 위에서 봤던 request 스코프의 특징에 대해 생각해보자. 클라이언트에서 HTTP 요청 메시지가 들어오기 이전엔 생성되지 않는다고 하였다.
  • 따라서 MyLogger가 스프링 빈으로 생성되지 않는다. 동시에 해당 스프링 빈을 LogDemoController에서 주입할 수 없으므로 예외가 발생하여 실행이 종료되게 된다.
  • 즉 이 문제는 싱글톤 스코프에 속한 스프링 빈들이 아직 요청이 들어오지 않은 상태에서 request 스코프의 스프링 빈을 생성고 주입받게 하려다가 실패하는 것이 원인이다.
    • 이 문제의 해결책은 말 그대로 LogDemoController와 같은 싱글톤 스코프에서 MyLogger 스프링 빈을 요청할 때, 고객의 요청이 들어올 때 까지 생성하는 것을 지연하는 것이다.

 

 

 

해결책 - Provider

  • 해당 문제 역시 Provider를 사용하여 해결이 가능하다. LogDemoController와 LogDemoService의 코드를 Provider를 사용하는 방식으로 수정해 보자.
@Controller
public class LogDemoController{
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> objectProvider;
    
    @Autowired
    LogDemoController(LogDemoService logDemoService , ObjectProvider<MyLogger> objectProvider){
        this.logDemoService = logDemoService;
        this.objectProvider = objectProvider;
    }
    
    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request){
        String requestURL = request.getRequestURL().toString();
        
        MyLogger myLogger = objectProvider.getObject();
        myLogger.setRequestURL(requestURL);
        
        myLogger.log("controller-test");
        logDemoService.logic("testID");
        
        return "OK";
    }
}
@Service
public class LogDemoService{
    private final ObjectProvider<MyLogger> objectProvider;
    
    @Autowired
    LogDemoService(ObjectProvider<MyLogger> objectProvider){
        this.objectProvider = objectProvider;
    }
    
    public void logic(String id){
        MyLogger myLogger = objectProvider.getObject();
        myLogger.log("serviceId = " + id);
    }
}

 

 

 

프록시를 사용한 문제 해결

  • 프록시(Proxy)패턴을 사용한 문제 해결 방식도 있는데 위의 Provider보다 더 깔끔한 코드를 작성할 수 있다.
  • 이 방식은 MyLogger에 적용시키면 된다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger{
    private String uuid;
    private String requestURL;
    
    public void setRequestURL(String url){
        requestURL = url;
    }
    
    public void log(String message){
        System.out.println("[" + this.uuid + "] [" + this.requestURL + "]" + message);
    }
    
    @PostConstruct
    public void init(){
        uuid = UUID.randomUUID().toString();
        System.out.println("[" + uuid + "] request scope bean created: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
  • proxyMode라는 것을 통해 프록시를 설정해줄 수 있는데 만약 클래스라면 TARGET_CLASS를 아니라면 INTERFACES를 넣으면 된다.
  • 이 방식을 사용하면 스프링이 컨트롤러와 서비스의 MyLogger를 요청할 때 가짜 프록시 클래스를 컨트롤러와 서비스에 주입해준다. 즉 가짜를 미리 주입해주어 빈이 반환되지 않음으로 인해 생기는 예외를 해결하는 것이다.
  • 나머지 컨트롤러와 서비스는 Provider 사용 이전으로 해도 아무 문제가 없다.
  • 이 가짜 프록시 클래스는 자신이 호출되면 그제서야 원래 객체를 찾아 호출한다. 그런데 위의 상황에서 MyLogger가 호출되는 상황은 컨트롤러에 HTTP 요청이 들어왔다는 것이므로 MyLogger객체가 생성되어져 있다. 따라서 프록시 클래스는 해당 객체를 찾아 logic()을 수행한다.
복사했습니다!