10 min read

<Spring> 스프링 핵심원리 이해 9 - 스프링 빈 스코프(프로토타입 스코프)

빈 스코프란

지금까지 우리는 스프링 빈이 스프링 컨테이너의 시작과 함께 생성되어서 스프링 컨테이너가 종료 될때까지 유지된다고 학습했다.이것은 스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문이다. scope란 단어의 뜻 그대로 스프링 빈이 존재할 수 있는 범위를 의미한다. 즉, 생존할 수 있는 기간을 뜻한다. 스프링은 다음과 같은 다양한 스코프들을 가진다.

  1. Singleton : 기본 스코프,스프링 컨테이너의 시작과 종료까지의 수명을 가지는 스코프이다.
  2. Prototype : 스프링 컨테이너는 프로토타입 스프링 빈의 생성과 의존관계 주입까지만 관여하고 이후의 과정은 관여하지 않는다.즉 생성에서 의존관계 주입까지 컨테이너의 관리를 받고 이후는 해당 빈을 호출한 사용자에의 의해서 종료된다.
  3. 웹 관련 스코프
    – request : 웹 요청이 들어오고 나갈때까지 유지되는 스코프– session : 웹 세션이 생성되고 종료될때까지 유지되는 스코프– application : 웹의 서블릿 Context와 같은 범위로 유지되는 스코프이다.

프로토타입 스코프

싱글톤 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.반면에 프로토타입 스코프 타입의 빈을 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 생성하고 해당 인스턴스를 반환한다.

<싱글톤 스코프 빈 요청 과정>

싱글톤 빈 요청
싱글톤 빈 요청
  1. 싱글톤 스코프의 빈을 컨테이너에 요청한다.
  2. 스프링 컨테이너는 본인이 관리하는 스프링 빈을 반환한다.
  3. 이후에 동일한 요청이 들어와도 같은 객체 인스턴스를 반환한다.

<프로토타입 스코프 빈 요청 과정>

  1. 프로토타입 스코프의 빈을 컨테이너에 요청한다.
  2. 스프링 컨테이너는 해당 시점에 프로토타입 빈을 생성하고, 필요한 의존관계 주입한다.
  3. 스프링 컨테이너는 생성한 프로토타입 빈을 클라이언트에 반환한다.
  4. 이후에 동일한 요청이 들어올 경우, 항상 새로운 인스턴스를 생성해서 반환한다.

여기서 우리가 알 수 있는 점은 스프링 컨테이너는 프로토타입 빈을 생성하고 의존관계 주입,초기화까지만 처리한다는 것이다. 클라이언트에게 반환 이후부터 스프링 컨테이너는 해당 스프링 빈에 대한 관리를 하지 않는다. 이후 빈의 종료까지 프로토타입을 관리할 책임은 빈을 요청한 클라이언트에게 있다.그래서 @PreDestroy같은 종료 메서드가 호출 되지 않는다.

<싱글톤 스코프 빈 테스트 코드>

public class SingletonTest {
    @Test
    void singletonBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);
        SingletonBean bean1 = ac.getBean(SingletonBean.class);
        SingletonBean bean2 = ac.getBean(SingletonBean.class);
        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        assertThat(bean1).isSameAs(bean2);

        ac.close();
    }
    static class SingletonBean {
        @PostConstruct
        public void init() {
            System.out.println("SingletonBean.init");
        }
        @PreDestroy
        public void destroy() {
            System.out.println("SingletonBean.destroy");
        }
    }
} 
싱글톤 스코프 테스트 결과
싱글톤 스코프 테스트 결과

<프로토타입 스코프 빈 테스트 코드>

public class PrototypeTest {
    @Test
    void prototypeBeanFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        PrototypeBean bean1 = ac.getBean(PrototypeBean.class);
        PrototypeBean bean2 = ac.getBean(PrototypeBean.class);
        System.out.println("bean1 = " + bean1);
        System.out.println("bean2 = " + bean2);
        assertThat(bean1).isNotSameAs(bean2);

        bean1.destroy();
        bean2.destroy();

        ac.close();
    }
    @Scope("prototype")
    static class PrototypeBean{
        @PostConstruct
        public void init() {
            System.out.println("PrototypeBean.init");
        }

        @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.close");
        }
    }
}
프로토타입 스코프 테스트 결과
프로토타입 스코프 테스트 결과

프로토타입 스코프 - 싱글톤 빈과 함께 사용하기

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

  • 클라이언트 A는 스프링 컨테이너에 프로토타입 빈을 요청한다.
  • 컨테이너는 프로토타입 빈을 새롭게 생성해서 A에게 반환한다.(x01)
  • 해당 빈의 count 필드 값은 0이다.
  • 클라이언트는 조회한 프로토타입 빈에 addCount()를 호출하여 count를 1 증가시킨다.
  • 결과적으로 프로토타입빈(x01)의 count는 1이 된다.
  • 클라이언트 B또한 동일한 요청을 컨테이너에게 전달한다.
  • 컨테이너는 새로운 프로토타입 빈(x02)를 만들어서 반환한다.
  • 마찬가지로 addCount()를 수행하면 x02의 count 값도 1이 된다.

