3 분 소요

본 포스트는 Inflearn의 김영한님 강의를 바탕으로 작성했습니다.

Bean Scope

  • 스프링은 default로 빈을 싱글톤 스코프로 생성한다. 싱글톤 스코프로 생성된 빈은 스프링 컨테이너 시작 시점에 생성되어 스프링 컨테이너 종료까지 유지된다.
  • 스프링은 싱글톤 스코프 말고도 프로토타입 스코프, 웹 스코프(request, session, application)를 지원한다. 하나씩 살펴보자

프로토타입 스코프

  • 프로토타입 스코프를 잘 이해하기 위해 싱글톤 스코프부터 복습해보자. 싱글톤 스코프로 생성된 빈은 조회를 하면 항상 같은 instance의 스프링 빈이 반환된다. (이미지 참고)

img1

  • 이미지와 같이 요청할 때마다 새로운 객체가 반환되는 것이 아니라 동일한 객체가 반환되는 것을 확인할 수 있다.
  • 그렇다면 프로토타입 스코프로 생성된 빈은 어떨까?

img2

  • 클라이언트에서 빈을 찾을 때에 생성되고 의존관계도 주입되서 클라이언트로 뿌린다.
  • 여기서 주목해야 할 점은 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 해주고 더 이상 관리를 안해준다는 것이다.
  • 때문에 @PreDestroy가 붙은 메서드도 호출되지 않는다. 클라이언트쪽에서 필요하다면 직접 호출해야 한다.

  • 프로토타입 빈 정리
    • 프로토타입 빈은 스프링 컨테이너에 요청할 때마다 새로 생성된다.
    • 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더 이상 관리하지 않는다. (이후에 클라이언트쪽에서 알아서 관리해야 한다.)
    • 종료 메서드가 호출되지 않기 때문에 필요하다면 클라이언트쪽에서 직접 호출해야 한다.

프로토타입 빈의 문제점

  • 프로토타입빈을 싱글톤 빈과 같이 사용할 때 문제가 발생할 수 있다. 아래 그림을 보자.

img3

  • 싱글톤 스코프로 생성되는 빈은 스프링 컨테이너 시작 시점에 생성되면서 이 때 의존관계를 주입받는다. 그런데 주입받는 객체가 프로토타입이면 어떨까?
  • 프로토타입 스코프를 사용하는 빈의 사용 목적은 호출할 때마다 다른 빈이 생성되서 나오도록 하는 것이다. 그러나 싱글톤 빈 내부에 가지고 있는 프로토타입 빈은 스프링 컨테이너 생성 시점에 주입이 끝난 빈일 것이다. 때문에 호출할 때마다 다른 빈이 생성되서 나오는 것이 아니라 계속 같은 빈이 나올 것이다.
  • 이제 이 문제를 해결하는 여러 방법에 대해 살펴보고 어떤 방법이 가장 좋을지 고민해보자!

해결방법1 : 스프링 컨테이너에 직접 요청

  • 가장 단순하고 무식한 방법이다. 싱글톤 빈 내부에 스프링 컨테이너 의존성을 주입받고 프로토타입 빈을 사용할 때 마다 스프링 컨테이너에 요청하는 방식이다.
static class ClientBean {
    @Autowired
    private ApplicationContext ac;
    
    public int logic() {
        PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
        prototypeBean.addCount();
        int count = prototypeBean.getCount();
        return count;
    } 
}
  • 이런식으로 의존관계를 외부에서 주입받는 것이 아니라 필요한 의존관계를 직접 찾는 것을 Dependency Lookup(DL, 의존관계 조회) 라고 한다.
  • 하지만 위 코드와 같이 스프링 컨테이너 전체를 주입 받으면 스프링 컨테이너에 종속적이게 되고, 단위 테스트도 어렵다.
  • 그리고 여기서 필요한 기능은 DL뿐인데 ApplicationContext는 너무 다양한 기능을 제공한다.
  • DL만 해주는 무언가가 있으면 좋을텐데?? -> 스프링에 이미 있다. ObjectProvider

