Spring/스프링 핵심 원리 - 기본편

53. 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점

DEV-HJ 2023. 8. 5. 22:31
반응형

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

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


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


프로토타입 빈 직접 요청


스프링 컨테이너에 프로토타입 빈 직접 요청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이 된다.

 

코드로 확인

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class SingoletonWithPrototypeTest1 {


    @Test
    void protoTypeFild(){

       AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(ProtoTypeBean.class);

       ProtoTypeBean protoTypeBean1 = ac.getBean(ProtoTypeBean.class);
       protoTypeBean1.addCound();
       Assertions.assertThat(protoTypeBean1.getCount()).isEqualTo(1);

       ProtoTypeBean protoTypeBean2 = ac.getBean(ProtoTypeBean.class);
       protoTypeBean2.addCound();
       Assertions.assertThat(protoTypeBean2.getCount()).isEqualTo(1);

    }

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

        public void addCound(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init "+this);
        }

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


    }


}

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


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

 

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

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

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

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

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

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

 

테스트 코드

package hello.core.scope;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;

public class SingoletonWithPrototypeTest1 {


    @Test
    void protoTypeFild(){

       AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(ProtoTypeBean.class);

       ProtoTypeBean protoTypeBean1 = ac.getBean(ProtoTypeBean.class);
       protoTypeBean1.addCound();
       Assertions.assertThat(protoTypeBean1.getCount()).isEqualTo(1);

       ProtoTypeBean protoTypeBean2 = ac.getBean(ProtoTypeBean.class);
       protoTypeBean2.addCound();
       Assertions.assertThat(protoTypeBean2.getCount()).isEqualTo(1);

    }

    @Test
    void singletonClientUsePrototype(){
        AnnotationConfigApplicationContext ac  = new AnnotationConfigApplicationContext(ProtoTypeBean.class, ClientBean.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;

        @Autowired
        public ClientBean(ProtoTypeBean protoTypeBean){
            this.protoTypeBean = protoTypeBean;
        }

        public int logic(){
            protoTypeBean.addCound();
            int count = protoTypeBean.getCount();
            return count;
        }

    }


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

        public void addCound(){
            count++;
        }

        public int getCount(){
            return count;
        }

        @PostConstruct
        public void init(){
            System.out.println("PrototypeBean.init "+this);
        }

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


    }


}

 

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다.

그런데 싱글톤 빈은 생성시점에만 의존관계 주입을 받기 때문에,

프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는것 이 문제다.


아마 원하는 것이 이런 것은 아닐 것이다.

프로토타입 빈을 주입 시점에만 새로 생성하는게 아니라, 사용할 때 마다 새로생성해서 사용하는 것을 원할 것이다.


참고:

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

반응형