Blog

[SpringCore] 빈 스코프 - 싱글톤과 프로토타입

Category
Author
Tags
PinOnMain
1 more property
스프링 빈은 스프링 빈 컨테이너의 시작과 함께 생성되어 종료 될 때 까지 유지되는 싱글톤 스코프로 생성된다.
Table of Content

빈 스코프 종류

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

싱글톤 스코프

싱글톤은 항상 같은 인스턴스의 스프링 빈을 반환한다. 매번 요청마다 같은 인스턴스 주소값을 가진 객체를 반환한다.
package hello.core.scope; import org.assertj.core.api.Assertions; 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; public class SingletonScopeTest { @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(); } @Scope("singleton") static class SingletonBean { @PostConstruct public void init() { System.out.println("SingletonBean.init"); } @PreDestroy public void destroy() { System.out.println("SingletonBean.destroy"); } } }
Java
복사

프로토타입 스코프

하지만 프로토타입 스코프는 요청 시점에 프로토타입 빈을 생성하고 필요한 의존관계를 주입하고 빈을 반환한 후 더이상 관리하지 않고 새로운 요청에 또 새로운 빈을 생성한다.
따라서 “스프링 컨테이너는 프로토타입 빈을 생성하고, 의존관계 주입, 초기화 까지만 처리한다. 빈을 관리하는 책임은 해당 빈을 받은 클라이언트에 있다. 따라서 @PreDestroy 같은 종료 메서드가 호출되지 않는다.
package hello.core.scope; import org.assertj.core.api.Assertions; 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; public class PrototypeScopeTest { @Test void prototypeBeanFind() { AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(PrototypeBean.class); System.out.println("PrototypeScopeTest.prototypeBeanFind 1"); PrototypeBean prototypeBean1 = ac.getBean(PrototypeBean.class); System.out.println("PrototypeScopeTest.prototypeBeanFind 2"); PrototypeBean prototypeBean2 = ac.getBean(PrototypeBean.class); System.out.println("prototypeBean1 = " + prototypeBean1); System.out.println("prototypeBean2 = " + prototypeBean2); Assertions.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"); } } }
Java
복사
프로토타입 스코프의 빈은 스프링 컨테이너에서 빈을 조회(요청) 할 때 생성되고 초기화 메서드도 실행된다.
빈을 두번 조회 했으므로 다른 스프링 빈이 생성되며 초기화도 두번 실행 된 것을 확인 할 수 있다.
또한 스프링 컨테이너가 생성, 의존관계주입, 초기화 까지 관여하고 이후 관리하지 않기 때문에 @PreDestroy 메서드(close())가 실행되지 않은 것을 볼 수 있다.
종료 메서드는 직접 해야 한다. PrototypeBean.close();

싱글톤과 프로토타입 스코프를 함께 사용하는 경우

프로토타입은 호출 시점마다 생성되는 것 같지만 다음처럼 싱글톤 내부에서 프로토타입 빈을 생성하는 경우 싱글톤 빈과 생명주기를 함께하는 것을 볼 수 있다.
package hello.core.scope; 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; 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 singgletonClientUseProtorype() { 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") 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 " + this); } } }
Java
복사
스프링은 일반적으로 싱글톤 빈을 사용하기 때문에 이렇게 작동하면 프로토타입 스코프를 이용하는 의미가 없어진다.

Provider로 위 문제 해결

의존관계를 외부에서 주입(DI)받는 것이 아니라 의존관계를 찾는 것을 Dependency Lookup(DL)이라 한다. 지금 필요한 기능은 프로토타입 빈을 컨테이너에서 찾아주는 DL 기능만 제공하는 것이 필요하다.

ObjectFactory, ObjectProvider

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 singgletonClientUseProtorype() { 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") static class ClientBean { @Autowired private ObjectProvider<PrototypeBean> prototypeBeanObjectProvider; public int logic() { PrototypeBean prototypeBean = prototypeBeanObjectProvider.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 " + this); } } }
Java
복사
ObjectProvider.getObject()를 통해서 새로운 프로토타입 빈이 생성되는 것을 확인 할 수 있다.
getObject()는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(DL)
새로 요청하기 때문에 프로토타입의 새로운 빈이 생성 되어 원하는 프로토타입 빈이 생성되는 것이 확인
스프링 프레임워크에 의존적인 점이 있다.

JSR-330 자바 표준 라이브러리 사용

위 ObjectProvider는 스프링 프레임워크에 의존적인 문제가 있다. 자바 표준에서 Provider를 제공해주는 라이브러리를 활용하면 보다 범용성있게 사용(다른 컨테이너 사용) 할 수 있다.
build.gradle에서 Provider를 사용하기 위해 라이브러리를 주입해야 한다.
//JSR-330 자바 표준 Provider 사용하기 implementation 'javax.inject:javax.inject:1'
Java
복사
이후 코드에서 ObjectProvider가 아닌 Provider 인터페이스를 직접 사용 할 수 있다. 해당 인터페이스는 get()처럼 조회하는 메서드가 포함되어 있다.
... @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; } } ...
Java
복사
실행해보면 provider.get()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인 할 수 있다.

프로토타입 빈 필요?

실제로 스프링 컨테이너의 싱글톤으로 대부분 완성하기 때문에 위처럼 요청마다 새로운 인스턴스를 생성하는 프로토타입의 필요성에 궁금증이 생긴다. 직접 사용할 상황은 많지 않지만 사용시마다 새로운 객체가 필요한 상황에 사용하면 된다.
다시 정리하면
싱글톤 : 스프링 컨테이너의 시작과 끝까지 함께하는 매우 긴 스코프
프로토타입 : 생성과 의존관계 주입, 초기화 까지 진행하는 특별한 스코프

Toggle H1

Toggle H2

Toggle H3

Search
 | Main Page | Category |  Tags | About Me | Contact | Portfolio