<테스트 코드>

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);
}
@Scope("prototype")
    static class PrototypeBean{
        private int count = 0 ;

        public void addCount() {
            count++;
        }
        public int getCount() {
            return count;
        }
        @PostConstruct
        void init() {
            System.out.println("PrototypeBean.init" + this);
        }
        @PreDestroy
        void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}
스프링 컨테이너에 프로토 타입 직접 요청 시 결과
스프링 컨테이너에 프로토 타입 직접 요청 시 결과

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

  • clientBean은 싱글톤이므로 스프링 컨테이너 생성 시점에 함께 생성되고 의존관계 주입도 발생한다.
  • clientBean은 자동 의존관계주입을 사용하기에 주입 시점에 프로토타입 빈을 스프링 컨테이너에게 요청한다.
  • 스프링 컨테이너는 해당 요청을 받으면 프로토타입 빈을 생성해서 반환한다.이때 프로토타입빈의 count값은 0이다.
  • 이제 clientBean은 프로토타입빈을 내부 필드에서 관리한다.
  • 클라이언트 A는 clientBean을 스프링 컨테이너에 요청해서 받는다.해당 스프링 빈은 싱글톤 스코프라서 항상 같은 clientBean이 반환된다.
  • A는 clientBean.logic()을 호출한다.
  • clientBean은 프로토타입빈의 addCount() 메서드를 호출해서 프로토타입 빈의 count를 증가시킨다.count의 값은 1이된다.
  • 클라이언트 B는 clientBean을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean이 반환된다.
  • 이 과정에서 중요한 포인트가 있는데 B가 요청해서 전달받는 clientBean은 이전에 이미 생성되어서 의존관계 주입까지 끝난 싱글톤 패턴의 스프링 링 빈이라는 점이다.이는 프로토타입 빈의 주입은 싱글톤 빈의 생성단계에서 진행되고 이후에 해당 clientBean은 동일한 프로토타입 빈에 대한 의존관계를 유지한다.
  • 즉, A가 요청했을때 생성된 clientBean이 그대로 반환되는데 이때 의존관계 또한 고정된채로 동일한 프로토타입 빈이 반환된다는 것이다.
  • B가 clientBean.logic()을 호출한다.
  • clientBean은 prototypeBean의 addCount()를 호출하며 count값은 1에서 2로 증가한다.

<테스트 코드>

 public class SingletonWithPrototypeTest1 {
	//...
    @Test
    void singletonClientUsePrototype() {
        AnnotationConfigApplicationContext ac = new
AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        assertThat(count1).isEqualTo(1);
        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        assertThat(count2).isEqualTo(2);
    }
    static class ClientBean {
        private final PrototypeBean prototypeBean;
        @Autowired
        public 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
        void init() {
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy
        void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}
  • 해당 테스트의 결과는 동일한 prototypeBean 두개를 출력한다.
  • 즉 프로토타입 빈은 클라이언트가 요청할 때마다 서로 다른 객체가 의존관계 주입되기는 원하는데 지금 상황은 같은 객체를 의존성 주입하는 것이다.
  • 이제부터 해당 문제를 해결하기위한 방법을 소개하겠다.

Provider로 문제 해결하기

현재 의존성을 주입하는것(DI) 때문에 위와 같은 문제가 발생하는데 이를 주입받는것이 아니라 직접 필요한 객체를 찾는 행위를 통해 의존성을 연결시켜야 한다.그러기 위해서는 Dependancy-LookUp(DL)이라는 기능을 수행해야한다.이를 수행하는 객체가 두가지 있는데 다음과 같다.

ObjectProvider 사용하기

@Autowired
  private ObjectProvider<PrototypeBean> prototypeBeanProvider;
  public int logic() {
      PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}
  • 기존 코드를 위와 같이 변경하고 테스트를 실행해보면 prototypeBeanProvider.getObject()를 통해서 항상 새로운 타입의 프로토타입 빈이 생성되는 것을 알 수 있다.
  • getObject()메서드는 내부에서 스프링 컨테이너를 통해 필요한 해당 빈(객체)를 찾아내서 반환한다.(DL)

JSR-330 Provider 사용하기

  • 먼저 build.gradle에 implementation'javax.inject:javax.inject:1'를 추가한다.
  • 이후 아래와 같이 코드를 변경한다.
@Autowired
private Provider<PrototypeBean> provider;
  public int logic() {
      PrototypeBean prototypeBean = provider.get();
      prototypeBean.addCount();
      int count = prototypeBean.getCount();
      return count;
}
  • 실행해보면 provicde.get()을 통해서 항상 새로운 타입의 프로토타입 빈이 생성되는 것을 확인 할 수 있다.

정리

  • 그러면 우리는 언제 프로토타입 빈을 사용해야하는 것일까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할때 사용하면 된다.그런데 실무에선 싱글톤 빈 스코프가 대부분의 문제를 해결하기에 프로토타입 빈을 사용하는 일은 드물다고 본다.
  • Provider는 프로토타입 빈을 사용할때 뿐만 아니라 DL이 필요한 모든 곳에서 사용 가능하다.

소스코드 : https://github.com/brido4125/core-spring

해당 글은 김영한님의 <스프링 핵심> 강좌를 기반으로 작성되었습니다.