김찬진의 개발 블로그

[23/04/25] singleton scope bean객체가 prototype scope bean을 의존할 때 발생하는 문제점과 그에 대한 해결방안(ApplicationContext, ObjectProvider<>, JSR330 Provider) 본문

1일1배움/Spring (김영한 님)

[23/04/25] singleton scope bean객체가 prototype scope bean을 의존할 때 발생하는 문제점과 그에 대한 해결방안(ApplicationContext, ObjectProvider<>, JSR330 Provider)

kim chan jin 2023. 4. 25. 13:04

상황1: ClientBean이 PrototypeBean에 의존

public class SingletonWithPrototypeTest1 {

    @Test public void prototypeFind() { PrototypeBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("prototype") static class PrototypeBean{ Prototype 객체는 의존하지 않음 }

 

    @Test public void singletonClientUserPrototype() { PrototypeBean, ClientBean 읽고 컨테이너 객체 생성, 1, 2 }

    @Scope("singleton") static class ClientBean { ClientBean 객체는 PrototypeBean을 의존함 }

}

핵심

ClientBean 객체가 컨테이너에 등록될 때 PrototypeBean에 의존한 채로 등록되는데,

이 때 PrototypeBean은 ClientBean 객체의 필드로 존재하므로

PrototypeBean은 prototype scope bean임에도 불구하고 singleton scope bean처럼 작동한다.

package springBasic.demo.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

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

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    public void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        // 설정정보를 읽고 prototype scope bean객체를 컨테이너에 등록할 때에는 bean객체가 생성되지 않음

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

        ac.close(); // bean객체 사용 종료 시점, 하지만 destroy 메서드 호출안됨
    }

    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct // bean객체 초기화 시점을 알리고, bean객체 초기화된 이후에 메서드(init)가 호출되는 것을 가능하게 하는 세번째 방법
        public void init() {
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy // bean객체 사용 종료 시점을 알리고, 컨테이너를 닫을 때(close) 메서드(destroy)가 호출되는 것을 가능하게 하는 세번째 방법
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

// prototype scope bean은 객체가 조회되어야 할 때마다 매번 객체로 생성된다

    @Test
    void singletonClientUserPrototype() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
        // ClientBean 클래스 설정정보를 읽고 clientBean bean객체(singleton scope bean객체)를 컨테이너에 등록하기 위해 clientBean bean객체가 생성됨
        // clientBean bean객체(singleton scope bean객체)는 prototypeBean bean객체(prototype scope bean객체)가 의존관계 주입되어야하므로 prototypebean bean객체가 생성됨
        // singleton scope bean객체가 container에 한번 등록될 때 prototype scope bean객체까지 휩쓸려서 prototype이 singleton이 되어버린다!

        ClientBean clientBean1 = ac.getBean(ClientBean.class); // singleton scope bean객체 조회
        int count1 = clientBean1.logic();
        // clientBean1 bean객체의 prototypeBean bean객체필드의 count필드를 +1
        // clientBean1 bean객체는 singleton scope bean객체이므로 clientBean1 bean객체는 container에 도로 등록된다.
        // 이 때 prototype scope bean객체의 count 필드는 1인 상태이다.
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class); // singleton scope bean객체 조회
        int count2 = clientBean2.logic();
        // clientBean2 bean객체의 prototypeBean bean객체필드의 count필드가 1이었으므로 여기에 logic() 메서드를 호출하면
        // clientBean2 bean객체의 prototypeBean bean객체필드의 count필드는 1+1 하여 2가 된다.
        // clientBean2 bean객체는 singleton scope bean객체이므로 clientBean1 bean객체는 container에 도로 등록된다.
        // 이 때 prototype scope bean객체의 count 필드는 2인 상태이다.
        assertThat(count2).isEqualTo(2);
        
        ac.close(); // bean객체 사용 종료 시점
    }
    
    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean;

        @Autowired
        public ClientBean(PrototypeBean prototypeBean) {
            // singleton scope bean객체를 container에 등록할 때 prototype scope bean객체와의 의존관계를 형성해야하므로
            // prototype scope bean객체를 새롭게 생성해서 주입
            this.prototypeBean = prototypeBean;
        }

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

 

 

 

 

 


 

 

 

 

 

상황2: ClientBean이 ApplicationContext에 의존

public class SingletonWithPrototypeTest1 {

