당니의 개발자 스토리
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결 본문
프로토타입 스코프 - 싱글톤 빈과 함께 사용시 Provider로 문제 해결
이번 시간에는 프로토타입 스코프를 싱글톤 빈과 함께 사용할 때 Provider 라는 걸 사용해서 문제를 해결하는 방법에 대해서 알려드리겠습니다.

싱글톤 빈과 프로토타입 빈을 함께 사용할 때, 어떻게 하면 사용할 때 마다 항상 새로운 프로토타입 빈을 생성할 수 있을까요? 이전 시간에 살짝 해봤죠.

가장 간단한 방법은 싱글톤 빈이 프로토타입을 사용할 때마다 스프링 컨테이너에 getBean()으로 새로 요청하는 거예요. 이전 시간에 코드로 작성해봤죠.


ClientBean이 @Autowired로 ApplicationContext를 주입받아서 logic()에서 prototypeBean의 addCount()를 호출할 때마다 컨테이너에서 프로토타입 빈을 새로 받으면 돼요.
이전 시간에 봤으니까 바로 설명을 해드릴게요.

실행을 해보면 ApplicationContext에서 ac.getBean()을 통해 항상 새로운 프로토타입이 생성되는 것을 확인할 수 있습니다.
그리고 의존관계를 외부에서 주입(DI) 받는게 아니라, 이렇게 내가 직접 필요한 의존관계를 찾는 것을 Dependency Lookup(DL) 의존관계 조회(탐색)이라고 합니다.
원래는 프로토타입 빈을 의존관계로 주입받았었는데, 지금은 ClientBean에서 필요한 프로토타입 빈을 스프링 컨테이너에서 getBean()으로 직접 찾고 있죠. 의존관계를 직접 찾는 겁니다(DL). 이렇게 찾기만 해도 스프링 컨테이너한테 프로토타입 빈을 달라고 요청하는 거니까, 새로 생성될 겁니다.
그런데 이렇게 스프링의 ApplicationContext 전체를 주입받게 되면, 스프링 컨테이너에 너무 종속적인 코드가 되고, 단위 테스트도 짜기 어려워져요. 단위 테스트는 스프링의 도움 없이, 순수한 java 코드로 테스트하는 건데 ApplicationContext를 주입을 받아야 되버리니깐 스프링을 꼭 띄워야만 되는 거죠.
그래서 지금 필요한 기능은 지정한 프로토타입 빈을 컨테이너에서 대신 찾아주는 딱! DL 정도의 기능만 제공하는 무언가가 있으면 돼요.
그럼 컨테이너에서 프로토타입 빈을 찾아주기만 하면 매번 새로 생성하겠죠.

스프링에는 이 고민을 해결해줄, DL을 대신 해주는 기능이 이미 준비되어 있습니다.
크게 ObjectFactory, ObjectProvider가 있는데요.

코드로 먼저 보여드리고 설명하겠습니다. SingletonWithPrototypeTest1로 가서, ClientBean2을 지우고,

ClientBean에다가 필드 prototypeBean과 생성자를 지우고, 이렇게 다른 걸 의존관계 주입으로 받을 겁니다.
그러면 이 prototypeBeanProvider가 스프링 컨테이너에서 대신 찾아주는 거예요.

getObject()로 꺼내시면 prototypeBean이 나옵니다. 우선 테스트를 돌려볼게요. 테스트가 실패 합니다. 당연히 실패해야죠.

원래 2였는데, 이젠 새로 사용할 때마다 새로 생성되는 거니까 1로 수정해야 합니다.
다시 돌려보면, 테스트가 성공합니다.

그리고 출력을 보면 프로토타입 빈이 두 개가 생성된 걸 확인할 수 있죠.

ObjectProvider가 뭐냐면, 스프링 컨테이너에서 대신 찾아서 제공해주는 건데 지금 PrototypeBean이라고 지정해두고, 이 지정한 빈을 컨테이너에서 찾아서 제공해달라고 하는 겁니다.

