빈 스코프

udonehn·2023년 11월 4일
0

빈 스코프란?

스프링은 스프링 컨테이너의 시작과 동시에 함께 생성되어서 스프링 컨테이너가 종료될 때까지 유지되는 싱글톤 스코프 뿐만 아니라 다양한 스코프를 지원한다.

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

프로토타입 스코프

프로토타입 스코프는 싱글톤 스코프와는 다르게 요청이 올 때마다 항상 새로운 인스턴스를 생성하여 반환한다. 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 관리하고 그 후에 스프링 컨테이너는 생성된 프로토타입 빈을 관리하지 않는다. 따라서 빈을 관리할 책임은 클라이언트에 있으며, @PreDestroy 같은 종료 메서드는 호출되지 않는다.

프로토타입 스코프로 빈을 등록하는 예시이다. @Scope("prototype")를 사용하였다. 초기화 이후에는 스프링 컨테이너가 관리하지 않기에, @PreDestroy 메서드는 호출되지 않는다.

    @Scope("prototype")
    public class PrototypeBean{
        @PostConstruct
        public void init(){
            System.out.println("SingletonBean.init");
        }
    }

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

싱글톤 빈에서 프로토타입 빈을 주입받아 사용할 경우를 생각해보자. 이 경우 문제가 발생할 수 있다. 싱글톤 빈은 컨테이너에 등록될 때에 프로토타입 빈을 딱 한 번 주입받아 컨테이너에 등록한다. 그 후 싱글톤 빈을 요청할 때마다 등록될 때 생성된 빈을 사용하는데, 여기서 문제가 발생한다. 항상 같은 싱글톤 빈을 사용하기 때문에 요청할 때마다 프로토타입 빈이 생성되어 주입되는 것이 아니고, 프로토타입 빈도 딱 한번만 생성되어 주입되게 된다.

프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 찾아주는 DL(Dependency Lookup) 기능을 제공하는 ObjectFactory와 ObjectProvider가 있다.

    @Scope("singleton")
    static class ClientBean{

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

        private int logic(){
            PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
  1. ObjectProvider<PrototypeBean>: PrototypeBean 프로토타입 클래스를 주입받는다.
  2. prototypeBeanProvider.getObject(): getObject() 메서드를 호출할 때마다 새로운 인스턴스를 생성하여 반환한다.

ObjectFactory는 getObject() 메서드를 제공하며, ObjectProvider는 ObjectFactory를 상속받아 더 많은 기능을 제공한다.

JSR-330 Provider

자바 표준을 사용하는 방법도 있다.

    @Scope("singleton")
    static class ClientBean{

        @Autowired
        private Provider<PrototypeBean> provider;

        private int logic(){
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
  1. private Provider<PrototypeBean>: javax.inject.Provider를 사용한다.
  2. provider.get(): 프로토타입 빈을 생성해 반환한다.

get() 메서드 하나로 기능이 매우 단순하며, 자바 표준을 준수하는 방법이다.

웹 스코프

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

request 스코프 예제 만들기

웹 스코프를 사용하기 위해 스프링 웹 라이브러리를 bulid.gradle에 추가한다. 이제 내장 톰켓 서버를 통해 웹 서버와 스프링을 함께 실행시킨다.

스코프와 Provider

request스코프 빈은 고객 요청이 왔을 때 생성되기 때문에 스프링 애플리케이션 실행과 동시에 주입을 시도하면 오류가 발생한다. 이에 Provider를 사용해 해결할 수 있다.

MyLogger

@Component
@Scope(value = "request")
public class MyLogger {
    public String uuid;
    public 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();
        System.out.println("[" + uuid + "] request scope bean created:" + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

@Scope(value = "request")를 통해 고객 요청이 왔을 때 생성되는 request 스코프로 사용된다.

LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    public final LogDemoService logDemoService;
    public 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";
    }
}
  • public final ObjectProvider<MyLogger>: provider를 사용한다.
  • myLoggerProvider.getObject(): MyLogger 빈을 반환한다. 이 때 MyLogger 빈이 생성되게 된다.

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final ObjectProvider<MyLogger> myLoggerProvider;

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

    }
}

myLoggerProvider.getObject(): 컨트롤러에서 생성된 MyLogger 빈을 반환한다.

정리

  • ObjectProvider를 사용하면 컨트롤러, 서비스가 컨테이너에 등록될 때 MyLogger를 주입하는 것이 아닌 DL(Dependency Lookup)를 할 수 있는 객체가 주입된다.
  • Lazy Injection: 의존성을 런타임 시점에 동적으로 주입할 수 있다.
  • 객체 별로 관리: 여러 요청이 오더라도 객체마다 따로 관리가 가능하기 때문에 편리하게 사용 할 수 있다.

스코프와 프록시

프록시 방식을 사용하면 마치 싱글톤을 사용하는 것처럼 편리하게 request scope를 사용할 수 있다.

MyLogger

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class MyLogger {
    public String uuid;
    public 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();
        System.out.println("[" + uuid + "] request scope bean created:" + this);
    }

    @PreDestroy
    public void close(){
        System.out.println("[" + uuid + "] request scope bean close:" + this);
    }
}

proxyMode = ScopedProxyMode.TARGET_CLASS: MyLogger의 가짜 프록시 클래스를 만들어 다른 빈에 주입된다.

LogDemoController

@Controller
@RequiredArgsConstructor
public class LogDemoController {

    public final LogDemoService logDemoService;
    public 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";
    }
}

실제 myLogger가 아닌 프록시 myLogger가 주입된다.

LogDemoService

@Service
@RequiredArgsConstructor
public class LogDemoService {

    private final MyLogger myLogger;

    public void logic(String id) {
        myLogger.log("service id = " + id);

    }
}

정리

  • @Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)를 사용하면 실제 빈을 등록하는 것이 아닌, 이를 상속한 가짜 프록시 객체를 생성해 스프링 컨테이너에 등록한다. (CGLIB에 의한 바이트코드 조작)
  • 가짜 프록시 객체는 실제 요청이 오면 그 때 내부에서 실제 빈을 요청한다.
  • ScopedProxyMode.TARGET_CLASS를 사용하면 스코프 프록시가 상속을 통해 실제 타겟 빈을 생성하고 관리하며, 필요할 때마다 새로운 인스턴스를 반환한다.

본 포스팅은 김영한 강사의 스프링 핵심 원리 강의를 수강하고 요약한 것으로, 해당 강의의 영상 및 강의자료를 참고하였습니다.

profile
안녕하세요. 만나서 반갑습니다.

0개의 댓글

관련 채용 정보