    @Test public void prototypeFind() { PrototypeBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("prototype") static class PrototypeBean{ Prototype 객체는 의존하지 않음 }

 

    @Test public void singletonClientUserPrototype() { PrototypeBean, ClientBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("singleton") static class ClientBean { ClientBean 객체는 ApplicationContext 객체를 의존함 }

}

핵심

상황1에서 ClientBean 객체의 필드로 PrototypeBean 객체를 둬서 prototype scope bean이 singleton scope bean처럼 다뤄졌었다.

그에 대한 해결방안으로 ClientBean 객체의 필드에 PrototypeBean 객체를 삭제하고 컨테이너 객체(ApplicationContext 객체)를 만든다.

ClientBean 객체에 컨테이너 객체가 주입되어도 상관없다.

컨테이너 객체가 singleton scope bean객체로 다뤄져도 prototype scope bean객체는 컨테이너에서 조회될 때마다 매번 새롭게 만들어질 것이기 때문이다. 

package springBasic.demo.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

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

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    public void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        // 설정정보를 읽고 prototype scope bean객체를 컨테이너에 등록할 때에는 bean객체가 생성되지 않음

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

        ac.close(); // bean객체 사용 종료 시점, 하지만 destroy 메서드 호출안됨
    }

    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct // bean객체 초기화 시점을 알리고, bean객체 초기화된 이후에 메서드(init)가 호출되는 것을 가능하게 하는 세번째 방법
        public void init() {
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy // bean객체 사용 종료 시점을 알리고, 컨테이너를 닫을 때(close) 메서드(destroy)가 호출되는 것을 가능하게 하는 세번째 방법
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

    @Test
    void singletonClientUserPrototype() {
        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(1);
        
        ac.close(); // bean객체 사용 종료 시점
    }
    
    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("singleton")
    static class ClientBean {
        // ClientBean이 컨테이너에 등록될 때 PrototypeBean과는 아무 의존관계가 없다
        // PrototypeBean은 singleton scope bean이 아니라 prototype scope bean으로 사용될 수 있다!
        
        @Autowired
        private ApplicationContext ac;

        @Autowired
        public ClientBean(ApplicationContext ac) {
            this.ac = ac;
        }

        public int logic() {
            PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
}

 

 

 

 

 


 

 

 

 

 

상황3 : ClientBean이 ObjectProvider<PrototypeBean>에 의존

public class SingletonWithPrototypeTest1 {

    @Test public void prototypeFind() { PrototypeBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("prototype") static class PrototypeBean{ Prototype 객체는 의존하지 않음 }

 

    @Test public void singletonClientUserPrototype() { PrototypeBean, ClientBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("singleton") static class ClientBean { ClientBean 객체는 ObjectProvider<PrototypeBean>객체를 의존함 }

}

핵심

상황1에서 ClientBean 객체의 필드로 PrototypeBean 객체를 둬서 prototype scope bean이 singleton scope bean처럼 다뤄졌었다.

그에 대한 해결방안으로 ClientBean 객체의 필드에 PrototypeBean 객체를 삭제하고 ObjectProvider<PrototypeBean> 객체를 만든다.

ClientBean 객체에 ObjectProvider<PrototypeBean>객체가 주입되어도 상관없다.

ObjectProvider<PrototypeBean>객체가 singleton scope bean객체로 다뤄져도 prototype scope bean객체는 컨테이너에서 조회될 때마다 매번 새롭게 만들어질 것이기 때문이다. 

package springBasic.demo.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

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

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    public void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        // 설정정보를 읽고 prototype scope bean객체를 컨테이너에 등록할 때에는 bean객체가 생성되지 않음

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

        ac.close(); // bean객체 사용 종료 시점, 하지만 destroy 메서드 호출안됨
    }

    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct // bean객체 초기화 시점을 알리고, bean객체 초기화된 이후에 메서드(init)가 호출되는 것을 가능하게 하는 세번째 방법
        public void init() {
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy // bean객체 사용 종료 시점을 알리고, 컨테이너를 닫을 때(close) 메서드(destroy)가 호출되는 것을 가능하게 하는 세번째 방법
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

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

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        // 컨테이너에서 PrototypeBean객체를 조회할 때 prototype scope bean이므로 객체를 새로 생성
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        // 컨테이너에서 PrototypeBean객체를 조회할 때 prototype scope bean이므로 객체를 새로 생성
        assertThat(count2).isEqualTo(1);
        
        ac.close(); // bean객체 사용 종료 시점
    }
    
    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider; // ObjectFactory가 상위 인터페이스
        // ApplicationContext의 수많은 기능을 다 하는 것이 아니라
        // prototype scope bean객체만 찾아주는 기능만 해줌

        @Autowired
        public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanObjectProvider) {
            this.prototypeBeanObjectProvider = prototypeBeanObjectProvider;
        }

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
}

 

 

 

 


 

 

 

 

 

상황4 : ClientBean이 Provider<PrototypeBean>에 의존

public class SingletonWithPrototypeTest1 {

    @Test public void prototypeFind() { PrototypeBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("prototype") static class PrototypeBean{ Prototype 객체는 의존하지 않음 }

 

    @Test public void singletonClientUserPrototype() { PrototypeBean, ClientBean 읽고 컨테이너 객체 생성, 1, 1 }

    @Scope("singleton") static class ClientBean { ClientBean 객체는 ObjectProvider<PrototypeBean>객체를 의존함 }

}

핵심

상황1에서 ClientBean 객체의 필드로 PrototypeBean 객체를 둬서 prototype scope bean이 singleton scope bean처럼 다뤄졌었다.

그에 대한 해결방안으로 ClientBean 객체의 필드에 PrototypeBean 객체를 삭제하고 Provider<PrototypeBean> 객체를 만든다.

ClientBean 객체에 Provider<PrototypeBean>객체가 주입되어도 상관없다.

Provider<PrototypeBean>객체가 singleton scope bean객체로 다뤄져도 prototype scope bean객체는 컨테이너에서 조회될 때마다 매번 새롭게 만들어질 것이기 때문이다. 

package springBasic.demo.scope;

import org.junit.jupiter.api.Test;
// import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Scope;

import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
import javax.inject.Provider; // build.gradle에 라이브러리 추가해놨어야 함

import static org.assertj.core.api.Assertions.*;

public class SingletonWithPrototypeTest1 {

    @Test
    public void prototypeFind() {
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        // 설정정보를 읽고 prototype scope bean객체를 컨테이너에 등록할 때에는 bean객체가 생성되지 않음

        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean1.addCount();
        assertThat(prototypeBean1.getCount()).isEqualTo(1);

        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);
        // prototype scope bean객체를 조회할 때 새로운 PrototypeBean 객체 생성
        prototypeBean2.addCount();
        assertThat(prototypeBean2.getCount()).isEqualTo(1);

        ac.close(); // bean객체 사용 종료 시점, 하지만 destroy 메서드 호출안됨
    }

    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("prototype")
    static class PrototypeBean {
        private int count = 0;

        public void addCount() {
            count++;
        }

        public int getCount() {
            return count;
        }

        @PostConstruct // bean객체 초기화 시점을 알리고, bean객체 초기화된 이후에 메서드(init)가 호출되는 것을 가능하게 하는 세번째 방법
        public void init() {
            System.out.println("PrototypeBean.init" + this);
        }

        @PreDestroy // bean객체 사용 종료 시점을 알리고, 컨테이너를 닫을 때(close) 메서드(destroy)가 호출되는 것을 가능하게 하는 세번째 방법
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }

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

        ClientBean clientBean1 = ac.getBean(ClientBean.class);
        int count1 = clientBean1.logic();
        // 컨테이너에서 PrototypeBean객체를 조회할 때 prototype scope bean이므로 객체를 새로 생성
        assertThat(count1).isEqualTo(1);

        ClientBean clientBean2 = ac.getBean(ClientBean.class);
        int count2 = clientBean2.logic();
        // 컨테이너에서 PrototypeBean객체를 조회할 때 prototype scope bean이므로 객체를 새로 생성
        assertThat(count2).isEqualTo(1);
        
        ac.close(); // bean객체 사용 종료 시점
    }
    
    // 내부 정적 클래스이므로 @Configuration 생략 가능
    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider; // ObjectFactory가 상위 인터페이스
        // ApplicationContext의 수많은 기능을 다 하는 것이 아니라
        // prototype scope bean객체만 찾아주는 기능만 해줌

        @Autowired
        public ClientBean(ObjectProvider<PrototypeBean> prototypeBeanObjectProvider) {
            this.prototypeBeanObjectProvider = prototypeBeanObjectProvider;
        }

        public int logic() {
            PrototypeBean prototypeBean = prototypeBeanObjectProvider.getObject();
            prototypeBean.addCount();
            int count = prototypeBean.getCount();
            return count;
        }
    }
}

 

 

Comments