그래서 getObject()를 호출하면, 그때서야 스프링 컨테이너에서 PrototypeBean을 찾아서 우리한테 반환해주는 거예요.
우리가 애플리케이션 컨테스트한테서 직접 찾는 게 아니라, ObjectProvider가 찾아주는 기능만 제공해주는 거예요.
이렇게 하면 필요할 때마다 스프링 컨테이너에 요청하는 기능을 사용할 수 있죠.
다시 돌아와서,

참고로 ObjectFactory가 있고, ObjectProvider가 있는데 둘 다 똑같은 건데,

이렇게 바꿔도 잘 동작합니다.

옛날에는 ObjectFactory라는 게 있었는데, 얘도 인터페이스지만 ObjectFactory의 자식 인터페이스가

ObjectProvider 입니다. ObjectProvider는 인터페이스인데 ObjectFactory를 상속받고 있죠. ObjectProvider가 ObjectFactory 보다 더 많은 편의 기능을 제공합니다.


실행해보면 prototypeBeanProvider.getObject() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있습니다.
그런데 핵심이 프로토타입을 쓰는 게 핵심이 아니예요. 이 ObjectProvider는 스프링 컨테이너를 통해서 Dependecy Lookup, 즉 찾아주는 과정을 조금 더 간단하게 도와주는 거예요.
ObjectProvider가 프로토타입 전용으로 사용되는 게 아니라, 핵심 컨셉은 스프링 컨테이너에서 찾아서 조회하는데 내가 직접 조회 하는 것보다는 ObjectProvider를 통해서 대신 조회해주는 '대리자' 정도로 생각하시면 됩니다.
그래서 ObjectProvider 의 getObject() 를 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환합니다. 그런데 이게 프로토타입 빈이잖아요. 그러니까 스프링 컨테이너에 요청하면, 그때 새로 만들어서 반환해주겠죠. 이거를 DL이라고 합니다.
여전히 스프링이 제공하는 기능을 사용해서 스프링에 의존적이지만, 기능이 단순하므로 단위테스트를 만들거나 mock 코드를 만들기는 훨씬 쉬워져요.

물론 나중엔 생성자 주입으로 바꿔줘야겠죠.
ObjectProvider는 지금 딱 필요한 DL 정도의 기능만 제공합니다.

그런데 생각해보면, @Autowired 라고 되어있는데, 그럼 스프링 컨테이너에 ObjectProvider가 있어야된다는 소리인데, 난 스프링 컨테이너에 ObjectProvider를 빈으로 등록해준 적이 없어요.
스프링이 이런 거 정도는 자동으로 만들어서 주입해줍니다.
이렇게 해서 우리가 프로토타입 빈을 사용할 때마다 스프링 컨테이너에 요청해서 그때그때 생성을 해서 사용한다는 요구사항에 맞았습니다.
자 그래서 특징은

ObjectFactory는 기능이 단순하고 별도의 라이브러리가 필요없어요. 그런데 스프링에 의존적이죠.
ObjectProvider는 ObjectFactory를 상속받기 때문에 옵션, 스트림 처리 등 편의 기능이 많습니다. 그리고 Java 8의 Optional 같은 거로 할 수 있는 여러가지 컨셉의 기능들이 많습니다.
그런데 별도의 라이브러리가 필요없지만, 이것도 스프링에 의존적 입니다.

스프링 코드를 그대로 가져다가 쓰는 거니까요. 스프링 프레임워크에 의존해서 사용하고 있죠.
자 그래서 스프링에 의존하지 않는 새로운 기술이 나옵니다.

JSR-330 Provider이라는 건데요. JSR - xxx는 다 java 표준이라고 했죠. java에서 표준으로 정한 거예요. 이렇게 뭔가를 컨테이너에서 가져올 때, Provider 개념을 자바 진영에서 표준화한 거예요.
이렇게 자바 표준으로 정한 걸 사용하면 스프링이 아닌, 다른 컨테이너에서도 사용이 가능하죠.

그런데 단점이 gradle에 추가해줘야 돼요.

