싱글톤 빈과 함께 사용시 문제점 과 문제 해결 방법

스프링 컨테이너에 프로토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다.

하지만 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않으므로 주의해야 한다.

 

프로토타입 빈 직접 요청

  • 클라이언트A는 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환('x01')한다. 해당 빈의 count 필드 값은 0이다.
  • 클라이언트는 조회한 프로토타입 빈에 addCount를 호출하면서 count필드를 +1 한다.
  • 결과적으로 프로토타입 빈 ('x01')의 count는 1이 된다.

  • 클라이언트B는 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너는 프로토타입 빈을 새로 생성해서 반환('x02')한다. 해당 빈의 count 필드 값은 0이다.
  • 클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하면서 count 필드를 +1 한다.
  • 결과적으로 프로토타입 빈('x02')의 count는 1이 된다.

Test

public class SingletonWithPrototypeTest1 {

    @Test
    void prototypeFind(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(PrototypeBean.class);

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        prototypeBean1.addCount();
        System.out.println("prototypeBean1 : " + prototypeBean1.getCount());
        Assertions.assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        System.out.println("prototypeBean2 : " + prototypeBean2.getCount());
        Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(1);

//        출력
//        PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@5fb4aba9
//        prototypeBean1 : 1
//        PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@2ed424ed
//        prototypeBean2 : 1
    }

    @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.init " + this);
        }

        @PreDestroy
        public void destory(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}

싱글톤 빈에서 프로토타입 빈 사용

이번에는 clientBean이라는 싱글톤 빈이 의존관계 주입을 통해서 프로토타입 빈을 주입받아서 사용하는 예를 보자

 

  • clientBean은 싱글톤 이므로 , 보통 스프링 컨테이너 생성 시점에 함께 생성되고 , 의존관계 주입도 발생한다.
  • clientBean은 의존관계 자동 주입을 사용한다. 주입 시점에 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너는 프로토타입 빈을 생성해서 clientBean에 반환한다. 프로토타입 빈의 count 필드 값은 0이다.
  • 이제 clientBean은 프로토타입 빈을 내부 필드에 보관한다. (정확히는 참조값을 보관한다.)

  • 클라이언트A는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • 클라이언트A는 clientBean.logic() (addCount메소드 포함)을 호출한다.
  • clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. count값이 1이 된다.

  • 클라이언트B는 clientBean을 스프링 컨테이너에 요청해서 받는다. 싱글톤이므로 항상 같은 clientBean이 반환된다.
  • clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지 , 사용 할 때마다 새로 생성되는 것이 아니다.
  • 클라이언트B는 clientBean.logic() (addCount메소드 포함)을 호출한다.
  • clientBean은 prototypeBean의 addCount()를 호출해서 프로토타입 빈의 count를 증가한다. 원래 1이였으므로 2가 된다.

 

Test

public class SingletonWithPrototypeTest1 {

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext ac =
                new AnnotationConfigApplicationContext(ClientBean.class , PrototypeBean.class);

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        Assertions.assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        Assertions.assertThat(count2).isEqualTo(2);

    }
    @Scope("singleton")
    static class ClientBean{
        private final PrototypeBean prototypeBean;

        // PrototypeBean 호출 시점이 ClientBean이 스프링 빈으로 등록 될 때 1번 만 호출
        // 결국 같은 PrototypeBean이 사용된다.
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public int logic(){
            prototypeBean.addCount();
            return prototypeBean.getCount();
        }
    }

    @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.init " + this);
        }

        @PreDestroy
        public void destory(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}
  • 스프링은 일반적으로 싱글톤 빈을 사용하므로 , 싱글톤 빈이 프로토타입 빈을 사용하게 된다.
  • 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에 , 프로토타입 빈이 생성되기는 하지만 싱글톤 빈과 함께 계속 유지되는 것이 문제다.
  • 프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라 , 사용할 때 마다 새로 생성해서 사용하는것을 원한다.

 

참고

  • 여러 빈에서 같은  프로토타입 빈을 주입 받으면 , "주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다"
  • 예를 들어
    • clientA , clientB가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입 받는다.
    • clientA -> prototypeBean@x01
    • clientB -> prototypeBean@x02
    • 물론 사용할 때 마다 새로 생성되는 것은 아니다.

 

문제 해결

싱글톤 빈과 프로토타입 빈을 함께 사용할 때 ,

어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?

 

스프링 컨테이너에 요청

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.

 

  • ac.getBean()을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
  • 의존관계를 외부에서 주입(DI)받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL)의존관계 조회(탐색) 이라 한다.
  • 그런데 이렇게 스프링의 애플리케이션 컨텍스트 전체를 주입 받게 되면 , 스프링 컨테이너에 종속적인 코드가 되고 ,단위 테스트도 어려워 진다.
  • 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너 에서 대신 찾아주는 DL정도의 기능만 제공하는 무언가가 있으면 된다.

ObjectProvider , ObjectFactory

    @Scope("singleton")
    static class ClientBean{
        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

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

 

  • ObjectProvider
    • prototypeBeanProvider.getObject()는 ac.getBean() 처럼 스프링 컨테이너를 통해 (제네릭에 정의된)PrototypeBean을 찾아 반환해준다. (DL)
    • 스프링이 제공하는 기능을 사용하지만 , 기능이 단순하므로 단위테스트를 만들거나 mock코드를 만들기는 훨씬 쉬워진다.
    • 너무 무거운 ac를 새로 생성하는 것 보다 딱 필요한 DL정도의 기능만 제공한다.
    • ObjectFactory 상속 , 옵션 , 스트림 처리의 편의 기능이 많고, 별도의 라이브러리 필요없고,  스프링에 의존한다.
  • ObjectFactory
    • 기능이 단순 , 별도의 라이브러리 필요 없고 , 스프링에 의존한다.
    • getObject() 메소드 하나만 존재한다.

JSR-330 Provider

마지막 방법은 javax.inject.Provider라는 JSR-330 자바 표준을 사용하는 방법이다.

 

Gradle추가

implementation 'javax.inject:javax.inject:1'
    @Scope("singleton")
    static class ClientBean{
        @Autowired
        private Provider<PrototypeBean> prototypeBeanProvider;

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

 

  • provider.get()을 통해 항상 새로운 프로토타입 빈을 생성한다.
  • provider.get()을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
  • 기능이 단순하므로 단위테스트를 만들거나 mock코드를 만들기는 훨씬 쉬워진다.
  • Provider는 지금 딱 필요한 DL정도의 기능만 제공한다.
  • 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

 

 

그러면 프로토타입 빈을 언제 사용할까? 

  • 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.
  • 그런데 실무에서 개발하다 보면 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접 적으로 사용하는 경우는 매우 드물다.
  • ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 , 스프링 외에 별도의 의존관계 추가가 필요 없이 때문에 편리하다.
  • 만약 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야 한다.

 

 

 

 

 

스프링 핵심 원리 - 기본편 - 인프런

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다. 초급 프레임워크 및 라이브러리 웹 개발 서버 개발 Back-End Spring 객체지향 온

www.inflearn.com

 

'스프링 핵심 원리 > 프로토 타입 스코프' 카테고리의 다른 글

웹 스코프  (0) 2021.01.02
빈 스코프란?  (0) 2021.01.02