본문 바로가기
Spring/Spring core basic

[Spring core basic] 44 - 프로토타입 빈을 싱글톤 빈과 함께 사용하면 발생하는 문제점

by Kloong 2022. 5. 3.

참고

더보기

Spring core basic 시리즈는 김영한 님의 "스프링 핵심 원리 - 기본편" 강의를 정리한 글입니다. 글에 첨부된 사진은 해당 강의의 강의 자료에서 캡쳐한 것입니다. 제 Github에만 올려뒀다가, 정보 공유와 강의 홍보(?)를 위해 블로그에도 업로드합니다. 마크다운을 잘 쓰지 못해서 가독성이 조금 떨어지는 점 양해 바랍니다.

 

 

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

스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., - 강의 소개 | 인프런...

www.inflearn.com

프로토타입 빈을 싱글톤 빈과 함께 사용하면 발생하는 문제점

스프링 컨테이너에 프토토타입 스코프의 빈을 요청하면 항상 새로운 객체 인스턴스를 생성해서 반환한다. 그런데 프로토타입 빈을 싱글톤 빈과 함께 사용할 때는 의도한 대로 잘 동작하지 않을 수 있으므로 주의해야 한다.

그림과 코드로 한 번 살펴보자.

프로토타입 빈 직접 요청

먼저 스프링 컨테이너에 프로토타입 빈을 직접 요청하는 예제를 살펴보자.

스프링 컨테이너에 프로토타입 빈 직접 요청 1

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

스프링 컨테이너에 프로토타입 빈 직접 요청 2

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

위의 상황을 코드로 확인해보자.

SingletonWithPrototypeTest1.java
//package, import 생략

public class SingletonWithPrototypeTest1 {

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

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

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        prototypeBean2.addCount();
        System.out.println(
        "prototypeBean2.getCount() = " + prototypeBean2.getCount());
        Assertions.assertThat(prototypeBean2.getCount()).isEqualTo(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 destroy() {
            System.out.println("PrototypeBean.destroy " + this);
        }
    }
}

코드를 실행시키면,

PrototypeBean.init com.kloong.corebasic1.scope.SingletonWithPrototypeTest1$PrototypeBean@7188af83
prototypeBean1.getCount() = 1
PrototypeBean.init com.kloong.corebasic1.scope.SingletonWithPrototypeTest1$PrototypeBean@7ac296f6
prototypeBean2.getCount() = 1

두 빈의 count가 모두 1임을 확인할 수 있다.

사실 굳이 코드로 확인 안해봐도 알 수 있는 당연한 결과다. 이 예제는 다음의 예제를 살펴보기 위한 빌드업이다.

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

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

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

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

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

  1. 클라이언트 A는 스프링 컨테이너에 clientBean을 조회해서 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean 객체가 반환된다.
  2. 클라이언트 A는 clientBean.logic() 을 호출한다.
  3. clientBean은 prototypeBean의 addCount() 를 호출해서 prototypeBean의 count를 1 증가시킨다. 따라서 prototype의 count값이 0애서 1로 증가한다.

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

  1. 클라이언트 B는 스프링 컨테이너에 clientBean을 조회해서 받는다. clientBean은 싱글톤이므로 항상 같은 clientBean 객체가 반환된다.
  2. 여기서 중요한 점이 있는데, clientBean이 내부에 가지고 있는 prototypeBean은 clientBean의 DI 시점에 생성되어 주입된 빈이다. 앞에서 언급 했듯이 프로토타입 빈은 스프링 컨테이너에 요청할 때마다 생성되는데, clientBean의 DI 시점에만 prototypeBean이 요청을 받아서 생성되었고, 그 이후로는 DI 받은 해당 빈을 계속 사용하기만 하는 것이지 새롭게 생성하는 것이 아니다.
  3. 클라이언트 B는 clientBean.logic() 을 호출한다.
  4. clientBean 은 prototypeBean의 addCount() 를 호출해서 prototypeBean의 count를 증가한다. 이 프로토타입 빈은 client A가 addCount() 한 빈과 동일한 빈이므로, count 값이 0에서 1이 아닌, 1에서 2로 증가한다.

