ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • (스프링 기본) 30 - 빈 스코프 (싱글톤, 프로토 타입 - ObjectProvider, Provider)
    개발/Spring 2024. 10. 29. 15:34

    이전까지는 스프링 빈의 경우

    스프링 컨테이너 시작과 함께 시작 -> 스프링 컨테이너 종료 까지 유지된다고 배웠다.

    이는 기본적으로 스프링 빈이 싱글톤 빈으로 생성되기 때문인데,

    스코프란 빈이 존재할 수 있는 범위를 뜻한다. 

     

    * 스프링 스코프

    - 싱글톤 : 스프링 컨테이너 시작과 종료까지 (가장 넓은 범위)

    - 프로토타입 : 프로토타입 빈의 생성 의존관계 주입까지만 관여하고 더는 관리하지 않음 (짧음)

     

    * 웹 관련 스코프

    - request : 웹 요청이 들어오고 나갈때 까지 유지

    - session : 웹 세션이 생성되고 종료될 때까지 유지

    - application : 웹의 서블릿 컨텍스와 같은 범위로 유지

     

    ■ 프로토타입 스코프

     

    프로토타입은 싱글톤과 달리 새로운 인스턴스 생성해서 반환한다.

     

    빈을 스프링 컨테이너에 요청하게 되면,

    컨테이너는 이 시점에 프로토타입 빈을 생성하고 의존 관계를 주입한다. 

     

     

    스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환하지만 관리는 하지 않는다.

    같은 요청이 오면 항상 새로운 프로토타입 빈을 생성해서 반환한다. 

     

    ■ 정리

    스프링 컨테이너는 프로토 타입 빈을 생성하고, 의존관계 주입, 초기화까지만 처리한다. 

    관리는 하지 않기 때문에 프로토 타입 빈 책임은 프로토 타입 빈을 받은 클라이언트에 있다. 

    따라서 @PreDestroy 같은 종료 메서드가 호출되지 않는다. 

     

    public class PrototypeTest {
    
        @Test
        void prototypeBeanFind() {
            AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
            System.out.println("find prototypeBean1");
            PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
            System.out.println("find prototypeBean2");
            PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
    
            System.out.println("prototypeBean1 = " + prototypeBean1);
            System.out.println("prototypeBean2 = " + prototypeBean2);
            assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
    
            ac.close();
        }
    
        @Scope("prototype")
        static class PrototypeBean {
            @PostConstruct
            public void init() {
                System.out.println("PrototypeBean.init");
            }
    
            @PreDestroy
            public void destroy() {
                System.out.println("PrototypeBean.destroy");
            }
        }
    }
    

     

    싱글톤 빈은 스프링 컨테이너 생성 시점에 초기화 메서드 실행 되지만,

    프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회할 때 생성되고, 초기화 메서드도 실행된다.

     

    프로토타입은 생성, 의존관계 주입, 초기화까지만 관여해서 

    스프링 컨테이너 종료 시 @PreDestroy 와 같은 종료 메서드가 실행되지 않는다.

     

    ■ 프로토타입 빈 특징 정리

    - 스프링 컨테이너에서 요청할 때마다 생성

    - 스프링 컨테이너는 프로토타입 빈 생성, 의존관계 주입, 초기화까지만 관여

    - 종료 메서드 미호출

    - 조회한 클라이언트가 프로토타입 빈 관리해야 함. (종료 메서드에 대한 호출도 클라이언트가 해야함)

     

    ■ 프로토 타입 스코프를 싱글톤 빈과 함께 사용할 경우 생기는 문제 (프로토타입이 계속 생기지 않음)

    public class SingletonWithPrototypeTest1 {
    
        @Test
        void prototypeFind() {
            AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
            PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
            prototypeBean1.addCount();
            assertThat(prototypeBean1.getCount()).isEqualTo(1);
    
            PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
            prototypeBean2.addCount();
            assertThat(prototypeBean2.getCount()).isEqualTo(1);
        }
    
        @Test
        void singletonClientUserPrototype() {
            AnnotationConfigApplicationContext ac =
                    new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
            ClientBean clientBean1 = ac.getBean(ClientBean.class);
            ClientBean clientBean2 = ac.getBean(ClientBean.class);
    
            int count1 = clientBean1.logic();
            assertThat(count1).isEqualTo(1);
            int count2 = clientBean2.logic();
            assertThat(count2).isEqualTo(2);
        }
    
        @Scope("singleton")
        static class ClientBean{
            private final PrototypeBean prototypeBean; //생성시점에 주입
    
            @Autowired
            ClientBean(PrototypeBean prototypeBean) {
                this.prototypeBean = prototypeBean;
            }
    
            public int logic() {
                prototypeBean.addCount();
                int count = prototypeBean.getCount();
                return count;
            }
        }
    
    
        @Scope("prototype")
        static class PrototypeBean {
            private int count = 0;
    
            public void addCount() {
                count++;
            }
    
            public int getCount() {
                return count;
            }
    
            @PostConstruct
            public void init() {
                System.out.println("PrototypeBean.int" + this);
            }
    
            @PreDestroy
            public void destroy() {
                System.out.println("PrototypeBean.destroy");
            }
        }
    }

     

    clientBean 은 싱글톤으로 스프링 컨테이너 생성 시점에 함께 생성되고 의존관계도 주입된다. (자동주입)

    clientBean 주입 시점에 스프링 컨테이너 프로토타입 빈을 요청한다.

     

    스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean 반환하고, 프로토타입의 빈 count 는 0 을 가지게 된다.

    clientBean 은 프로토타입 빈을 내부 필드에 보관하게 되는 것이다. (참조값 보관)

     

     

    클라이언트에서 clientBean.logic() 을 호출하면 clientBean 은 prototypeBean 의 addCount() 를 호출하여

    프로토타입 빈의 count 를 증가시켜 1이 되게 된다. 

     

     

    클라이언트 B가 clientBean 스프링 컨테이너에 요청해서 받을 때 싱글톤이므로 항상 같은 clientBean 이 반환된다.

    여기서 clientBean 이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 생성자 주입으로 주입이 끝난 빈이다. 

     

    따라서 주입 시점에 스프링 컨테이너 요청에 의해 프로토타입 빈이 새로 생성된 것이지,

    사용할 때마다 새로 생성되는 것이 아니다.

     

    여기서 logic() 을 호출하게 되면 clientBean 은 prototypeBean 의 addCount() 를 호출해서 

    프로토타입 빈의 count 를 증가시키는데, 원래 1이였기 때문에 값은 2가 된다. 

     

        @Scope("singleton")
        static class ClientBean{
    //        private final PrototypeBean prototypeBean; //생성시점에 주입
    
            @Autowired
            ApplicationContext applicationContext;
    
    //        @Autowired
    //        ClientBean(PrototypeBean prototypeBean) {
    //            this.prototypeBean = prototypeBean;
    //        }
    
            public int logic() {
                PrototypeBean prototypeBean = applicationContext.getBean(PrototypeBean.class);
                prototypeBean.addCount();
                int count = prototypeBean.getCount();
                return count;
            }
        }

     

    프로토타입 생성을 원하면 단순하게 생성자 주입이 아니라,

    logic() 을 호출할 때마다 프로토타입 객체를 만들어주는 방법도 있다. 

     

    의존관계를 외부에서 주입(DI) 받는 게 아니라 직접 필요한 의존관계를 찾는 것을

    Dependency Lookup (DL) 의존관계 조회(탐색) 이라고 한다.

     

    ApplicationContext 전체 주입 받게 되면, 스프링 컨테이너에 종속적인 코드가 되고

    후에 단위테스트도 어려워진다. 

     

    지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에 대신 찾아주는 DL 기능만 제공하면 된다.

     

    ■ ObjectProvider, ObjectFactory

    @Scope("singleton")
    static class ClientBean{
    
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeansProvider;
    
        public int logic() {
            PrototypeBean prototypeBean = prototypeBeansProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

     

    getObject() 호출하면 그 때 PrototypeBean 찾아서 우리에게 반환해준다. 

    ObjectProvider 가 ObjectFactory 를 상속받고 있어서 더 많은 기능 보유한다. 

     

    ObjectProvider 스프링 컨테이너를 통해서 DL 을 간단하게 도와주는 것이다. 

    스프링이 제공하는 기능이나, 기능이 단순하여 단테나 mock 코드 만들기 훨씬 쉽다.

     

    * 특징

    - ObjectFactory : 기능 단순, 별도 라이브러리 필요 없음, 스프링 의존

    - ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리 등 편의 기능이 많음, 별도 라이브러리 필요 없음, 스프링 의존

     

    ■ JSR-330(자바표준) Provider

    implementation 'jakarta.inject:jakarta.inject-api:2.0.1'

     

    build.gradle 에 해당 코드 추가 

    import jakarta.inject.Provider;
    @Scope("singleton")
    static class ClientBean{
        @Autowired
        private Provider<PrototypeBean> prototypeBeansProvider;
    
        public int logic() {
            PrototypeBean prototypeBean = prototypeBeansProvider.get();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }

     

    import 후 Provider 교체하면 정상적으로 새로운 빈으로 등록된다.

    get() 으로 호출하게 되면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아 반환함 (DL)

    자바 표준이고 기능이 단순하여 단테나 mock 코드 만들기 쉽다.

     

    * 특징

    - get() 메서드 하나로 기능 매우 단순

    - 별도 라이브러리 필요

    - 자바 표준이므로 다른 컨테이너에서도 사용 가능

     

    ■ 정리

    - 매번 사용할 때마다 의존 관계 주입이 완료된 새로운 객체가 필요할 때 사용하면 되는데

    실무에서는 거의 사용하지 않는다. 

    - ObjectProvider, JSR-330(자바표준) Provider 프로토타입 뿐 아니라 DL 필요한 경우 언제든 사용 가능

     

    ※ 만약 다른 컨테이너를 사용할 일이 있다면 자바 표준을 사용하되,

    그렇지 않으면 스프링이 제공하는 기능을 사용하도록 하자.

Designed by Tistory.