본문 바로가기
Back-End/Spring Boot

[빈 스코프] 웹 스코프 - request 스코프, 프록시

by 달의 조각 2022. 3. 28.
이 글은 김영한 님의 스프링 핵심 원리 - 기본편을 수강하며 정리한 글입니다.

 

  웹 스코프는 웹 환경에서만 동작하며, 스프링이 해당 스코프의 종료 시점까지 관리한다. 따라서 종료 메서드가 호출된다.

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

 


 

request 스코프

 

클라이언트 A와 B가 동시에 같은 요청을 하면 각각 다른 인스턴스가 할당된다

 

  동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어려운데, 이때 request 스코프를 사용한다.

[d06b992f...] request scope bean create
[d06b992f...][http://localhost:8080/log-demo] controller test
[d06b992f...][http://localhost:8080/log-demo] service id = testId
[d06b992f...] request scope bean close
  • 포맷: [UUID][requestURL] {message}

 

@Component
@Scope(value = "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
    public void init() {
        uuid = UUID.randomUUID().toString(); //UUID: 전세계적으로 유일한 ID
        System.out.println("[" + uuid + "] request scope bean create:" + this);
    }

    @PreDestroy
    public void close() {
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}
  1. 이 빈은 HTTP 요청당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
  2. 생성되는 시점에 @PostConstruct 초기화 메서드를 사용해서 uuid를 생성해서 저장해 둔다.
  3. requestURL은 이 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력 받는다.

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody //View, 화면 없이 문자를 반환한다
    //HttpServletRequest: 자바에서 제공하는 표준 서블릿 규약, 고객 요청 정보 받기 가능
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURL().toString(); //고객이 어떤 URL로 요청했는지 알 수 있다

        System.out.println("myLogger = " + myLogger.getClass());
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("testId"); //Service에서도 호출해 보기
        return "OK";
    }
}
  1. 여기서 HttpServletRequest를 통해서 요청 URL을 받는다.
  2. 컨트롤러에서 controller test라는 로그를 남긴다.

※ requestURL을 MyLogger에 저장하는 부분은 컨트롤러보다는 공통 처리가 가능한 스프링 인터셉터나 서블릿 필터 같은 곳을 활용하는 것이 좋다. 인터셉터를 이용하면 HTTP Request 요청을 컨트롤러 호출 직전에 공통화해서 처리할 수 있다.

 

😧 request 스코프 빈은 실제 고객의 요청이 와야 생성할 수 있다

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;

의존관계 주입이 일어날 때 스프링 컨테이너에게 myLoger를 요구하는데, myLoger는 request 스코프이다. 이는 생존 범위가 고객이 요청이 들어와서 나갈 때까지이므로 request가 없는 문제가 발생한다. (현재는 생존 범위가 아님!)

 

🐬 ObjectProvider 적용

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody //View가 따로 없으므로 그냥 return 값인 OK를 화면에 출력한다
    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";
    }
}

HTTP 요청이 오면 ObjectProvider.getObject() 시점에 빈이 생성된다. 스프링 컨테이너에게 요청을 지연하는 것이다. 핵심은 동시에 같은 요청이 오더라도 요청마다 객체를 각각 따로 관리해 준다.

 

🐬proxyMode

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS) // 적용 대상: 클래스
public class MyLogger {

}

가짜 프록시 클래스를 만들어 두고 HTTP request와 상관 없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둘 수 있다.

 

가짜 프록시 객체는 요청이 오면 그때 내부에서 진짜 빈을 요청하는 위임 로직(진짜 myLogger를 찾는 방법)이 들어있다.

클라이언트가 myLogger.logic()을 호출하면 사실은 가짜 프록시 객체의 메서드를 호출한 것이다. 가짜 프록시 객체는 request 스코프의 진짜 myLogger.logic()를 호출한다.

가짜 프록시 객체는 원본 클래스를 상속 받아서 만들어졌기 때문에 이 객체를 사용하는 클라이언트 입장에서는 사실 원본인지 아닌지도 모르게, 동일하게 사용할 수 있다. (다형성)

가짜 프록시 객체는 실제 request scope와는 관계가 없다. 그냥 가짜이고, 내부에 단순한 위임 로직만 있고, 싱글톤처럼 동작한다.

 


 

Provider를 사용하든, 프록시를 사용하든 핵심 아이디어는 진짜 객체 조회를 꼭 필요한 시점까지 지연 처리 한다는 점이다. 이런 특별한 scope는 꼭 필요한 곳에만 최소화해서 사용하자, 무분별하게 사용하면 유지보수하기 어려워진다.

댓글