본문 바로가기
  • 개발공부 및 일상적인 내용을 작성하는 블로그 입니다.
Spring basic

스프링 핵심원리 : 기본편 - 프로토타입 스코프와 싱글톤

by 방구석 대학생 2022. 2. 19.

"인프런의 스프링 핵심원리 - 기본편 강의를 듣고 작성한 글 입니다."

https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-%ED%95%B5%EC%8B%AC-%EC%9B%90%EB%A6%AC-%EA%B8%B0%EB%B3%B8%ED%8E%B8/dashboard

 

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

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

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 이 된다.

 

코드로 확인해보자.

test 폴더 아래 hello.core.scope 패키지에 SingletonWithPrototypeTest1 클래스를 만들고 아래와 같이 코드를 작성해보자.

- SingletonWithPrototypeTest1.java

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

        // 프로토타입 빈 이기 때문에 어차피 이 메소드는 호출되지 않는다.
        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}

결과 : 테스트 통과

PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@25df00a0
PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@16c069df

 

위의 결과를 보면 프로토타입으로 스프링 빈 객체를 생성했을 때, 매 요청마다 서로 다른 인스턴스를 생성하고 있는 것을 알 수 있다.

 

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

이번에는 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가 된다.

 

코드는 다음과 같다.

- SingletonWithPrototypeTest1.java

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);
    }

    @Test
    void singletonClientUsePrototype(){

        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.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);
    }

    @Scope("singleton")
    @RequiredArgsConstructor
    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
        public void init(){
            System.out.println("PrototypeBean.init " + this);
        }

        // 프로토타입 빈 이기 때문에 어차피 이 메소드는 호출되지 않는다.
        @PreDestroy
        public void destroy(){
            System.out.println("PrototypeBean.destroy");
        }
    }
}

위의 코드에서 singletonClientUsePrototype() 테스트를 실행시켜 보면 테스트가 정상적으로 통과되는 것을 확인 할 수 있다.

 

스프링은 일반적으로 싱글톤 빈을 사용하므로, 싱글톤 빈이 프로토타입 빈을 사용하게 된다. 그런데 싱글톤 빈은 생성 시점에만 의존관계 주입을 받기 때문에, 프로토타입 빈이 새로 생성되기는 하지만, 싱글톤 빈과 함께 계속 유지되는 것이 문제다.

 

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

+ 참고 : 여러 빈에서 같은 프로토타입 빈을 주입 받으면, 주입 받는 시점에 각각 새로운 프로토타입 빈이 생성된다.

예를 들어서 clientA, clientB 가 각각 의존관계 주입을 받으면 각각 다른 인스턴스의 프로토타입 빈을 주입받는다.

clientA -> prototypeBean@x01
clientB -> prototypeBean@x02

 

 

프로토타입 스코프 - 싱글톤 빈과 함께 사용 시 Provider 로 문제 해결

싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?

 

* 스프링 컨테이너에 요청

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.

- PrototypeProviderTest.java

public class PrototypeProviderTest {
 
    @Test
    void providerTest() {
        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);
    }
 
    static class ClientBean {
        
        @Autowired
        private ApplicationContext ac;
        
        public int logic() {
            PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
            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
    public void init() {
        System.out.println("PrototypeBean.init " + this);
    }
    @PreDestroy
        public void destroy() {
            System.out.println("PrototypeBean.destroy");
        }
    }
}

- 실행해보면 ac.getBean() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

- 의존관계를 외부에서 주입(DI) 받는게 아니라, 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency LookUp(DL) 의존관계 조회(탐색) 이라고 한다.

- 그런데 이렇게 스프링의 어플리케이션 컨텍스트 전체를 주입받게 되면, 스프링 컨테이너에 종속적인 코드가 되고, 단위 테스트도 어려워진다.

- 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는, 딱 DL 정도의 기능만 제공하는 무언가가 있으면 된다.

 

스프링에는 이미 모든게 준비되어 있다.

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너 에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.

참고로 과거에는 ObjectFactory 가 있었는데, 여기에 편의 기능을 추가해서 ObjectProvider 가 만들어졌다.

@Test
void singletonClientUsePrototype(){

    AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class, ClientBean.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);
}

@Scope("singleton")
@RequiredArgsConstructor
static class ClientBean{

    // 싱글톤 빈 객체가 프로토타입의 빈 객체를 의존성 주입 받는다.
//    private final PrototypeBean prototypeBean;

    // 일단은 필드 주입으로 선언하자.
    // 실제로는 가능하다면 생성자 주입으로 해주자.
    @Autowired
    private ObjectProvider<PrototypeBean> prototypeBeanProvider;

    public int logic(){
        // 호출될 때마다 getObject() 에 의해 새로운 프로토타입 빈이 생성된다.
        PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
        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
    public void init(){
        System.out.println("PrototypeBean.init " + this);
    }

    // 프로토타입 빈 이기 때문에 어차피 이 메소드는 호출되지 않는다.
    @PreDestroy
    public void destroy(){
        System.out.println("PrototypeBean.destroy");
    }
}

결과 : 테스트 통과

PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@37313c65
PrototypeBean.init hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@7354b8c5

 

- 실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

- ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)

- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.

- ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.

 

* 특징

- ObjectFactory : 기능이 단순하고 별도의 라이브러리가 필요 없다. 스프링에 의존한다.

- ObjectProvider : ObjectFactory 상속, 옵션, 스트림 처리 등의 편의 기능이 많고, 별도의 라이브러리가 필요 없다. 스프링에 의존한다.

 

 

JSR-330 Provider

마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.

이 방법을 사용하려면 javax.inject:javax.inject:1 라이브러리를 gradle 에 추가해야 한다.

- build.gradle

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter'
            // JSR-330 의존성 추가
	implementation 'javax.inject:javax.inject:1'

	testImplementation 'org.springframework.boot:spring-boot-starter-test'

	// lombok 라이브러리 추가 시작
	compileOnly 'org.projectlombok:lombok'
	annotationProcessor 'org.projectlombok:lombok'
	
	testCompileOnly 'org.projectlombok:lombok'
	testAnnotationProcessor 'org.projectlombok:lombok'
	// lombok 라이브러리 추가 끝
}

- javax.inject.Provider 참고용 코드

package javax.inject;

public interface Provider<T> {
    T get();
}

 

- JSR-330 Provider 를 활용한 코드는 다음과 같다.

// 클래스 import 에 주의하자
@Autowired
private Provider<PrototypeBean> provider;

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

- 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.

- provider 의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)

- 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워진다.

- Provider 는 지금 딱 필요한 DL 정도의 기능만 제공한다.

 

* get() 메소드 하나로 기능이 매우 단순하다.

- 별도의 라이브러리가 필요하다.

- 자바 표준이므로 스프링이 아닌 다른 컨테이너 에서도 사용할 수 있다.

 

* 정리

- 그러면 프로토타입 빈을 언제 사용할까? 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다.

- 그런데 실무에서 웹 어플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.

- ObjectProvider, JSR330 Provider 등은 프로토타입 뿐만 아니라 DL 이 필요한 경우는 언제든지 사용할 수 있다.

 

+ 참고 : 스프링이 제공하는 메소드에 @LookUp 어노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고, 고려해야할 내용도 많아서 생략한다.

 

+ 참고 : 실무에서 자바 표준의 JSR-330 Provider 를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider 를 사용할 것인지 고민이 될 것이다.

ObjectProvider 는 DL 을 위한 편의 기능을 많이 제공해주고, 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다.

만약 (그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너 에서도 사용할 수 있어야 한다면 JSR-330 Provider 를 사용해야 한다.

 

스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠 때가 있다.

대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면 스프링이 제공하는 기능을 사용하면 된다.