-
Spring 기본 36 - 프로토타입 스코프(싱글톤 빈과 함께 사용시 Provider로 문제 해결)FrameWork/Spring&Spring-boot 2023. 11. 30. 11:26
프로토타입 스코프(싱글톤 빈과 함께 사용시 Provider로 문제 해결)
싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까?
스프링 컨테이너에 요청
가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때 마다 스프링 컨테이너에 새로 요청하는 것이다.
package com.hello.core.scope; import lombok.RequiredArgsConstructor; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; 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 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(1); } @Scope("singleton") static class ClientBean{ //생성시점에 주입 private ApplicationContext ac; public ClientBean(ApplicationContext ac, PrototypeBean prototypeBean) { this.ac = ac; } public int logic(){ PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); prototypeBean.addCount(); return prototypeBean.getCount(); } } @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"); } } }
핵심코드
@Scope("singleton") static class ClientBean{ private ApplicationContext ac; public ClientBean(ApplicationContext ac, PrototypeBean prototypeBean) { this.ac = ac; } public int logic(){ PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class); prototypeBean.addCount(); return prototypeBean.getCount(); } }
- 실행해보면 `ac.getBean()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- 의존관계를 외부에서 주입(DI)받는게 아니라 이렇게 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라고 한다.
- 그런데 이렇게 스프링 애플리테이션 컨텍스트 전체를 주입받게 되며, 스프링 컨테이너에 너무 종속적이 코드가 되고, 단위 테스트도 어려워진다.
- 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능남 제공하는 무언가가 있으면 된다.
ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 `ObjectProvider`이다. 참고로 과거에는 `ObjectFactory`가 있었는데, 여기에 편의 기능을 추가해서 `ObjectProvider`가 만들어졌다.
static class ClientBean{ @Autowired private ObjectProvider<PrototypeBean> prototypeBeanProvider; public int logic(){ PrototypeBean prototypeBean = prototypeBeanProvider.getObject(); prototypeBean.addCount(); return prototypeBean.getCount(); } }
- 실행해보면 `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에 추가 해야한다.
package javax.inject; public interface Provider<T> { T get(); }
@Scope("singleton") static class ClientBean{ @Autowired private Provider<PrototypeBean> prototypeBeanProvider; public int logic(){ PrototypeBean prototypeBean = prototypeBeanProvider.get(); prototypeBean.addCount(); return prototypeBean.getCount(); } }
- 실행해보면 `provider.get()`을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
- `provider`의 `get()`을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환한다.(*DL*)
- 자바 표준이고, 기능이 단순하므로 단위테스트를 만들거나 mock코드를 만들기는 훨씬 쉬워진다.
- `Provider`는 딱 필요한 DL 정도의 기능만 제공한다.
특징
- `get()`메서드 하나로 기능이 매우 단순하다.
- 별도의 라이브러리가 필요하다.
- 자바 표준으로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다.
정리
- 그러면 프로토타입 빈을 언제 사용할까? 매번 사용할 때 마다 의존관계 주입이 완료된 새로운 객체가 필요하면 사용하면 된다. 그런데 실무에서 웹 어플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
- 지연(lazy)해서 가져오거나 optional로 객체 인스턴스를 가져 올 때
- A가 B를 의존하고 B가A를 의존할 때 (순환참조,breaking circular dependencies)
- `ObjectProvider`,`JSR330 Provider`등은 프로토타입 뿐만 아니라 DL이 핑요한 경우는 언제든지 사용할 수 있다.
➕참고
스프링이 제공하는 메서드에`@Lookup`애노테이션을 사용하는 방법도 있지만, 고려해야 할 사항이 많다.[출처 - 스프링 핵심 원리 - 기본편]
스프링 핵심 원리 - 기본편 - 인프런 | 강의
스프링 입문자가 예제를 만들어가면서 스프링의 핵심 원리를 이해하고, 스프링 기본기를 확실히 다질 수 있습니다., 스프링 핵심 원리를 이해하고, 성장하는 백엔드 개발자가 되어보세요! 📢
www.inflearn.com
'FrameWork > Spring&Spring-boot' 카테고리의 다른 글
스프링 MVC 2 - 메시지, 국제화(소개, 스프링 메시지 소스 설정) (0) 2023.12.07 Spring 기본 37 - 웹 스코프(request스코프 예제 만들기) (1) 2023.11.30 Spring 기본 35 - 프로토타입 스코프 (싱글톤 빈과 함께 사용시 문제점) (0) 2023.11.30 Spring 기본 34 - 프로토타입 스코프 (1) 2023.11.29 Spring 기본 33 - 빈 스코프 (0) 2023.11.29