위 상황을 코드로 확인해보자.

SingletonWithPrototypeTest1.java
//package, import 생략

public class SingletonWithPrototypeTest1 {

    @Test
    void singletonClientUsePrototype() {
        ConfigurableApplicationContext 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);

        PrototypeBean prototypeBean1 = clientBean1.getPrototypeBean();
        PrototypeBean prototypeBean2 = clientBean2.getPrototypeBean();

        Assertions.assertThat(prototypeBean1).isSameAs(prototypeBean2);

        ac.close();
    }

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

        //clientBean을 생성하는 시점에 prototypeBean이 생성되고 주입 됨.
        //clientBean은 싱글톤이므로 한 번만 생성된다.
        //따라서 prototypeBean도 하나만 존재하게 된다.
        public ClientBean(PrototypeBean prototypeBean) {
            this.prototypeBean = prototypeBean;
        }

        @PostConstruct
        public void init() {
            System.out.println("ClientBean.init");
        }

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

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

        //테스트용 코드
        public PrototypeBean getPrototypeBean() {
            return prototypeBean;
        }
    }

    @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 destroy() {
            System.out.println("PrototypeBean.destroy " + this);
        }
    }
}

코드를 실행시켜보면,

22:23:21.092 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Creating shared instance of singleton bean 'singletonWithPrototypeTest1.ClientBean'
PrototypeBean.init com.kloong.corebasic1.scope.SingletonWithPrototypeTest1$PrototypeBean@7a35b0f5
22:23:21.112 [main] DEBUG org.springframework.beans.factory.support.DefaultListableBeanFactory - Autowiring by type from bean name 'singletonWithPrototypeTest1.ClientBean' via constructor to bean named 'singletonWithPrototypeTest1.PrototypeBean'
ClientBean.init
22:23:21.173 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@635eaaf1, started on Wed Apr 13 22:23:21 KST 2022
ClientBean.destroy

실행 과정을 분석해보자.

  1. 먼저 clientBean이 생성된다.
  2. 이 때 constructor injection에 의해 prototypeBean을 주입받아야 한다. 프로토타입 빈은 요청 시점에 생성되므로, 이 시점에 prototypeBean을 생성한다.
  3. prototypeBean을 생성하고, DI를 하고 (물론 이 예제에서는 prototypeBean에 DI 해줄 게 없다), 초기화 메서드를 실행시킨다.
  4. 그 다음 clientBean에 prototypeBean을 주입한다.
  5. clientBean의 초기화 메서드를 실행한다.

여기까지가 스프링 컨테이너 생성 시점에서 일어나는 일이다.

이후에 clientBean을 여러 번 조회해도, clientBean은 싱글톤이기 때문에 객체를 생성하지 않는다. logic() 메서드를 호출해서 prototypeBean을 사용하더라도, prototypeBean은 이미 싱글톤 빈인 clientBean의 생성 시점에서 주입되어졌기 때문에, clientBean이 다시 생성되지 않는 이상은 새로 만들어지지 않는다. 다시 말하지만 clientBean은 싱글톤이기 때문에 다시 생성되는 일은 없다.

따라서 clientBean1에서 호출한 logic() 의 결과가 그대로 남아있어서, count가 0에서 1, 1에서 2로 증가하게 된다.

확인차 clientBean1의 prototypeBean과 clientBean2의 prototypeBean이 같은 객체인지 확인해봤더니, 프로토타입 빈임에도 불구하고 같은 객체임을 확인할 수 있다.

개발자의 원래 의도 - logic() 호출 시마다 prototypeBean을 새로 생성한다

개발자가 굳이 프로토타입 빈을 사용한 이유는, prototypeBean을 사용(조회)할 때마다 새로운 빈을 받아서 쓰고 싶기 때문일 것이다. 그게 아니라면 그냥 싱글톤을 쓰면 된다.

이 문제를 해결하기 위해서 ApplicationContext를 clientBean이 주입받아서, logic() 을 실행할 때마다 prototypeBean을 새로 생성받는 것도 가능하긴 하다. 하지만 코드가 너무 복잡해진다.

이 문제를 해결하는 방법은 다음 장에 계속...

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

댓글