추가해준 다음에 코끼리를 눌러주면 됩니다.

여기에 들어가보면,

Provider랑 get만 있습니다.

패키지도 jakarta.inject 라고 되어있습니다. 그래서 이걸로 바꿔보겠습니다.

jakarta.inject.Provider이고, 아까 메서드가 get이었으니까

바꿔서 돌려보면,

똑같이 동작합니다. 프로토타입 빈이 두 개이고 테스트도 성공했습니다.
설명드리겠습니다.

그래서 이거를 실행해보면 provider.get() 을 통해서 항상 새로운 프로토타입 빈이 생성되는 것을 확인할 수 있어요.
provider의 get() 을 호출하면 내부에서는 스프링 컨테이너를 통해 해당 빈을 찾아서 반환합니다. DL이 일어나는 거죠.
얘가 자바 표준이에요. 그리고 기능이 단순하기 때문에 단위테스트를 만들거나 mock 코드를 만들기 편합니다.
그리고 Provider 는 지금 딱 필요한 DL 정도의 기능만 제공하죠.
이제 JSR-330 Provider의 특징을 설명드릴게요.

get() 메서드 하나로, 기능이 매우 단순합니다. 그런데 단점이 스프링을 사용하면, Java 표준이긴 한데 라이브러리를 당겨야 돼요. 그래서 우리가 gradle에 추가해줬었죠.
그런데 자바 표준이므로 스프링이 아닌 다른 컨테이너에서도 사용할 수 있다는 장점이 있습니다.
정리해볼게요.

그러면 프로토타입 빈을 언제 사용할까요?
매번 사용할 때마다 의존관계 주입이 완료된 새로운 객체 인스턴스가 필요하면 그 때 사용하면 돼요. 그런데 막상 실무에서 웹 애플리케이션을 개발해보면, 싱글톤 빈으로 대부분의 문제를 해결할 수 있기 때문에 프로토타입 빈을 직접적으로 사용하는 일은 매우 드물어요. 사실 거의 안 써요.
ObjectProvider , JSR330 Provider 등은 프로토타입 뿐만 아니라 DL이 필요한 경우에는 언제든지 사용할 수 있습니다.

표준이 좋은 이유가 여러가지가 있지만, 이 문장이 뭐냐면 Provider에 대한 개념이 지연해서 가져오거나, 옵셔널하게 가져올 때,

그리고 순환 dependency 생길 때입니다. 그러니까 개발하다보면 거의 잘 발생하지는 않지만, A가 B를 의존하고 B가 A를 의존할 때가 있거든요. 그러면 A 입장에서는 B가 필요하고, B 입장에서는 A가 필요하니까 의존관계 순환이 일어나죠.
이러한 순한 참조가 일어날 때, Provider를 쓰면 어떻게 될까요?
A는 의존관계 주입할 때 B가 당장 필요하지만, B가 A를 필요해하는 시점은 나중이잖아요. 실제 사용하는 시점은 뒤로 밀리기 때문에, Provider를 쓰면 이 순환 참조에 문제가 발생하지 않아요.

그리고 참고로 스프링이 제공하는 메서드에 @Lookup 애노테이션을 사용하는 방법도 있어요. 그런데 이거는 이전 방법들로 충분하고, 고려해야할 내용도 많아서 생략하겠습니다.

