본문 바로가기
SpringFramework/Spring 중요 개념

[Spring] 빈 스코프 (1) - 프로토타입 스코프

by 쭈봉이 2021. 12. 23.

빈 스코프란?

스프링 빈이 기본적으로 싱글톤 스코프로 생성되기 때문에 스코프는 번역 그대로 빈이 존재할 수 있는 범위를 뜻한다.

 

스프링은 다음과 같은 다양한 스코프를 지원한다.

  • 싱글톤: 기본 스코프, 스프링 컨테이너의 시작과 종료까지 유지되는 가장 넓은 범위의 스코프이다.
  • 프로토타입: 스프링 컨테이너는 프로토타입 빈의 생성과 의존관계 주입까지만 관여하고 더는 관리하지 않는 매우 짧은 범위의 스코프이다.
  • 웹 관련 스코프
    • request: 웹 요청이 들어오고 나갈때까지 유지되는 스코프
    • session: 웹 세션이 생성되고 종료될때 까지 유지되는 스코프
    • application: 웹의 서블릿 컨텍스와 같은 범위로 유지되는 스코프

프로토 타입 스코프

싱글톤 스코프의 빈을 조회하면 스프링 컨테이너는 항상 같은 인스턴스의 스프링 빈을 반환한다.

반면에 프로토 타입 스코프를 스프링 컨테이너에 조회하면 스프링 컨테이너는 항상 새로운 인스턴스를 반환한다.

싱글톤 스코프의 빈을 스프링 컨테이너에 요청하면 스프링 컨테이너는 자신이 관리하고 있던 스프링 빈을 반환하고, 같은 요청이 와도 같은 객체 인스턴스 스프링 빈을 반환한다.

 

반면, 프로토타입 스코프빈을 스프링 컨테이너에 요청할 경우 요청받는 시점에 프로토타입 빈을 생성하고, 필요한 의존관계를 주입한다. 그리고 생성된 빈은 관리하지 않고 반환한다.

즉, 스프링 빈을 생성 및 의존관계 주입, 초기화를 해준 후 클라이언트에 반환하고 아무것도 안한다.

(그래서 @PreDestroy 같은 종료 메서드가 호출되지 않는다)

 

싱글톤 예제

@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);
        Assertions.assertThat(singletonBean1).isSameAs(singletonBean2);
        ac.close();

//        SingletonBean.init
//        singletonBean1 = hello.core.scope.SingletonTest$SingletonBean@1603cd68
//        singletonBean2 = hello.core.scope.SingletonTest$SingletonBean@1603cd68
//        SingletonBean.destroy
    }

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

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

프로토타입 예제

@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);
        Assertions.assertThat(prototypeBean1).isNotSameAs(prototypeBean2);
        ac.close();

//        find prototypeBean1
//        SingletonBean.init
//        find prototypeBean2
//        SingletonBean.init
//        prototypeBean1 = hello.core.scope.PrototypeTest$PrototypeBean@1603cd68
//        prototypeBean2 = hello.core.scope.PrototypeTest$PrototypeBean@9ebe38b

    }

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

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

출력과 같이 destroy 호출이 안된걸 볼수 있고,

Bean을 조회할때마다 매번 init 메서드가 실행되는 것으로 보아 매번 생성해주고 있는 것을 확인 할 수 있다.

destroy가 필요할 경우 prototypeBean1.destroy 메서드를 매번 호출해줘야한다.

 

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

싱글톤 빈과 함께 사용할 경우 의도한대로 잘 동작하지 않으므로 주의해야한다.

 

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

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

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

 

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

클라이언트 B는 clientBean 을 스프링 컨테이너에 요청해서 받는다.싱글톤이므로 항상 같은 clientBean 이 반환된다.
여기서 중요한 점이 있는데, clientBean이 내부에 가지고 있는 프로토타입 빈은 이미 과거에 주입이 끝난 빈이다. 

주입 시점에 스프링 컨테이너에 요청해서 프로토타입 빈이 새로 생성이 된 것이지, 사용 할 때마다 새로 생성되는 것이 아니다!
5. 클라이언트 B는 clientBean.logic() 을 호출한다.
6. clientBean 은 prototypeBean의 addCount() 를 호출해서 프로토타입 빈의 count를 증가한다.

원래 count 값이 1이었으므로 2가 된다.

 

여기서 문제는

Prototype을 사용할때마다 매번 생성해서 사용하고 싶은데 ClientBean이 싱글톤이기 때문에 매번 Prototype 빈을 호출, 및 생성하지 않고 있는걸 그대로 쓰게 된다.

 

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

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

 

ObjectFactory, ObjectProvider

지정한 빈을 컨테이너에서 대신 찾아주는 

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

//        PrototypeBean.init = hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@13330ac6
//        PrototypeBean.init = hello.core.scope.SingletonWithPrototypeTest1$PrototypeBean@7203c7ff

    }

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

getObject()를 호출하면 그때 prototypeBean을 호출하여 받아준다.

즉, ObjectProvider는 스프링 컨테이너에서 Bean을 직접 조회할 수 있는 기능을 제공하는것이다.

 

ObjectProvider는 ObjectFactory를 상속 받는다. 따라서 ObjectProvider에 기능이 더 많고 좋다

하지만 라이브러리를 추가하지 않아도 되지만 스프링에 의존한다.

 

JSR-330 Provider

JSR-330 자바 표준을 사용하는 방법

build.gradle에 라이브러리를 추가

implementation 'javax.inject:javax.inject:1'
@Scope("singleton")
static class ClientBean {
    @Autowired
    private Provider<PrototypeBean> prototypeBeanProvider;

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

javax provider를 사용할것

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

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

별도의 라이브러리가 필요하며, 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.

 

실무에서는 JSR-330 Provider를 사용할 것인지 ObjectProvider를 사용할 것인지에 대한 고민

스프링이 아닌 다른 컨테이너에서 사용하게 되면 JSR-330 Provider를 쓰지만... 왠만하면 스프링이 제공하는 기능을 쓰자

댓글