티스토리 뷰

request 스코프는 웹 스코프 중 하나로 HTTP 요청이 들어오고 나갈 때까지 유지되는 스코프다. 각각의 HTTP 요청마다 별도의 빈 인스턴스가 생성되고 관리된다.

예제로 Logger를 하나 생각해보자.

HTTP 요청마다 id를 부여해 HTTP 요청을 구분하고 싶다. 이때 request 스코프를 이용하면 HTTP 요청을 쉽게 구분할 수 있다.

 

 

@Component
@Scope(value = "request")   // 이 빈은 http 요청 당 하나씩 생성 & http 끝나는 시점에 소멸
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 생성해서 저장. 다른 http 요청과 구분!
        System.out.println("[" + uuid + "] request scope bean create : " + this  );
    }

    @PreDestroy
    public void close() {
        //고객 요청이 끝날 때 호출
        System.out.println("[" + uuid + "] request scope bean close : " + this);
    }

}

Logger 객체를 만든다. @Scope(value="request")에 주목하자. request 스코프를 사용했다.

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;  
  
    @RequestMapping("log-demo")
    @ResponseBody   //String을 그대로 반환
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testID");
        return "OK";
    }

}
package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;
 
    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("servide id = " + id);
    }
}

HTTP 요청을 처리하기 위한 Controller, Service 객체도 하나씩 만들자. 

 

이렇게 하고 실행해보면...

위와 같은 에러가 나온다. request가 active하지 않다...?

그렇다. 처음 Controller를 생성할 때 컨테이너에 MyLogger를 찾아서 주입해달라고 요청하는데, 아직 HTTP 요청이 들어오지 않은 상태다! request 스코프는 HTTP 요청이 들어왔을 때 생성된다고 했다. 따라서 컨테이너는 request 스코프 빈인 MyLogger를 줄 수 없고, 에러를 띄운 것이다.

 

그렇다면.. MyLogger 생성을 미루어두었다가(지연시켰다가) HTTP 요청이 들어왔을 때 컨테이너에게 MyLogger를 조회해달라고 요청(Dependency Lookup)할 순 없을까..?

 

가만, 비슷한 것을 프로토타입 스코프를 공부할 때 배웠었다! 컨테이너에게 특정 빈을 달라고 요청할 수 있는 방법..!

바로 Provider다!

 

Controller와 Service 객체를 다음과 같이 수정하자.

 

package hello.core.web;


import hello.core.common.MyLogger;
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 javax.servlet.http.HttpServletRequest;

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    // private final MyLogger myLogger;    //에러 발생. 컨테이너에 myLogger 요청하지만, 이때
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @RequestMapping("log-demo")
    @ResponseBody   //String을 그대로 반환
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testID");
        return "OK";

    }

}
package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

   // private final MyLogger myLogger;
    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("servide id = " + id);
    }
}

 

이제 Controller와 Service가 생성될 때는 MyLogger를 찾을 수 있는 Provider까지만 생성된다. 그리고 MyLogger를 요청하는 것은 HTTP 요청이 들어오고 난 다음으로 미루어졌으므로 문제가 해결되었다. 실행해보면...

MyLogger가 제대로 동작한다!

 

 

(참고)

프록시를 사용해 Provider를 대체할 수 있다.

package hello.core.common;


import org.springframework.context.annotation.Scope;
import org.springframework.context.annotation.ScopedProxyMode;
import org.springframework.stereotype.Component;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import java.util.UUID;

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)   // 이 빈은 http 요청 당 하나씩 생성 & http 끝나는 시점에 소멸
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 생성해서 저장. 다른 http 요청과 구분!
        System.out.println("[" + uuid + "] request scope bean create : " + this  );
    }

    @PreDestroy
    public void close() {
        //고객 요청이 끝날 때 호출
        System.out.println("[" + uuid + "] request scope bean close : " + this);
    }

}

@Scope 어노테이션의 옵션을 바꾸어 주자.

proxyMode를 설정하는데, 아래 객체가 class면 TARGET_CLASS로, interface면 INTERFACES로 두자.

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;  
  
    @RequestMapping("log-demo")
    @ResponseBody   //String을 그대로 반환
    public String logDemo(HttpServletRequest request) {
        String requestURL = request.getRequestURI().toString();
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);
        myLogger.log("controller test");
        logDemoService.logic("testID");
        return "OK";
    }

}
package hello.core.web;

import hello.core.common.MyLogger;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;
 
    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("servide id = " + id);
    }
}

 

그리고 Controller와 Service는 Provider를 적용하기 전으로 돌리자.

그러면 정상 작동하는 걸 알 수 있다.

어떻게 된 걸까?

옵션에 프록시를 설정하면, 처음에 빈을 등록할 때 가짜 MyLogger가 등록된다. 그리고 MyLogger가 클라이언트에 의해 호출되면, 이 가짜 MyLogger가 진짜 MyLogger(request 스코프 빈)을 찾아서 필요한 로직을 수행하게 한다.

MyLogger.getClass()를 출력해보면, $$EnhancerBySpringCGLIB... 이 붙은 것을 볼 수 있는데, 스프링 컨테이너에 의해 가짜 myLogger 객체가 생성된 것이다.

 

이처럼 프록시를 이용해, Provider와 마찬가지로 request 스코프 빈의 생성을 지연시킬 수 있다. 프록시를 이용하면 마치 싱글톤 빈을 사용하는 것처럼 별도의 코드를 추가하지 않고 request 스코프 빈을 활용할 수 있다. 어노테이션만 바꿔서 기능을 달리 할 수 있다는 것이 스프링이 얼마나 다형성 있는 코드에 최적화되어 있는지를 보여준다.

 

 

정리하자면,

request 스코프 빈은 HTTP 요청이 들어왔을 때 생성되어 HTTP 요청 처리가 끝났을 때 소멸되는 빈이다.

이런 스코프 빈은 문제가 생겼을 때 디버깅하기 어렵고, 테스트 코드를 작성하기도 힘들다. 따라서 반드시 필요한 곳에만 활용하자.

 

 

사진, 내용 출처 : 인프런, 김영한 강사님의 '스프링 핵심 원리 - 기본편' 강좌, 강의자료

https://inf.run/TErf

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
«   2024/10   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30 31
글 보관함