꼬물꼬물

빈 스코프 [웹 스코프] 본문

스터디/스프링 핵심 원리 - 기본편

빈 스코프 [웹 스코프]

멩주 2022. 10. 4. 17:40
싱글톤은 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프
프로토타입은 생성과 의존관계 주입, 그리고 초기화까지만 진행하는 특별한 스코프

 

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작한다.
  • 웹 스코프는 프로토타입과 다르게 스프링이 해당 스코프의 종료시점까지 관리한다. 따라서 종료 메서드가 호출된다.

 

웹 스코프의 종류

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

 

나머지는 범위만 다르지 동작 방식은 비슷하다. A전용 빈이 생성된다.

하나의 HTTP request 내에서는 같은 객체를 바라보게 된다.

프로토 타입은 요청마다 생성된다면 리퀘스트는 HTTP 리퀘스트에 맞춰서 각각의 하나를 관리한다.

A가 요청하면 A 전용 빈 객체가 생성되어 사용되다가 응답이 끝나면 파기된다.

 

Request 스코프 예제 만들기

웹 스코프는 웹 환경에서만 동작하므로 web 환경이 동작되도록 라이브러리를 추가해야한다.

//web 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-web'
  • spring-boot-starter-web 라이브러리를 추가하면 스프링 부트는 내장 톰캣 서버를 활용해 웹 서버와 스프링을 함께 실행시킨다.
  • 스프링 부트는 웹 라이브러리가 없으면 AnnotationConfigApplicationContext를 기반으로 애플리케이션을 구동한다.
  • 웹 라이브러리가 추가되면 웹과 관련된 추가 설정과 환경들이 필요해 AnnotationConfigServletWebServerApplicationContext를 기반으로 애플리케이션을 구동한다.

 

request 스코프 예제 개발

동시에 여러 HTTP 요청이 오면 정확히 어떤 요청이 남긴 로그인지 구분하기 어렵다. 이때 사용하기 좋은 것이 request 스코프이다.

  • 기대하는 공통 포맷: [UUID][requestURL][message]
  • UUID를 사용해 HTTP 요청을 구분하자
  • requestURL 정보도 추가로 넣어 어떤 URL을 요청해 남은 로그인지 확인하자

 

@Scope("request")
@Component
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("[" + this.uuid + "] " + "[" + requestURL + "] " + "[" + message + "]");
    }
    
    @PostConstruct
    public void init(){
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + this.uuid + "] " + "request scope bean create: " + this);
    }
    
    @PreDestroy
    public void close(){
        System.out.println("[" + this.uuid + "] " + "request scope bean close: " + this);
    }
}
  • log를 출력하기 위한 MyLogger 클래스
  • @Scope(value="request")를 사용해 request 스코프로 지정. 이제 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸한다.
  • @PostConstruct 초기화 메서드를 사용해 빈이 생성되는 시점에 uuid를 생성해 넣어준다.
  • requestURL은 빈이 생성되는 시점에는 알 수 없으므로, 외부에서 setter로 입력받는다.
  • HTTPServletRequest를 통해 요청 URL을 받는다.
  • 받은 requestURL값을 저장한다. myLogger는 HTTP 요청 당 각각 구분되므로 다른 HTTP 요청 때문에 값이 섞이는 걱정은 하지 않아도 된다.
  • 컨트롤러에서 controller test라는 로그를 남긴다.

 

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody // view없이 문자 반환
    public String logDemo(HttpServletRequest request){ // HttpServletRequest는 자바에서 제공하는 http 요청 정보를 받을 수 있는 것.
        String requestURL = request.getRequestURI().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("test ID");

        return "OK";
    }
}
  • 오류 발생.
  • 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주잉ㅂ이 가능하지만 request 스코프 빈은 고객의 요청이 와야 생성된다. @RequiredArgsConstructor에서 문제가 생긴다.

 

1) Provider 사용하기

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;

    @RequestMapping("log-demo")
    @ResponseBody // view없이 문자 반환
    public String logDemo(HttpServletRequest request){ // HttpServletRequest는 자바에서 제공하는 http 요청 정보를 받을 수 있는 것.
        String requestURL = request.getRequestURI().toString();
        // 현 시점에 가져오기
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("test ID");

        return "OK";
    }
}
@RequiredArgsConstructor
@Service
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

    public void logic(String id) {
        MyLogger myLogger = myLoggerProvider.getObject();
        myLogger.log("service id = " +id);
    }
}
  • ObjectProvider를 사용해 request가 들어오고 MyLogger를 주입 받는다.

하나의 요청당 다른 객체를 사용하고 있다.

  • 동시에 여러 요청이 와도 요청마다 각각 객체를 관리한다.
  • ObjectProvider 덕분에 Provider.getObject() 호출 시점까지 스프링 컨테이너에게 요청을 지연할 수 있다.
  • Provider.getObject()를 호출하는 시점에는 HTTP 요청이 진행중이므로 request scope 빈 생성이 정상 처리된다.
  • 같은 HTTP Request면 controller, service에서 찾는 스프링 빈은 같은 빈이 유지된다.

 

2) 스코프와 프록시

@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
@Component
public class MyLogger {}
  • proxyMode = ScropedProxyMode.TARGET_CLASS를 추가했다.
    • 적용 대상이 인터페이스가 아닌 클래스면 TARGET_CLASS
    • 적용 대상이 인터페이스면 INTERFACES 선택
  • MyLogger의 가짜 프록시 클래스를 만들어두고 HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해 둔다.
  • 기능을 실제 호출하는 시점에 진짜를 찾아서 동작한다.

진짜 MyLogger가 아니라 스프링 빈이 조작한 MyLogger가 올라가 있다. @Configuration

  • CGLIB라는 라이브러리로 내 클래스를 상속받은 가짜 프록시 객체를 만들어 주입한다.
  • @Scope(proxyMode=ScopedProxyMode.TARGET_CASS)를 설정하면 스프링 컨테이너는 CGLIB이라는 바이트 코드를 조작하는 라이브러리를 사용해 MyLogger를 상속받은 가짜 프록시 객체를 생성한다.
  • 의존관계 주입도 가짜 프록시 객체가 주입되어있다.

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

  • 가짜 프록시 빈은 매부에 실제 MyLogger를 찾는 방법을 알고있음
  • mylogger.logic()도 가짜 프록시 객체의 메서드를 호출한 것이다.
  • 가짜 프록시 객체가 request 스코프의 진짜 myLogger.logic()을 호출한다.
  • 가짜 프록시 객체는 원본 클래스를 상속받아 만들어져 클라이언트는 동일하게 사용할 수 있다.(다형성)
  • 가짜 프록시 객체는 싱글톤처럼 동작한다.

 

✔️ 정리

  • 프록시 객체 덕분에 클라이언트는 싱글톤 빈을 사용하듯 편리하게 request scope를 사용할 수 있다.
  • provider와 프록시의 핵심은 진짜 객체 조회를 꼭 필요한 시점까지 지연처리 한다는 점이다.
  • 단지 애노테이션 설정 변경만으로 원본 객체를 프록시 객체로 대체할 수 있다. 다형성과 DI 컨테이너가 가진 큰 장점!
  • 마치 싱글톤처럼 사용하는 것 같지만 다르게 동작하기 때문에 주의!
  • ㅣ런 특별한 Scope는 최소화해 사용해야 한다.