그러면 이제 이런 고민이 있겠죠. 실무에서 자바 표준인 JSR-330 Provider를 사용할 것인지, 아니면 스프링이 제공하는 ObjectProvider를 사용할 것인지 고민이 될거예요.
ObjectProvider는 DL을 위한 편의 기능을 많이 제공해주고, 스프링 외에 별도의 의존관계 추가가 필요없기 때문에 편리합니다. JSR-330 Provider처럼 외부 라이브러리를 땡겨오지 않아도 되죠.
만약(정말 그럴일은 거의 없겠지만) 코드를 스프링이 아닌, 다른 컨테이너에서도 사용할 수 있어야 한다면, JSR-330 Provider를 사용하는 게 맞습니다.
그리고 스프링을 사용하다 보면 이런 기능 뿐만 아니라, 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠 때가 많이 있거든요. '그러면 뭘쓰지..? 아잇 당연히 표준을 써야지~ 나중에 스프링 말고 다른 컨테이너도 바꿀 일이 있을지도 모르잖아' 라고 하지만, 사실은 아닙니다.
이게 JPA랑 스프링의 다른 점인데요. JPA는 표준이 승리했어요. JPA라는 표준을 쓰고, Hibernate가 처음에 인기를 끌다가 자바 진영에서 Hibernate 개발자를 데려와서 JPA 라는 Java 표준을 만들었단 말이에요.
그래서 Hibernate가 완전히 JPA 표준 구현체로 들어가버렸어요. 그래서 아예 JPA가 메인이 됐어요. 쉽게 말해서, 이 경우에는 Java 표준이 승리한 거예요.
그런데 스프링 같은 경우에는 애매해요. 스프링이 너무 잘 나가니까 스프링과 비슷하게 java 진영에서 스프링의 컨테이너 개념과 @Autowired랑 비슷한 @, Inject 등등 여러 개를 만들었어요. 그런데 이게 문제가 있어요.
사람들이 대부분 스프링을 쓰고, 표준에서 제공하는 기능이 불편한 거예요. 제약도 많고 해서 사람들이 잘 안 넘어가는 거예요.
그리고 사실 스프링을 쓰면서 사람들이 굳이 저걸 저렇게 해야 되냐 하면서 표준 쪽으로 안 넘어가요. 그래서 사실상 스프링 자체가 기술 표준이에요.
그래서 컨테이너 기술이나 이런 것들은 거의 다 스프링을 씁니다. 다른 컨테이너들을 잘 쓰지 않아요.
그래서 자바 표준과 스프링이 제공하는 기능이 겹칠때는 JPA와 관련된 건 표준을 써요.
그런데 대부분의 사람들은 Hibernate를 직접 쓰기 보다는 JPA를 가지고 구현체로 Hibernate를 선택하죠.
스프링 같은 경우엔 그냥 기능을 보고 주로 선택해요. 기능이 스프링 거가 더 편리하면, 표준 말고 스프링 거를 써요. 대표적으로 @Autowired가 있죠.
그런데 만약에 기능이 비슷해요. 그러면 스프링에서도 '그냥 표준을 쓰세요' 라고 권장하는 게 있어요. 이런 거는 스프링이 그냥 라이브러리를 처음부터 들고있는 경우도 많거든요.

대표적인 게 @PostConstruct, @PreDestroy가 있어요.

얘네도 들어가보면 java 표준이에요. 이런 경우에는 스프링에서도 권장하고 '그냥 표준 걸 쓰세요' 라고 합니다. 기능이 이걸로도 충분하면 그냥 표준을 사용합니다.
그래서 정리하면,
스프링을 사용하다 보면 이 기능 뿐만 아니라 다른 기능들도 자바 표준과 스프링이 제공하는 기능이 겹칠때가 있습니다. 대부분 스프링이 더 다양하고 편리한 기능을 제공해주기 때문에, 스프링이 '표준을 권장합니다' 라고 하는 경우를 제외하고는, 특별히 다른 컨테이너를 사용할 일이 없거나 기능상 차이가 없으면, 대부분 스프링이 제공하는 기능을 사용하면 됩니다.
그래서 잘 고민해서 JSR-330 Provider를 쓸 건지, ObjectProvider를 쓸 건지 여러분이 장단점을 비교해서 쓰면 됩니다.
'스프링 > 스프링 핵심 원리 - 기본편' 카테고리의 다른 글
| request 스코프 예제 만들기 (0) | 2024.01.28 |
|---|---|
| 웹 스코프 (0) | 2024.01.28 |
| 프로토타입 스코프 - 싱글톤 빈과 함께 사용시 문제점 (0) | 2024.01.27 |
| 프로토타입 스코프 (0) | 2024.01.27 |
| 빈 스코프란? (0) | 2024.01.27 |