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

gorapaduckoo·2023년 7월 26일
0

스프링 기본편

목록 보기
10/10
post-thumbnail

인프런 김영한님의 스프링 핵심 원리 - 기본편 강의 내용을 바탕으로 작성한 글입니다.


지금까지 다양한 빈 스코프에 대해 알아보았다. 개발자는 어플리케이션의 특성에 따라 적절한 빈 스코프를 선택해야 하지만, 프로토타입 빈과 싱글톤 빈을 함께 사용할 때는 주의해야 할 점이 있다.

먼저 아래와 같이 동작하는 프로토타입 빈이 있다고 가정하자.

  • 클라이언트 A가 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너가 프로토타입 빈 인스턴스(PrototypeBean@x01)를 생성해서 반환한다.
    • PrototypeBean@x01.count: 0
  • 클라이언트 A가 addCount()를 호출하여 count 필드를 1 증가시킨다.
    • PrototypeBean@x01.count: 1

위와 같은 상황에서, 클라이언트 B가 등장했다고 생각해보자.

1. 프로토타입 빈을 직접 요청하는 경우

  • 클라이언트 B가 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너가 프로토타입 빈 인스턴스(PrototypeBean@x02)를 생성해서 반환한다.
    • PrototypeBean@x02.count: 0
  • 클라이언트 B가 addCount()를 호출하여 count 필드를 1 증가시킨다.
    • PrototypeBean@x02.count: 1

테스트 코드를 통해 확인해보자.
ExampleBean

public class ExampleBean {

    private int count = 0;
    public ExampleBean() {
        System.out.println("create ExampleBean");
    }


    @PostConstruct
    public void init() throws Exception {
        System.out.println("ExampleBean.init");
    }

    @PreDestroy
    public void close() throws Exception {
        System.out.println("ExampleBean.destroy");
    }

    public void addCount() {
        count++;
    }

    public int getCount() {
        return count;
    }

}

ExampleTest

public class ExampleTest {

    @Test
    public void exampleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(ExampleConfig.class);
        System.out.println("ac = " + ac);

        // exampleBean1
        ExampleBean exampleBean1 = ac.getBean(ExampleBean.class);
        exampleBean1.addCount();
        System.out.println("exampleBean1.count = " + exampleBean1.getCount());

        // exampleBean2
        ExampleBean exampleBean2 = ac.getBean(ExampleBean.class);
        exampleBean2.addCount();
        System.out.println("exampleBean2.count = " + exampleBean2.getCount());
    }

    @Configuration
    static class ExampleConfig {
        @Bean
        @Scope("prototype")
        public ExampleBean exampleBean() {
            return new ExampleBean();
        }
    }
}

실행 결과

count값이 예상대로 증가하는 것을 확인할 수 있다. 이처럼 프로토타입 빈을 직접 요청할 때는 문제가 없지만, 싱글톤 빈에서 프로토타입 빈을 사용할 때는 문제가 발생한다.



2. 싱글톤 빈에서 프로토타입 빈을 사용하는 경우

  • 스프링 컨테이너가 생성되면, 싱글톤인 singletonBean의 인스턴스가 생성되고 의존관계 자동 주입이 발생한다.
  • singletonBean은 의존관계를 주입받기 위해 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 스프링 컨테이너가 프로토타입 빈 인스턴스(PrototypeBean@x02)를 생성하여 싱글톤 빈에게 참조값을 반환해준다.
    • PrototypeBean@x02.count: 0
    • singletonBean은 싱글톤 빈이기 때문에, 주입받은 프로토타입 빈의 참조값 PrototypeBean@x02는 이 시점 이후로 변경되지 않는다.

  • 클라이언트 A가 스프링 컨테이너에게 싱글톤 빈을 요청한다.
  • 스프링 컨테이너는 singletonBean@x01을 반환해준다.
  • 클라이언트 A가 singletonBean.logic()을 호출하면, 싱글톤 빈은 prototypeBean.addCount()를 호출하여 count를 1 증가시킨다.
    • PrototypeBean@x02.count: 1

  • 클라이언트 B가 스프링 컨테이너에게 싱클톤 빈을 요청한다.
  • 스프링 컨테이너는 singletonBean@x01을 반환해준다.
    • 싱글톤 빈이 주입받은 참조값은 주입 시점 이후로 바뀌지 않음
    • 프로토타입 빈은 싱글톤 빈이 스프링 컨테이너에 요청할 때 생성됨
      (사용할 때마다 생성되는 게 아님)
  • 클라이언트 B가 singletonBean.logic()을 호출하면, 싱글톤 빈은 prototypeBean.addCount()를 호출하여 count를 1 증가시킨다.
    • PrototypeBean@x02.count: 2