해결방법2 : ObjectProvider

  • ObjectProvider는 스프링에서 만든 것으로 DL 기능을 제공한다. 사용방법은 아래 코드를 보자!

@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;

public int logic() {
    PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

해결방법3 : Provider

  • 자바 표준에서 제공하는 것으로 스프링 컨테이너가 아닌 다른 컨테이너에서도 사용할 수 있다.
  • 사용방법은 ObjectProvider와 거의 같다. (코드 참고)

@Autowired
private Provider<PrototypeBean> provider;

public int logic() {
    PrototypeBean prototypeBean = provider.get();
    prototypeBean.addCount();
    int count = prototypeBean.getCount();
    return count;
}

프로토타입 빈 정리

  • 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 사용하면 된다.
  • 실제 실무에서 웹개발을 하면 대부분 싱글톤 빈으로 해결할 수 있기 때문에 사용할 일은 거의 없다고 보면 된다.
  • 다만 ObjectProvider, Provider 같은 DL을 지원하는 것들은 알아두면 좋다.

웹 스코프

  • 웹 스코프는 웹 환경에서만 동작하며 프로토타입 스코프랑 다르게 해당 웹 스코프 종료 시점까지 스프링 컨테이너가 관리해준다.
  • 그러면 request 스코프에 대해서 알아보자!

img4

  • request 스코프는 HTTP request 요청마다 생성 및 할당된다. => 생성 시점이 HTTP request가 들어오는 시점이다.
  • 위 그림에서 클라이언트A의 요청과 클라이언트B의 요청이 있는데 이 둘은 다른 HTTP request 이므로 다른 객체가 할당된다.

request 스코프는 언제 사용될까?

  • 로그 처리
  • 동시에 여러 HTTP request가 들어오면 어떤 요청에 의해 생성된 로그인지 구분하기 어려울 수 있다. 이럴때 사용하면 좋다.

@Component
@Scope(value = "request")
public class MyLogger {
    private String uuid;
    private String requestURL;

    ...
}
@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";
    } 
}
  • 위 코드를 실행해보면 서버가 제대로 뜨지 않는다.
  • LogDemoController는 싱글톤 빈이어서 스프링 컨테이너가 뜨는 시점에 생성되고 의존관계가 주입된다. 그런데 이 시점에는 아직 HTTP request가 들어온 것이 없다.
  • 아직 스프링 컨테이너에 MyLogger라는 request 스코프를 가지는 빈은 생성되지 않은 것이다.
  • 없는 것을 주입하려고 하니까 당연히 에러가 발생한다.

해결방법1 : Provider

  • ObjectProvider를 사용해서 실제 HTTP reqeust가 들어올 때 request 스코프 빈을 생성하면 된다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerProvider;
    
    @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";
    }
}
  • ObjectProvider를 통해 getObject()를 호출하는 시점까지 request 스코프 빈의 생성을 지연할 수 있다.
  • ObjectProvider를 호출할 때, 같은 HTTP 요청에 대해서는 같은 스프링 빈이 반환된다.

해결방법2 : 프록시

  • 두번째 방법은 가짜 객체(proxy)를 주입해놓고 실제 호출 시점에 request 스코프 빈을 생성하는 방법을 사용하는 것이다.
@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    ...
}
  • 적용할 대상이 구현체면 TARGET_CLASS, 인터페이스면 INTERFACES를 넣어주면 된다.

img5

  • 프록시 객체는 내부에 실제 객체를 찾는 방법을 가지고 있다.
  • 실제 필요한 시점까지 생성을 미뤄뒀다가 필요할 때 생성해서 실제 객체를 가져온다.

핵심

  • 프록시 방법, Provider 방법의 핵심은 실제 필요할 때까지 객체의 생성을 미룬다는 점이다.

출처

  • https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8

태그:

카테고리:

업데이트: