— 2 min read
spring을 늘 쓰지만 자주 접하지 않는 상황에선 설정이 헷갈릴 때가 있다. 얼마 전 한 프로젝트의 bean 설정을 확인하다가 아래 코드와 같이 List타입과 List 내부에 있는 객체 타입 (편의상 List<T>
타입과 T
타입 이라고 하겠다) 의 설정이 둘 다 있는 경우를 마주했다.
1@Bean2 List<Cake> availableCakes() {3 return List.of(4 lemonCake(),5 chocolateCake(),6 strawberryCake()7 );8 }910 @Bean11 Cake chocolateCake() {12 return new Cake("초코");13 }1415 @Bean16 Cake strawberryCake() {17 return new Cake("딸기");18 }1920 @Bean21 Cake lemonCake() {22 return new Cake("레몬");23 }
List<Cake>
을 주입받아 사용하는 코드:
1@Component2class Bakery {34 private final List<Cake> availableCakes;56 Bakery(List<Cake> availableCakes) {7 this.availableCakes = availableCakes;8 }9}
(케이크가 먹고싶어서 객체이름을 이렇게 바꿔봤다..)
원래는 cake가 어떤 순서로 주입되든 상관이 없어서 이 코드는 정상적으로 동작하고 있었다. 그런데 레몬 케이크가 무조건 첫 순서가 되어야하는 요구사항이 추가되었다.
위 코드로 테스트해보니, 생각했던 것과 다르게 Bakery에 주입된 cake 리스트의 첫번째(0번째..)는 초코 케이크였다.
여러가지 방법이 있어서 순서를 픽스해서 배포는 무사히 했지만 뭔가 여전히 남아있는 궁금한 것들이 있었다. .
List<T>
타입과 T
타입이 둘 다 bean으로 등록되어있을 때, 정확히 어떻게 동작하는가? List<T>
만 정의했을 때와 동작이 어떻게 다른가? (왜 List<T>
는 무시되는가)List<T>
타입과 T
타입이 둘 다 bean으로 등록되어있을 때, 정확히 어떻게 동작하는가?한가지 잊고있었던 사실은 List<T>
를 선언하지 않더라도 T
타입의 bean들이 있으면 List필드로 주입이 된다는 거였다.
의존성 주입 시 스프링 컨텍스트는 타입이 같은 모든 빈 객체들을 찾아 List에 빈을 주입한다.
1@Bean2 Cake chocolateCake() {3 return new Cake("초코");4 }56 @Bean7 Cake strawberryCake() {8 return new Cake("딸기");9}
위 코드처럼 bean 설정하고 나면 아래 Bakery에 주입되는 List<Cake>
에는 초코, 딸기 케이크가 다 포함된다.
1@Component2class Bakery {34 private final List<Cake> availableCakes;56 Bakery(List<Cake> availableCakes) {7 this.availableCakes = availableCakes;8 }9}
호옹.. 그렇다면 리스트 타입 bean은 무시하나?? 왜 무시하나??
살펴보다가 까딱 잘못 쓰면 (물론 테스트를 잘 하면 인지할 수 있지만..) 의도랑 달라질 수 있겠단 생각이 들었다.
가령 아래와 같은 코드로 작성한 경우,
1@Bean2 List<Cake> availableCakes() {3 return List.of(4 lemonCake(),5 chocolateCake(),6 strawberryCake()7 );8 }910 @Bean11 Cake chocolateCake() {12 return new Cake("초코");13 }1415 @Bean16 Cake strawberryCake() {17 return new Cake("딸기");18 }1920/** 실수로 bean 어노테이션을 빼먹었다고 가정 **/21 Cake lemonCake() {22 return new Cake("레몬");23 }
⇒ @Bean
으로 선언한 초코,딸기 케이크만 주입된다. (레몬 어디갔어)
어떻게 동작하는 건지 궁금해서 스프링 프레임워크 코드에 디버그를 찍어서 살펴봤다. 위 코드처럼 List<Cake>
타입과 Cake
타입의 빈들 모두 bean으로 설정하고 시작.
** spring 버전: 6.1.14
검색을 하다가 DefaultListableBeanFactory
라는 클래스를 발견했고, 이곳 저곳 찍어보다가 주입할 객체들을 찾는 doResolveDependency
메서드를 발견했다. 이름만 봐도 관련이 있어보인다.
이 메서드는 같은 클래스의 resolveMultipleBeans
를 호출하고, 그 안에서 resolveMultipleBeanCollection
가 호출된다.
resolveMultipleBeanCollection
내부를 보면
beanName (”bakery”) 에 대한 matchingBeans를 findAutowireCandidates()를 호출해서 찾아낸다.
** resolveMultipleBeanCollection 코드:
1@Nullable2private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {3 Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric(new int[0]);4 if (elementType == null) {5 return null;6 } else {7 Map<String, Object> matchingBeans = this.findAutowireCandidates(beanName, elementType, new MultiElementDescriptor(descriptor));8 if (matchingBeans.isEmpty()) {9 return null;10 } else {11 if (autowiredBeanNames != null) {12 autowiredBeanNames.addAll(matchingBeans.keySet());13 }1415 TypeConverter converter = typeConverter != null ? typeConverter : this.getTypeConverter();16 Object result = converter.convertIfNecessary(matchingBeans.values(), descriptor.getDependencyType());17 if (result instanceof List) {18 List<?> list = (List)result;19 if (list.size() > 1) {20 Comparator<Object> comparator = this.adaptDependencyComparator(matchingBeans);21 if (comparator != null) {22 list.sort(comparator);23 }24 }25 }2627 return result;28 }29}
doResolveDependency
는 주입할 빈을 찾았기 때문에 바로 이것을 리턴하고 끝.1Object multipleBeans = this.resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);2if (multipleBeans != null) {3 Object var26 = multipleBeans;4 return var26; // 리턴!5}
List<T>
타입의 bean만 정의했을 때와 동작이 어떻게 다른가?List만 bean으로 설정한 뒤 똑같은 부분을 확인해봤다.
(doResolveDependency
> resolveMultipleBeans
> resolveMultipleBeanCollection
)
resolveMultipleBeanCollection
에서는 List안의 element가 어떤 타입인지 확인하고 element의 타입을 찾는다.
1private Object resolveMultipleBeanCollection(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) {2 Class<?> elementType = descriptor.getResolvableType().asCollection().resolveGeneric(new int[0]);
this.findAutowireCandidates
메서드를 호출할 때 elementType 인자는 Cake.class
가 된다.이 다음 동작은
doResolveDependency
로 돌아가서, 또다시 findAutowireCandidates를 호출하는데, 이번에는 type에 List를 넘긴다.
Map<String, Object> matchingBeans = this.findAutowireCandidates(beanName, type, descriptor);
availableCakes
가 담긴다.doResolveDependency
전체 코드는 너무 길어서 글 최하단에 붙여놨다.⇒ 여기까지 본걸로 정리해보면
List안에 포함되는 각각의 객체가 꼭 bean이어야 하는지 다시 생각해볼 필요가 있다. 아니라면 List 타입만 bean으로 정의하면 리스트에 넣는 순서대로 주입이 된다.
1@Bean2 List<Cake> availableCakes() {3 return List.of(4 lemonCake(),5 chocolateCake(),6 strawberryCake()7 );8 }910// @Bean11 Cake chocolateCake() {12 return new Cake("초코");13 }1415// @Bean16 Cake strawberryCake() {17 return new Cake("딸기");18 }1920// @Bean21 Cake lemonCake() {22 return new Cake("레몬");23 }
1@Test2void test1() {3 List<Cake> actual = sut.getAvailableCakes();45 assertThat(actual).hasSize(3);6 assertThat(actual.get(0).getMainIngredient()).isEqualTo("레몬");7 assertThat(actual.get(1).getMainIngredient()).isEqualTo("초코");8 assertThat(actual.get(2).getMainIngredient()).isEqualTo("딸기");9}10// => 통과한다.
@Order
를 사용하기
@Order
로 정렬을 하게 되면@Order
의 값에 따라 bean 순서가 정렬이 된다.1@Bean2 @Order(2)3 Cake chocolateCake() {4 return new Cake("초코");5 }67 @Bean8 @Order(1)9 Cake strawberryCake() {10 return new Cake("딸기");11 }
⇒ 결과
1@Test2void test1() {3 List<Cake> actual = sut.getAvailableCakes();45 assertThat(actual).hasSize(2);6 assertThat(actual.get(0).getMainIngredient()).isEqualTo("딸기");7 assertThat(actual.get(1).getMainIngredient()).isEqualTo("초코");8}9// => 통과한다.
spring문서에 나와있는 방법이고 @Order 만 붙이면 되어서 간편하긴 한데, 아쉬운 것은 각각의 bean 메서드를 확인해야만 전체 순서를 알 수 있다는 것.. 나중의 나 / 다른 팀원에게 어려울 수 있다.
주입받는 객체 쪽(지금 예제에서는 Bakery) 을 수정할 여지가 있다면, List타입으로 주입받지 않고 Cake를 주입받게 한 다음 Composite Cake를 만들 수도 있다. (Composite객체에서 리스트를 처리)
1@Component2class Bakery {3 private final Cake availableCake;45 Bakery(Cake cake) {6 this.availableCake = cake;7 }8}
1@Bean2@Primary3Cake cakeComposite(4 Cake lemonCake,5 Cake chocolateCake,6 Cake strawberryCake7) {8 List<Cake> cakes = new ArrayList<>();9 cakes.add(lemonCake);10 cakes.add(chocolateCake);11 cakes.add(strawberryCake);12 return new CakeComposite(cakes);13}1415@Bean16Cake chocolateCake() {17 return new Cake("초코");18}1920@Bean21Cake strawberryCake() {22 return new Cake("딸기");23}2425@Bean26Cake lemonCake() {27 return new Cake("레몬");28}
어떤 방법을 선택할지 고민하기 전에 근본적으로 왜 여기에서 순서를 지정해야하는지, 다른 방법으로 더 심플하게 해결할 수는 없는지도 생각해봐야할 것 같다. ///
** 참고: doResolveDependency() 전체 코드
1@Nullable2public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName, @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {3 InjectionPoint previousInjectionPoint = ConstructorResolver.setCurrentInjectionPoint(descriptor);45 Object type;6 try {7 Object shortcut = descriptor.resolveShortcut(this);8 if (shortcut == null) {9 Class<?> type = descriptor.getDependencyType();10 Object value = this.getAutowireCandidateResolver().getSuggestedValue(descriptor);11 if (value == null) {12 Object multipleBeans = this.resolveMultipleBeans(descriptor, beanName, autowiredBeanNames, typeConverter);13 if (multipleBeans != null) {14 Object var26 = multipleBeans;15 return var26;16 }1718 Map<String, Object> matchingBeans = this.findAutowireCandidates(beanName, type, descriptor);19 if (!matchingBeans.isEmpty()) {20 Object instanceCandidate;21 String autowiredBeanName;22 if (matchingBeans.size() > 1) {23 autowiredBeanName = this.determineAutowireCandidate(matchingBeans, descriptor);24 if (autowiredBeanName == null) {25 if (!this.isRequired(descriptor) && this.indicatesArrayCollectionOrMap(type)) {26 Object var31 = null;27 return var31;28 }2930 Object result = descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);31 return result;32 }3334 instanceCandidate = matchingBeans.get(autowiredBeanName);35 } else {36 Map.Entry<String, Object> entry = (Map.Entry)matchingBeans.entrySet().iterator().next();37 autowiredBeanName = (String)entry.getKey();38 instanceCandidate = entry.getValue();39 }4041 if (autowiredBeanNames != null) {42 autowiredBeanNames.add(autowiredBeanName);43 }4445 if (instanceCandidate instanceof Class) {46 instanceCandidate = descriptor.resolveCandidate(autowiredBeanName, type, this);47 }4849 Object result = instanceCandidate;50 if (instanceCandidate instanceof NullBean) {51 if (this.isRequired(descriptor)) {52 this.raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);53 }5455 result = null;56 }5758 if (!ClassUtils.isAssignableValue(type, result)) {59 throw new BeanNotOfRequiredTypeException(autowiredBeanName, type, instanceCandidate.getClass());60 }6162 Object var14 = result;63 return var14;64 }6566 multipleBeans = this.resolveMultipleBeansFallback(descriptor, beanName, autowiredBeanNames, typeConverter);67 if (multipleBeans != null) {68 Object var29 = multipleBeans;69 return var29;70 }7172 if (this.isRequired(descriptor)) {73 this.raiseNoMatchingBeanFound(type, descriptor.getResolvableType(), descriptor);74 }7576 Object var28 = null;77 return var28;78 }7980 if (value instanceof String) {81 String strValue = (String)value;82 String resolvedValue = this.resolveEmbeddedValue(strValue);83 BeanDefinition bd = beanName != null && this.containsBean(beanName) ? this.getMergedBeanDefinition(beanName) : null;84 value = this.evaluateBeanDefinitionString(resolvedValue, bd);85 }8687 TypeConverter converter = typeConverter != null ? typeConverter : this.getTypeConverter();8889 try {90 Object var24 = converter.convertIfNecessary(value, type, descriptor.getTypeDescriptor());91 return var24;92 } catch (UnsupportedOperationException var18) {93 Object var27 = descriptor.getField() != null ? converter.convertIfNecessary(value, type, descriptor.getField()) : converter.convertIfNecessary(value, type, descriptor.getMethodParameter());94 return var27;95 }96 }9798 type = shortcut;99 } finally {100 ConstructorResolver.setCurrentInjectionPoint(previousInjectionPoint);101 }102103 return type;104}