코드를 통해 직접 확인해보자.

public class ExampleTest {

    @Test
    public void exampleTest() {
        ConfigurableApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class, PrototypeBean.class);

        // 클라이언트 A
        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        System.out.println("client A call logic()");
        singletonBean1.logic();

        // 클라이언트 B
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);
        System.out.println("client B call logic()");
        singletonBean2.logic();
    }


    @Scope("singleton")
    static class SingletonBean {
        private PrototypeBean prototypeBean;

        @Autowired
        public SingletonBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        public void logic() {
            prototypeBean.addCount();
            System.out.println("count = " + prototypeBean.getCount());
        }
    }


    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public int getCount() {
            return count;
        }

        public void addCount() {
            count++;
        }
    }
}

count값이 1이 되어야 하는 예상과 달리 count값이 2가 된 걸 확인할 수 있다. 싱글톤 빈이 생성 시점에만 의존관계를 주입받기 때문에 싱글톤 빈에 주입된 프로토타입 빈도 싱글톤 빈과 함께 계속 살아있어서 이런 일이 발생한다.

프로토타입 빈은 스프링 컨테이너에 요청될 때마다 생성되므로, 프로토타입 빈을 사용할 때마다 싱글톤 빈에서 ac.getBean(PrototypeBean.class)를 통해 프로토타입 빈을 요청하면 해결되기는 한다. 하지만 이런 방법은 빈이 ApplicationContext를 받아와야 하기 때문에, 스프링에 종속적이게 되고 단위 테스트가 어려워진다.


3. 해결 방법: Provider 사용하기

싱글톤 빈은 프로토타입 빈을 사용하려 할 때마다 스프링 컨테이너에 의존관계를 요청해야 한다. 이처럼 의존관계를 주입받지 않고, 필요할 때 직접 요청하는 것을 Dependency Lookup (의존관계 조회) 라고 한다.

스프링에서는 ObjectProvider를 통해 특정 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공한다. SingletonBean을 아래와 같이 수정한 뒤 테스트를 다시 실행해보자.

    @Scope("singleton")
    static class SingletonBean {
        private ObjectProvider<PrototypeBean> objectProvider;

        @Autowired
        public SingletonBean(ObjectProvider<PrototypeBean> objectProvider) {
            this.objectProvider = objectProvider;
        }

        public void logic() {
            PrototypeBean prototypeBean = objectProvider.getObject();
            prototypeBean.addCount();
            System.out.println("count = " + prototypeBean.getCount());
        }
    }

count값이 1이 된 것을 확인할 수 있다. 의존관계 주입 시점에 ObjectProvider를 대신 주입받고, 프로토타입 빈이 필요한 시점에 objectProvider.getObject()를 통해 프로토타입 빈을 요청하여 새로 생성된 인스턴스를 받아오기 때문이다.

ObjectProvider는 스프링에 의존적이다. 스프링에 의존하고 싶지 않다면, javax.inject.Provider를 사용하면 된다. (gradle 추가 설정 필요)

    @Scope("singleton")
    static class SingletonBean {
        private Provider<PrototypeBean> provider;

        @Autowired
        public SingletonBean(Provider<PrototypeBean> provider) {
            this.provider = provider;
        }

        public void logic() {
            PrototypeBean prototypeBean = provider.get();
            prototypeBean.addCount();
            System.out.println("count = " + prototypeBean.getCount());
        }
    }

0개의 댓글