Web/Spring

ConstructorResolver:: Spring Helper Class 내부 구현 뜯어보기 (NoSuchBeanDefinitionException)

TLdkt 2023. 3. 4. 11:27
728x90
반응형

들어가며

앞선 포스팅에서 이미 에러를 해결하긴 했지만,

이번에는 NoSuchBeanDefinitionException 에러가 어떤 과정으로 도출됐는지도 알아보자

 

구체적인 에러와 해결책이 궁금하신 분은 앞서 적은 포스팅 참고하시길

https://kindspoon.tistory.com/228

 

[트러블슈팅/개념정리] WebMvcTest와 MockBean이 함께 쓰이는 이유(Feat. Mockito)

들어가며 컨트롤러 테스트를 하려고 코드를 열심히 적었따.. 기억에 의존해서 썼더니.... 실패는 물론이고 애초에 contextloader가 작동하지 않았다고 해서 몇 시간 동안 해결책을 찾아 헤맨 끝에 해

kindspoon.tistory.com

 

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가 열심히 빈 찾아주려고 하다가 실패했다는 뜻이다.

따봉헬퍼야 고마워!

728x90
반응형