들어가며
앞선 포스팅에서 이미 에러를 해결하긴 했지만,
이번에는 NoSuchBeanDefinitionException 에러가 어떤 과정으로 도출됐는지도 알아보자
구체적인 에러와 해결책이 궁금하신 분은 앞서 적은 포스팅 참고하시길
https://kindspoon.tistory.com/228
NoSuchBeanDefinitionException 에러 발생과정 파헤치기
에러메세지
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'teamProject.fitbackLogin.Service.AuthService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:1801)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1357)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1311)
at org.springframework.beans.factory.support.ConstructorResolver.resolveAutowiredArgument(ConstructorResolver.java:887)
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:791)
아래에서부터 읽으면 ConstructorResolver 791번 줄 -> 887번 줄 -> DefaultListableBeanFactory.resolveDependency 이동-> ...-> 에러 !
라고 해석할 수 있으니 ConstructorResolver부터 들어가자
ConstructorResolver
여기서부터 에러가 시작됐다는 걸 알 수 있다
ctrl + 클릭으로 이동하면 동일 클래스 내에 메서드가 나오고
문제가 됐던 위치인 887번 줄은 여기다
좀 논리가 건너뛰긴 했지만.. 아무튼 중간에 DefaultListableBeanFactory.resolveDependency 를 거쳐 NoSuchBeanDefinitionException 으로 catch하게 되면 아래처럼 된다.
DefaultListableBeanFactory 내부의 resolveDependency()
private void raiseNoMatchingBeanFound(
Class<?> type, ResolvableType resolvableType, DependencyDescriptor descriptor) throws BeansException {
checkBeanNotOfRequiredType(type, descriptor);
throw new NoSuchBeanDefinitionException(resolvableType,
"expected at least 1 bean which qualifies as autowire candidate. " +
"Dependency annotations: " + ObjectUtils.nullSafeToString(descriptor.getAnnotations()));
}
이 부분을 통과하면서
public NoSuchBeanDefinitionException(ResolvableType type, String message) {
super("No qualifying bean of type '" + type + "' available: " + message);
this.beanName = null;
this.resolvableType = type;
}
이 에러메세지와 합쳐지게 되는데
그 결과가 바로 위에서 나왔던 caused by 뒤의 내용이다 소름!
No qualifying bean of type 'teamProject.fitbackLogin.Service.AuthService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
비록 삽질을 몇 시간씩 했지만... 에러메세지를 만들기까지 여정에 함께한 알찬 시간이었따 ^^
ConstructorResolver 내의 createArgumentArray() 뜯어보기
ConstructorResolver에서 문제가 됐던 부분의 메서드만 잠깐 분석해보자.
분석 과정에서 DefaultListableBeanFactory를 왜 갔는지, resolveDependency()를 왜 호출하는지 알 수 있었다
메서드 분석
1. 생성자의 파라미터 array를 만들어주는 createArgumentArray() 메서드다
2. 빈 이름, 빈 definition, 파라미터 타입, 플래그 등등을 받는다
3. paramTypes를 순회하며 매칭되는 args를 찾는다(resolvedValues 내에서)
4. 찾으면 ArgumentsHolder에 넣는다
5. 못 찾았는데 'autowiring' 플래그가 true로 되어 있으면 기존 빈 팩터리에서 찾아내려고 애쓴다. 못 찾으면 예외를 던진다
6. 다 찾으면 or 만들어지면 ArgumentHolder를 리턴한다.
코드 보기
private ArgumentsHolder createArgumentArray(
String beanName, RootBeanDefinition mbd, @Nullable ConstructorArgumentValues resolvedValues,
BeanWrapper bw, Class<?>[] paramTypes, @Nullable String[] paramNames, Executable executable,
boolean autowiring, boolean fallback) throws UnsatisfiedDependencyException {
TypeConverter customConverter = this.beanFactory.getCustomTypeConverter();
TypeConverter converter = (customConverter != null ? customConverter : bw);
ArgumentsHolder args = new ArgumentsHolder(paramTypes.length);
Set<ConstructorArgumentValues.ValueHolder> usedValueHolders = new HashSet<>(paramTypes.length);
Set<String> autowiredBeanNames = new LinkedHashSet<>(4);
for (int paramIndex = 0; paramIndex < paramTypes.length; paramIndex++) {
Class<?> paramType = paramTypes[paramIndex];
String paramName = (paramNames != null ? paramNames[paramIndex] : "");
// Try to find matching constructor argument value, either indexed or generic.
ConstructorArgumentValues.ValueHolder valueHolder = null;
if (resolvedValues != null) {
valueHolder = resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders);
// If we couldn't find a direct match and are not supposed to autowire,
// let's try the next generic, untyped argument value as fallback:
// it could match after type conversion (for example, String -> int).
if (valueHolder == null && (!autowiring || paramTypes.length == resolvedValues.getArgumentCount())) {
valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders);
}
}
if (valueHolder != null) {
// We found a potential match - let's give it a try.
// Do not consider the same value definition multiple times!
usedValueHolders.add(valueHolder);
Object originalValue = valueHolder.getValue();
Object convertedValue;
if (valueHolder.isConverted()) {
convertedValue = valueHolder.getConvertedValue();
args.preparedArguments[paramIndex] = convertedValue;
}
else {
MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
try {
convertedValue = converter.convertIfNecessary(originalValue, paramType, methodParam);
}
catch (TypeMismatchException ex) {
throw new UnsatisfiedDependencyException(
mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam),
"Could not convert argument value of type [" +
ObjectUtils.nullSafeClassName(valueHolder.getValue()) +
"] to required type [" + paramType.getName() + "]: " + ex.getMessage());
}
Object sourceHolder = valueHolder.getSource();
if (sourceHolder instanceof ConstructorArgumentValues.ValueHolder) {
Object sourceValue = ((ConstructorArgumentValues.ValueHolder) sourceHolder).getValue();
args.resolveNecessary = true;
args.preparedArguments[paramIndex] = sourceValue;
}
}
args.arguments[paramIndex] = convertedValue;
args.rawArguments[paramIndex] = originalValue;
}
else {
MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
// No explicit match found: we're either supposed to autowire or
// have to fail creating an argument array for the given constructor.
if (!autowiring) {
throw new UnsatisfiedDependencyException(
mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam),
"Ambiguous argument values for parameter of type [" + paramType.getName() +
"] - did you specify the correct bean references as arguments?");
}
try {
Object autowiredArgument = resolveAutowiredArgument(
methodParam, beanName, autowiredBeanNames, converter, fallback);
args.rawArguments[paramIndex] = autowiredArgument;
args.arguments[paramIndex] = autowiredArgument;
args.preparedArguments[paramIndex] = autowiredArgumentMarker;
args.resolveNecessary = true;
}
catch (BeansException ex) {
throw new UnsatisfiedDependencyException(
mbd.getResourceDescription(), beanName, new InjectionPoint(methodParam), ex);
}
}
}
for (String autowiredBeanName : autowiredBeanNames) {
this.beanFactory.registerDependentBean(autowiredBeanName, beanName);
if (logger.isDebugEnabled()) {
logger.debug("Autowiring by type from bean name '" + beanName +
"' via " + (executable instanceof Constructor ? "constructor" : "factory method") +
" to bean named '" + autowiredBeanName + "'");
}
}
return args;
}
특징
autowiredBeanNames를 저장하기 위해 LinkedHaskSet()을 썼다.
참고로 LinkedHashSet은 HashSet의 요소를 이중 연결 리스트로 만든 형태다. 즉 순서가 있다는 말이다.
Autowired 시에 Bean 순서를 보장하는 게 왜 중요한지 잘 이해가 안 됐다면 아래 예시를 생각해보자.
크게 중요한 경우는 아닌 것 같지만.. 이런 쓰임도 있다는 정도로 알아두면 될 것 같다.
만약 두 레파지터리에 의존성이 있다면, 이들을 서비스에 주입할 때 순서가 보장되어야 할 것이다.
예를 들어 상품 레파지터리와 상품 이미지 레파지터리가 있다면 상품 레파지터리가 먼저 생성되어야 한다.
이처럼 특수한 상황을 고려해 HashSet이 아닌 LinkedHashSet을 사용했고, 그 말은 스프링이 내부적으로 autowired할 빈들의 순서를 알아서 확인한다는 말이기도 하다.
더 궁금한 점이 있다면 @DependsOn 키워드로 찾아보면 좋을 것 같다. 이 부분은 처음 공부하는 거라 완전하게 이해한 건 아니고 '이렇지 않을까...?' 정도라는 점 이해해주시길 ^0^
정리
Autowired를 사용했을 때 NoSuchBeanException이 났다면
스프링 Helper 클래스인 ConstructorResolver가 열심히 빈 찾아주려고 하다가 실패했다는 뜻이다.
따봉헬퍼야 고마워!
'Web > Spring' 카테고리의 다른 글
[트러블슈팅/개념정리] WebMvcTest와 MockBean이 함께 쓰이는 이유(Feat. Mockito) (0) | 2023.03.03 |
---|---|
Spring Data JPA의 동작 과정: 메타 데이터로 SQL DDL 만들기 (0) | 2023.03.03 |
Error Handling w.Spring: 꼼꼼하고 효율적인 예외처리는 어떻게 만들어질까? (2) | 2023.01.26 |
Spring Security의 간장공장공장장😱 Authentication? Authorization? Provider? Manager? FilterChain? 쉽게 정리하자 (0) | 2022.10.23 |
Spring WebFlux 세팅과 구현 방식 Reactive Programming in Spring Framework (1) | 2022.10.18 |