본문 바로가기
Framework/Spring

[Spring] 스프링 핵심 원리 (기본) - 8. 빈(Bean) 스코프

by pilgyeong 2023. 2. 5.

8. 빈 스코프

8.1 스코프 종류

Spring Bean은 기본적으로 싱글톤 스코프로 생성되어 스프링 컨테이너의 시작과 함께 생성되고, 스프링 컨테이너가 종료될 때까지 유지된다. 이 외에 다양한 스코프를 지원한다.

  • 싱글톤: 기본(default) 스코프로서 스프링 컨테이너의 시작과 종료가지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고, 더 이상 관리하지 않는 매우 짧은 스코프이다.
  • 웹 관련:
    • request: 웹 요청이 들어오고 나갈 때까지만 유지되는 스코프이다.
    • session: 웹 세션이 생성되고 종료될 때까지만 유지되는 스코프이다.
    • application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프이다.

 

8.2 프로토타입 스코프

해당 스코프를 스프링 컨테이너에서 조회하면, 스프링 컨테이너는 항상 새로운 인스턴스를 생성해서 반환한다. 여기서 핵심은 스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입과 초기화까지만 처리한다는 것이다. 클라이언트에 빈을 반환한 후 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 그래서 @PreDestory같은 종료 메소드가 호출되지 않는다.

 

8.2.1 싱글톤 빈에서 프로토타입 빈 사용시 문제점

싱글톤 빈은 보통 스프링 컨테이너 생성 시점에 함께 생성되고, 의존관계 주입도 발생한다. 따라서, 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청해서 내부 필드에 보관한다. 그런데, 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 프로토타입 빈이 싱글톤 빈과 함께 계속 유지되는 문제가 발생한다.

8.2.2 Provider로 문제 해결

싱글톤 빈과 프로토타입 빈을 함께 사용할 때마다 항상 새로운 프로토타입 빈을 생성하기 위해서는, 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이 가장 간단한 방법이다. 이처럼 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 ObjectProvider이다.

public class SingletonBean {
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic() {
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        prototypeBean.addCount();
        return prototypeBean.getCount();
    }   
}
  • ObjectProvidergetObject()를 호출하면, 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아 반환한다. (DL)
  • 이외의 많은 편의 기능을 제공하고, 스프링에 의존적이다.
public class SingletonBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanObjectProvider;

    public int logic() {
        PrototypeBean object = prototypeBeanObjectProvider.get();
        object.addCount();
        return object.getCount();
    }   
}
  • 자바 표준의 Provider를 사용하는 방법도 존재한다.
  • Providerget()을 호출하면, 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아 반환한다. (DL)
  • 이 경우에는 별도의 라이브러리가 필요하지만, 자바 표준으로 다른 컨테이너에서도 사용할 수 있는 점이 특징입니다.



8.3 웹 스코프

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

8.3.1 웹 스코프의 종류

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

 

8.3.2 request 스코프

동시에 많은 HTTP 요청이 들어올 때, 정확히 어떤 요청이 남긴 로그인지 구분하기 위해 request 스코프를 사용

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

    public void log(String message) {
        System.out.println("[" + this.uuid + "]" + "[" + this.requestURL + "] " + message);
    }

    public void setRequestURL(String requestURL) {
        this.requestURL = requestURL;
    }

    @PostConstruct
    public void init() {
        this.uuid = UUID.randomUUID().toString();
        System.out.println("[" + this.uuid + "] request scope bean create: " + this);
    }

    @PreDestroy
    public void destroy() {
        System.out.println("[" + this.uuid + "] request scope bean close: " + this);
    }
}
  • 로그를 출력하기 위한 클래스로서 @Scope(value="request")로 지정하여 이 빈은 HTTP 요청 당 하나씩 생성되고, HTTP 요청이 끝나는 시점에 소멸된다.
  • 이 빈이 생성되는 시점에 자동으로 초기화 메소드를 사용해서 'UUID'를 저장한다. HTTP 요청 당 하나씩 생성되므로, 다른 HTTP 요청과 구분할 때 UUID를 사용한다.
  • 이 빈이 소멸되는 시점에 소멸 전 메소드로 종료 메세지를 남긴다.
@Controller
@RequiredArgsConstructor
public class LogDemoController {
    private final LogDemoService logDemoService;
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
//    private final MyLogger myLogger;

    @RequestMapping("log-demo")
    @ResponseBody
    public String logDemo(HttpServletRequest request) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        String requestURL = request.getRequestURL().toString();
        myLogger.setRequestURL(requestURL);

        myLogger.log("controller test");
        logDemoService.logic("test id");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final ObjectProvider<MyLogger> myLoggerObjectProvider;
//    private final MyLogger myLogger;

    public void logic(String id) {
        MyLogger myLogger = myLoggerObjectProvider.getObject();
        myLogger.log("service id = " + id);
    }
}
  • 스프링 애플리케이션을 실행하는 시점에 싱글톤 빈은 생성해서 주입이 가능하지만, request 스코프 빈은 아직 생성되지 않기 때문에 MyLogger 빈이 아직 만들어지기 전이다.
  • 이를 해결하기 위해 Provider를 사용해서 request 스코프 빈의 생성을 지연할 수 있다.



8.4 스코프와 프록시

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
}
  • 프록시 방식으로서 MyLogger의 가짜 프록시 클래스를 만들어두고, HTTP request와 상관없이 가짜 프록시 클래스를 다른 빈에 미리 주입해둘 수 있다.
@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("test id");
        return "OK";
    }
}
@Service
@RequiredArgsConstructor
public class LogDemoService {
    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("service id = " + id);
    }
}
  • 'CGLIB' 라이브러리로 클래스를 상속 받은 가짜 프록시 객체를 만들어서 주입한다.
  • 가짜 프록시 객체는 요청이 들어오면, 그때 내부에서 진짜 빈을 요청하는 위임 로직을 갖고 있다.
  • 클라이언트 입장에서 사실 원본인지 아닌지도 모르며, 동일하게 사용할 수 있다. (다형성)

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