개발자 미니민의 개발스터디

[Spring 핵심원리 기본편] 빈 스코프

by mini_min


빈 스코프

빈 스코프는 빈이 존재할 수 있는 범위를 뜻한다. 스프링은 다양한 스코프를 지원해준다. 

싱글톤 : 기본 스코프, 스프링 컨테이너가 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
프로토타입 : 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.

 

웹 관련 스코프도 있다.

- request : 웹 요청이 들어오고 나갈 때 까지 유지되는 스코프
- session : 웹 세션이 생성되고 종료될 때 까지 유지되는 스코프
- application : 웹의 서블릿 컨텍스트와 같은 범위로 유지되는 스코프

 

 

프로토타입 스코프 

싱글톤 스코프의 빈을 조회하면 항상 같은 인스턴스의 스프링 빈을 반환한다. 반면 프로토타입 스코프는 조회할 때 마다 새로운 인스턴스를 생성해서 반환한다. 

 

- 싱글톤 빈 요청
3번 요청해도 100번 요청해도 똑같은 스프링 빈을 반환한다. (공유)

package hello.core.scope;

import org.junit.jupiter.api.Test;
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.assertThat;

public class SingletonTest {

    @Test
    void singletonBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(SingletonBean.class);

        SingletonBean singletonBean1 = ac.getBean(SingletonBean.class);
        SingletonBean singletonBean2 = ac.getBean(SingletonBean.class);

        System.out.println("singletonBean1 = " + singletonBean1);
        System.out.println("singletonBean2 = " + singletonBean2);

        assertThat(singletonBean1).isEqualTo(singletonBean2);

        ac.close();

    }

    @Scope("singleton")
    static class SingletonBean{
        @PostConstruct
        public void init(){
            System.out.println("singletonBean.init");
        }

        @PreDestroy
        public void destroy(){
            System.out.println("singletonBean.destroy");
        }

    }
}
singletonBean.init
singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@1d8bd0de
singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@1d8bd0de
16:36:29.087 [main] DEBUG org.springframework.context.annotation.AnnotationConfigApplicationContext - Closing org.springframework.context.annotation.AnnotationConfigApplicationContext@41ee392b, started on Mon May 29 16:36:28 KST 2023 singletonBean.destroy 

 

- 프로토타입 빈 요청1
프로토타입 스코프의 빈을 스프링 컨테이너에 요청하면, 스프링 컨테이너는 이 시점에 프로토타입 빈을 생성하고 필요한 의존관계를 주입한다. 

- 프로토타입 빈 요청2
생성한 프로토타입 빈을 클라이언트한테 던져주고, 관리 xxx
같은 요청이 와도 항상 새로운 프로토타입 빈을 생성해서 반환한다.

package hello.core.scope;

import org.junit.jupiter.api.Test;
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.assertThat;

public class PrototypeTest {

    @Test
    void prototypeBeanFind(){
        AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class);
        System.out.println("find prototypeBean1");
        PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class);

        System.out.println("find prototypeBean2");
        PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class);

        System.out.println("prototypeBean1 = " + prototypeBean1);
        System.out.println("prototypeBean2 = " + prototypeBean2);

        assertThat(prototypeBean1).isNotSameAs(prototypeBean2);

        ac.close();
    }

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

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

    }

}
find prototypeBean1
PrototypeBean.init
find prototypeBean2
PrototypeBean.init
prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@1d8bd0de
prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@45ca843

destroy 는 호출되지 않음

 

정리

핵심은 스프링 컨테이너는 프로토타입 빈을 생성, 의존관계 주입, 초기화까지만 관리한다는 것이다. 프로토타입 빈을 관리할 책임은 프로토타입 빈을 받은 클라이언트에게 있다. **그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다. 

 

프로토타입 빈의 특징 정리
스프링 컨테이너에 요청할 때 마다 새로 생성된다.
스프링 컨테이너는 클라이언트가 관리해야한다. 종료 메서드에 대한 호출도 클라이언트가 직접 해야한다. 

 


싱글톤 빈과 함께 사용시 문제점 

1. 클라이언트가 프로토타입 빈을 요청한다. 
2. 새로운 프로토타입 빈을 생성해서 반환한다. 
3. 다른 클라이언트가 프로토타입 빈을 요청한다. 
4. 새로운 프로토타입 빈을 생성해서 반환한다. 

 

 

clientBean 내부에 프로토타입 빈을 의존관계로 가져와서 사용한다면?

clientBean 은 싱글톤이므로, 보통 생성과 의존관계 주입이 함께 발생한다.
1. 의존관계 자동 주입을 사용하며, 주입 시점에 프로토타입 빈을 요청한다.
2. 스프링 컨테이너는 프로토타입 빈을 생성하고 관리하지 않기에 clientBean 에 반환한다. 프로토타입 빈의 count 필드 값은 0이다. 
3. 이제 clientBean 이 프로토타입 빈을 내부 필드에 보관한다. 

4. A 라는 사람은 clientBean 을 요청해서 받는다. clientBean은 싱글톤이므로 같은 clientBean 이 반환된다. 클라이언트 A 가 clientBean.logic() 을 호출한다.
-> clientBean 은 프로토타입의 addCount() 를 호출해서 프로토타입 빈의 count 를 증가한다. count 값이 1이 된다. 

5. B가 clientBean 을 요청해서 받는다. 싱글톤이므로 같은 clientBean 이 반환된다. 여기서 중요한 점 !!!! ✨✨✨ clientBean 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 사용할 때마다 새로 생성되는 것이 아니다! 그래서 프로토타입 빈의 count 는 기존 1++ 해서 2가 된다. 

(프로토타입 빈이라서 새로 생성되는 것을 기대했건만)

package hello.core.scope;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.ObjectProvider;
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;

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

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

    }

    @Scope("singleton")
    static class ClientBean {
        private final PrototypeBean prototypeBean;

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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





    }

}

 

(  싱글톤 clientBean  (  prototype Bean  )    ) 

= 우리의 의도는 프로토타입 빈을 호출할 때 마다 항상 새롭게 만들고 싶은거라면 ...?

 


싱글톤 빈과 함께 사용시 Provider 로 문제 해결

의존관계를 외부에서 주입 받는 것이 아니라, 직접 필요한 의존관계를 찾는 것을 DL 의존관계 조회(탐색)라고 한다. 
그런데, 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 정도의 기능을 스프링에서 제공해준다. 

    @Scope("singleton")
    static class ClientBean {

        @Autowired
        private ObjectProvider<PrototypeBean> prototypeBeanProvider;

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

    }

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다. 참고로 ObjectFactory 에 편의 기능이 추가된 것이 ObjectProvider 이다. 

 

실행해보면 prototypeBeanProvider.getObject() 를 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다. 
ObjectProvider 는 지금 딱 필요한 DL 정도의 기능만 제공한다. 

 

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

 


JSR-330 Provider

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

 

특징

- get() 메서드 하나로 기능이 매우 단순하다. 
- 별도의 라이브러리가 필요하다.
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다. 

 

정리

매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요할 때 해당 기능을 사용하면 되지만, 실무에서 대부분의 문제는 싱글톤으로 해결되기에 사용할 일이 많지는 않을 것 이다.

ObjectProvider 와 JSR-330 중에서 무엇을 사용해야할지 고민되는 상황에서 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면, JSR-330을 사용하는 것이 바람직하다. 
만약 특별히 다른 컨테이너를 사용할 일이 없다면, 스프링이 제공하는 기능을 사용하면 된다. 

 

 

 

블로그의 정보

개발자 미니민의 개발로그

mini_min

활동하기