(인프런) 김영한님의 스프링 핵심 원리-기본편을 공부하고 리뷰한 글입니다.
4. 프로토타입 스코프(싱글톤 빈과 함께 사용시 Provider로 문제 해결)
싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때마다 항상 새로운 프로토타입 빈을 생성하는 방법 3가지에 대해 공부한다.
1. 스프링 컨테이너에 요청
가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 새로 요청하는 것이다.
매우 비효율적인 방법이다.
1) providerTest() 코드 작성 및 ClientBean 코드 수정
package hello.core.scope;
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.AssertionsForClassTypes.assertThat;
public class SingletonWithPrototypeTest1 {
// 1. 프로토타입 빈 사용할 때마다 스프링 컨테이너에 요청
@Test
void providerTest() {
// ClientBean, PrototypeBean 빈 등록
AnnotationConfigApplicationContext ac = new AnnotationConfigApplicationContext(ClientBean.class, PrototypeBean.class);
// 클라이언트 요청1
ClientBean clientBean1 = ac.getBean(ClientBean.class);
int count1 = clientBean1.logic();
assertThat(count1).isEqualTo(1);
// 클라이언트 요청2
ClientBean clientBean2 = ac.getBean(ClientBean.class);
int count2 = clientBean2.logic();
assertThat(count2).isEqualTo(1);
}
// 싱글톤 빈
@Scope("singleton") // 생략O
static class ClientBean {
//private final PrototypeBean prototypeBean; // ClientBean 생성 시점에 주입
@Autowired
private ApplicationContext ac; // 스프링 컨테이너 찾기(부가기능)
public int logic() {
// logic()호출 할 때마다 PrototypeBean 생성
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount(); // 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");
}
}
}
2) 핵심 코드
@Autowired
private ApplicationContext ac; // ApplicaionContext 주입
public int logic() {
// logic()을 호출할 때마다 prototypeBean 생성
PrototypeBean prototypeBean = ac.getBean(PrototypeBean.class);
prototypeBean.addCount(); // prototypeBean 의 addCount() 호출
int count = prototypeBean.getCount();
return count; // 카운트 반환
}
스프링의 ApplicationContext 전체를 주입받으면, 스프링 컨테이너에 종속적인 코드가 되고 단위 테스트도 어려워진다.
→ 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 DL 정도의 기능만 제공하는 무언가다.
● Dependency Lookup(DL) = 의존관계 조회/탐색
- 의존관계를 외부에서 주입(DI)받는게 아니라, 직접 필요한 의존관계를 찾는 것
3) providerTest() 실행 결과
클라이언트가 요청을 할 때마다 getBean()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
2. ObjectFactory, ObjectProvider
지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공하는 것이 바로 ObjectProvider 이다.
1) 핵심코드
@Scope("singleton") // 생략O
static class ClientBean {
// 필드 주입
@Autowired
private ObjectProvider<PrototypeBean> prototypeBeanProvider;
public int logic() {
// logic() 호출할 때마다 프로토타입 빈을 대신 조회해서 반환(DL)
PrototypeBean prototypeBean = prototypeBeanProvider.getObject();
prototypeBean.addCount(); // prototypeBean 의 addCount() 호출
int count = prototypeBean.getCount();
return count; // 카운트 반환
}
}
- 여기서는 단순 테스트 용도로 간단히 필드 주입을 사용했다.
- ObjectProvider의 getObject()를 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈(PrototypeBean)을 찾아 반환한다. (DL)
- 스프링이 제공하는 기능을 사용하지만, 기능이 단순하므로 단위 테스트를 만들거나 mock 코드를 만들기가 훨씬 쉬워진다.
2) providerTest() 실행 결과
클라이언트가 요청을 할 때마다 prototypeBeanProvider.getObject()를 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
3) 특징
▶ ObjectFactory
- 단순한 기능
- 별도의 라이브러리 필요X
- 스프링에 의존
▶ ObjectProvider
- 지정한 빈을 컨테이너에서 대신 찾아주는 DL 서비스를 제공
- ObjectFacotry 상속: 과거 ObjectFactory에 편의 기능(옵션, 스트림 처리 등)을 추가해 만든 것
- 별도의 라이브러리 필요X
- 스프링에 의존(스프링이 제공하는 기능)
3. JSR-330 Provider
마지막 방법은 javax.inject.Provider 라는 JSR-330 자바 표준을 사용하는 방법이다.
이 방법을 사용하기 위해 javax.inect:javax.inject:1 라이브러리를 gradle 에 추가해야 한다.
<javax.inject.Provider 참고용 코드>
1) 핵심 코드
build.gradle에 javax.inject:javax.inject:1 라이브러리를 추가하고 reload를 한다.
@Scope("singleton") // 생략O
static class ClientBean {
// javax.inject
@Autowired
private Provider<PrototypeBean> prototypeBeanProvider;
public int logic() {
// logic() 호출할 때마다 프로토타입 빈을 대신 조회해서 반환(DL)
PrototypeBean prototypeBean = prototypeBeanProvider.get();
prototypeBean.addCount(); // prototypeBean 의 addCount() 호출
int count = prototypeBean.getCount();
return count; // 카운트 반환
}
}
- Provider의 get()을 호출하면 내부에서 스프링 컨테이너를 통해 해당 빈을 찾아 반환한다. (DL)
- 자바 표준이고 기능이 단순해 단위 테스트를 만들거나 mock 코드를 만들기가 훨씬 쉬워진다.
2) 실행 결과
클라이언트가 요청을 할 때마다 provider.get()을 통해 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있다.
3) 특징
- get() 메서드 하나로 기능이 매우 단순
- 별도의 라이브러리 필요(javax.inject:javax.inject:1)
- 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용O
<정리>
- 매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체가 필요하면 프로토타입 빈을 사용한다.
그런데 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물다.
- ObjectProvider, JSR330 Provider는 프로토타입 뿐 아니라 DL이 필요한 경우 언제든지 사용할 수 있다.
(참고) 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 있지만, 이전 방법들로 충분하고, 고려해야할 내용도 많아서 생략한다.
(참고)
Q. 실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider를 사용할 것인가?
- ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고 스프링 외에 별도의 의존관계 추가가 필요 없기 때문에 편리하다.
- 만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌 다른 컨테이너에서도 사용할 수 있어야 한다면 JSR-330 Provider를 사용해야한다.
Q. 스프링이 제공하는 기능 vs 자바 표준 어떤 것을 사용해야 할까?
스프링을 사용하다 보면 자바 표준과 스프링이 제공하는 기능이 겹칠때가 많이 있다.
- 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 특별히 다른 컨테이너를 사용할 일이 없다면 스프링이 제공하는 기능을 사용하면 된다.
- 기능이 거의 비슷해서 스프링에서 표준을 권장하는 경우, 자바 표준을 사용한다.
9-4. 프로토타입 스코프(싱글톤 빈과 함께 사용시 문제점) 질문 정리
Q. 아래 코드처럼 ApplicationContext를 @Autowired로 주입받는 것이 가능한 이유는?
@Autowired ApplicationContext ac;
A. @Autowired는 스프링 빈 뿐 아니라 스프링 컨테이너도 찾아주는 기능이 있다.
ApplicationContext(스프링 컨테이너)는 스프링 빈을 관리하기 때문에 컨테이너 자체가 스프링 빈으로 등록되는 것은 아니다. 이는 @Autowried의 부가 기능으로 가능한 것이다.
<@Autowired 기능>
1) 스프링 빈을 찾아 의존관계 주입하는 기능
2) ApplicationContext 같은 것도 편리하게 찾을 수 있는 부가 기능
그래서 아래 코드는 오류가 나지만 위 코드는 성공한다.
ApplicationContext bean = ac.getBean(ApplicationContext.class); // 오류
Q. logic() 메서드를 호출할 때마다 프로토타입 빈이 생성되도록 하기 위해 provider.get() 메서드를 호출하는 것과 new PrototypeBean()을 호출하는 것의 차이는?
A. 개발 시점에 둘 중 더 적절한 방법을 사용하면 된다.
1) provider.get()
- 스프링 컨테이너를 통해 해당 빈을 조회하기 때문에 필요한 의존관계 주입, 초기화의 도움을 받을 수 있다.
2) new PrototypeBean()
- 의존관계 주입X, 필요한 초기화X, 모든 것을 직접 수동으로 해주어야 한다.
[출처] https://www.inflearn.com/questions/91531
Q. prototypeBean은 같은 PrototypeBean을 참고하고 있는가?
clientBean은 싱글톤 빈이기 때문에, clientBean1과 clientBean2는 모두 같은 인스턴스를 참조하고 있다.
따라서 PrototypeBean도 같은 PrototypeBean을 참조하고 있다.
다만 Provider.get()을 호출할 때마다 계속 새로운 PrototypeBean이 생성되는 것이다.
[출처] https://www.inflearn.com/questions/142779
Q. ApplicationContext에 비해 ObjectProvider가 기능이 단순하므로 단위테스를 만들거나 mock 코드를 만들기가 훨씬 쉬워지는 이유는?
테스트에서 가짜 구현체를 만드는 경우가 있는데, 가짜 구현체는 모든 기능이 동작하지 않아도 된다. 즉, 테스트를 실행하는데 필요한 정도만 동작하면 된다.
1) ApplicationContext를 mock으로 만들려면 그곳에 있는 수 많은 인터페이스를 모두 구현해야한다.
2) ObjectProivder를 mock으로 만들려면 그곳에서 제공하는 매우 적은 인터페이스만 구현하면 된다.
[출처] https://www.inflearn.com/questions/216798
Q. ObjectProvider 를 스프링 컨테이너에 빈으로 등록하지 않고도 ObjectProvider를 @Autowired 로 의존관계 주입을 받을 수 있는 이유는?
스프링 컨테이너에 등록된 빈이어야 @Autowired로 의존관계 주입을 할 수 있는 것이 맞다.
하지만 ObjectProvider를 사용하면 별도의 빈을 등록하지 않아도 스프링이 자동으로 처리해주기 때문에 문제가 되지 않는다.
'Spring > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
[스프링 핵심 원리] 09. 빈 스코프 - 스코프와 Provider (0) | 2022.05.24 |
---|---|
[스프링 핵심 원리] 09. 빈 스코프 - 웹 스코프 & request 스코프 예제 만들기 (0) | 2022.05.24 |
[스프링 핵심 원리] 09. 빈 스코프 - 프로토타입 스코프(싱글톤 빈과 함께 사용시 문제점) (0) | 2022.05.23 |
[스프링 핵심 원리] 9. 빈 스코프 - 빈 스코프란? & 프로토타입 스코프 (0) | 2022.05.23 |
[스프링 핵심 원리] 08. 빈 생명주기 콜백 - 애노테이션 @PostConstruct, @PreDestroy (0) | 2022.05.20 |