jhhan의 블로그

스프링 코어(16) - 프로토타입과 싱글톤을 같이 쓰는 경우 본문

Spring

스프링 코어(16) - 프로토타입과 싱글톤을 같이 쓰는 경우

jhhan000 2021. 10. 4. 22:47

이번 포스트에서는 프로토타입과 싱글톤타입이 같이 쓰이는 경우에 대해서 알아보겠습니다.

 

이전 포스트에서 프로토타입에 대해서 다뤘습니다.

하지만 실무에서는 보통 싱글톤과 프로토타입이 같이 쓰이는 경우가 많다고 합니다.

그러면 아마도 문제가 생길 경우가 있을 것입니다.

그에 대해 알아보겠습니다.

 

 

먼저 프로토타입에 대해 한번 복습해봅니다.

scope 패키지 밑에 SingletonWithPrototypeTest1.java를 생성합니다.

import org.assertj.core.api.Assertions;

public class SingletonWithPrototypeTest1 {

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

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

        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
        bean2.addCount();
        System.out.println("bean2.count = " + bean2.getCount());
        Assertions.assertThat(bean2.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");
        }
    }
}

이렇게 코드를 작성해봅니다.

그리고 실행을 한다면..?

이렇게 나옵니다.

bean1과 bean2는 프로토타입 빈이므로 각각 따로 생성되어서 서로의 값에 영향을 주지 않습니다.

그래서 각각의 count 값이 1로 나오는 것을 확인할 수 있습니다.

(프로토타입에 대해 배웠으니 이정도는 굉장히 쉽습니다... ㅎㅎ)

 

그럼 프로토타입과 싱글톤이 같이 쓰이게 되는 경우를 한번 만들어봅니다.

import org.assertj.core.api.Assertions;

public class SingletonWithPrototypeTest1 {

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

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

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

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

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

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean2.logic();
//        Assertions.assertThat(count2).isEqualTo(1);
        Assertions.assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean 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 destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

위와 같이 ClientBean이라는 싱글톤 타입의 빈을 추가했습니다.

  • 분명히 프로토타입으로 선언을 했습니다.
  • 그런데 Assertions.assertThat(count1).isEqualTo(1); & Assertions.assertThat(count2).isEqualTo(2); 로
    검사를 하네요...
  • 실행해보면 통과가 됩니다.
  • 그렇다면 만약 Assertions.assertThat(count2).isEqualTo(1); 로 한다면..?

에러가 생깁니다.

1을 예상했는데 실제값은 2가 나와서 에러가 떳네요..

분명 프로토타입으로 선언을 했는데 하나는 1이고 다른 하나는 2가 되어버립니다...

 

 

ClientBean을 다시 살펴봅니다.

  • ClientBean은 싱글톤이기 때문에 생성시점에서 한 번 생성되고 스프링 컨테이너가 관리하게 됩니다.
  • 이 때 주입 시점에서 프로토타입 빈을 생성해서 ClientBean에게 넘겨줍니다.
  • 그 상태로 ClientBean이 계속 유지됩니다.(싱글톤 타입으로)
  • 그래서 프로토타입 빈으로 생성을 했더라도 실제 작동 방식은 싱글톤이 되는 것이죠.

그래서 logic()을 호출할 때마다 count의 값은 계속 증가합니다.

이러면 프로토타입을 사용하는 의미가 없어집니다.

 

그리고 또 다른 상황을 한 번 보죠

public class SingletonWithPrototypeTest1 {

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

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

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

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

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

        ClientBean bean2 = ac.getBean(ClientBean.class);
        int count2 = bean2.logic();
        Assertions.assertThat(count2).isEqualTo(1);
//        Assertions.assertThat(count2).isEqualTo(2);
    }

    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean;  // 생성시점에 주입 됨

        public ClientBean(PrototypeBean prototypeBean) { this.prototypeBean = prototypeBean; }

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

    @Scope("singleton")
    static class ClientBean2 {
        // 싱글톤으로 생성되지만 주입받을 때는 각각 따로 생성되어서 주입된다.
        private final PrototypeBean prototypeBean;  // 생성시점에 주입 됨

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

ClientBean2라는 빈을 새롭게 추가했습니다.

여기도 프로토타입 빈을 주입받습니다.

  • 여러 개의 빈에서 주입을 받는 상황입니다.
  • 이 때는 주입받는 시점에 각각 새로운 프로토타입 빈을 생성해서 주입됩니다.
  • 그래서 ClientBean과 ClientBean2는 PrototypeBean이라는 주입 받았지만, 서로 다른 PrototypeBean을 주입 받은 것입니다.

이렇게 알아본 것은 그냥 덤 입니다. ㅎ

 

 

어쨌든 프로토타입처럼 사용하려 하는데 이렇게 싱글톤으로 작동하면

로직이 원활히 진행되지 않을 것입니다.

 

 

다음 포스트에서 싱글톤 과 프로토타입을 함께 사용하는 방법을 알아보겠습니다.

 

 

 

 

 

 

 

출처: 인프런 - 스프링 핵심원리(기본편) by 김영한