-
Notifications
You must be signed in to change notification settings - Fork 4
Description
그림을 많이 넣으니깐 복잡하네요 ㅠㅠ,,, 세미나 때 말씀드렸던 내용인데 정리해서 공유드립니당 bb
🤔 왜 이슈를 생성했나요?
DI프레임워크는 인터페이스 구현체가 여러개일 때 어떤걸 주입해줄까? 궁금증이 들어서 직접 확인해봤습니다. 오류가 날까요? 아니면 내부적으로 우선순위 조건이 존재하여 그에 맞게 부여될까요?
SpringFramework에 기본 흐름을 이용하지 않고 ApplicationContext 를 직접 선언하였을 때와, SpringBoot 위에서 필드 주입, 생성자 주입을 하였을 때 어떻게 되는지 각각 확인해보았습니다.
사용된 클래스: HelloController, HelloService(Interface), SimpleHelloService(impl), DetailHelloService(impl)
확인할 항목들
- Case1. GenericApplicationContext
- Case2. AnnotationConfigWebApplicationContext
- Case3. @Autowired (필드 주입)
- Case4. @requiredargsconstructor (생성자 주입)
😁 공유하고 싶은 내용
Case별 확인
Case1. GenericApplicationContext
GenericApplicationContext (Spring Framework 6.0.7 API)
- GenericApplicationContext는 명시적으로 Bean을 등록하는 형태입니다.
- 아래와 같은 형태로 빈 등록 구문을 명시한 후에 확인해봤습니다.
// SpringContainer를 만든다.
GenericApplicationContext applicationContext = new GenericApplicationContext();
applicationContext.registerBean(HelloController.class); // 생성할 빈들을 명시
applicationContext.registerBean(SimpleHelloService.class);
applicationContext.registerBean(DetailHelloService.class);
applicationContext.refresh(); // 이걸 통해 빈 오브젝트들을 만들어준다.-
빌드 진행시 오류발생
- No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: tobySpringBoot.learn.SimpleHelloService,tobySpringBoot.learn.DetailHelloService
- HelloService 역할을 하는 컴포넌트가 2개여서 선택을 하지 못하고 에러를 냅니다.
-
에러 전문
Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'tobySpringBoot.learn.HelloController': Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: tobySpringBoot.learn.SimpleHelloService,tobySpringBoot.learn.DetailHelloService at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800) at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1228) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582) at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542) at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335) at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333) at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208) at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955) at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:920) at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583) at tobySpringBoot.learn.LearnApplication.main(LearnApplication.java:30) Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: tobySpringBoot.learn.SimpleHelloService,tobySpringBoot.learn.DetailHelloService at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220) Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: tobySpringBoot.learn.SimpleHelloService,tobySpringBoot.learn.DetailHelloService
Case2. AnnotationConfigWebApplicationContext
AnnotationApplicationContext는 @component 애노테이션이 붙은 클래스를 자동으로 스캔하여 Bean으로 등록합니다. 마찬가지로 에러를 내고 코드가 종료됩니다.
- 코드
public static void run(Class<?> applicationClass, String... args) {
// SpringContainer를 만든다.
AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext() {
@Override
protected void onRefresh() { // onRefresh는 applicationContext.refresh() 내부에서 호출된다.
super.onRefresh();
ServletWebServerFactory serverFactory = this.getBean(ServletWebServerFactory.class);
DispatcherServlet dispatcherServlet = this.getBean(DispatcherServlet.class);
// dispatcherServlet.setApplicationContext(this); // SrpingContainer를 연결해준다.
// 근데 여기에 주입 구문을 안넣어줘도 정상적으로 동작한다. 왜? SpringContainer가 알아서 주입해줌!
// 이걸 이해하려면 Bean의 LifeCycle Method라는 개념을 알아야함
WebServer webServer = serverFactory.getWebServer(servletContext -> {
servletContext.addServlet("dispatcherServlet", dispatcherServlet).addMapping("/*"); // path pattern matching
});
webServer.start();
}
};
applicationContext.register(applicationClass); // 구성정보를 담고있는 클래스를 불러와줘야한다.
applicationContext.refresh(); // 이걸 통해 빈 오브젝트들을 만들어준다.
}- 결과
14:20:06.506 [main] WARN tobySpringBoot.learn.MySpringApplication$1 - Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'helloController' defined in file [/Users/hyeon/Documents/GitHub/toby-spring-boot-study/hyeonsik/spring-boot-project/out/production/classes/tobySpringBoot/learn/HelloController.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: detailHelloService,simpleHelloService
at org.springframework.beans.factory.support.ConstructorResolver.createArgumentArray(ConstructorResolver.java:800)
at org.springframework.beans.factory.support.ConstructorResolver.autowireConstructor(ConstructorResolver.java:229)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireConstructor(AbstractAutowireCapableBeanFactory.java:1372)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBeanInstance(AbstractAutowireCapableBeanFactory.java:1222)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:582)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:542)
at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:335)
at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234)
at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:333)
at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:208)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:955)
at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:920)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:583)
at tobySpringBoot.learn.MySpringApplication.run(MySpringApplication.java:31)
at tobySpringBoot.learn.LearnApplication.main(LearnApplication.java:42)
Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: detailHelloService,simpleHelloService
at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1369)
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)
... 14 moreCase3,4 Spring 자동 주입
이후 케이스부터는 @SpringBootApplication 을 붙여 실제 스프링 부트를 실행을 활성화 시켰습니다. 이후 붙이자마자 바로 생성자 주입을 테스트 해봤습니다. 참고로 Spring 4.3 버전부터, 특정 클래스에 생성자가 1개뿐이고, 인자로 주입받는 클래스들이 빈으로 등록되어있다면 자동으로 생성자 주입이 일어나도록 되어있어 @Autowired를 생략해도 됩니다!
//@Configuration
//@ComponentScan
@SpringBootApplication
public class LearnApplication {
...- 결과
결과는 당연히 오류가 발생하네요 😅 정확히는 org.springframework.beans.factory.NoUniqueBeanDefinitionException 예외가 발생합니다. 이후 내용도 동일하기 때문에 설명은 생략하겠습니다!
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in tobySpringBoot.learn.HelloController required a single bean, but 2 were found:
- detailHelloService: defined in file [D:\github\toby-spring-boot-study\hyeonsik\spring-boot-project\out\production\classes\tobySpringBoot\learn\DetailHelloService.class]
- simpleHelloService: defined in file [D:\github\toby-spring-boot-study\hyeonsik\spring-boot-project\out\production\classes\tobySpringBoot\learn\SimpleHelloService.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Process finished with exit code 1Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'tobySpringBoot.learn.HelloService' available: expected single matching bean but found 2: detailHelloService,simpleHelloService
at org.springframework.beans.factory.config.DependencyDescriptor.resolveNotUnique(DependencyDescriptor.java:220)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1369)
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)
... 14 moreCase3. @Autowired (필드주입)
- 코드 (Setter 방식은 생략합니다!)
@RequestMapping()
@Controller
public class HelloController {
@Autowired
private HelloService helloService;
// public HelloController(HelloService helloService) {
// this.helloService = helloService;
// }
@GetMapping("/hello")
@ResponseBody
public String hello(String name) {
return helloService.sayHello(Objects.requireNonNull(name));
}
}- 결과
***************************
APPLICATION FAILED TO START
***************************
Description:
Field helloService in tobySpringBoot.learn.HelloController required a single bean, but 2 were found:
- detailHelloService: defined in file [D:\github\toby-spring-boot-study\hyeonsik\spring-boot-project\out\production\classes\tobySpringBoot\learn\DetailHelloService.class]
- simpleHelloService: defined in file [D:\github\toby-spring-boot-study\hyeonsik\spring-boot-project\out\production\classes\tobySpringBoot\learn\SimpleHelloService.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Process finished with exit code 1Case4. @requiredargsconstructor (생성자 주입)
- 결과
***************************
APPLICATION FAILED TO START
***************************
Description:
Parameter 0 of constructor in tobySpringBoot.learn.HelloController required a single bean, but 2 were found:
- detailHelloService: defined in file [/Users/hyeon/Documents/GitHub/toby-spring-boot-study/hyeonsik/spring-boot-project/out/production/classes/tobySpringBoot/learn/DetailHelloService.class]
- simpleHelloService: defined in file [/Users/hyeon/Documents/GitHub/toby-spring-boot-study/hyeonsik/spring-boot-project/out/production/classes/tobySpringBoot/learn/SimpleHelloService.class]
Action:
Consider marking one of the beans as @Primary, updating the consumer to accept multiple beans, or using @Qualifier to identify the bean that should be consumed
Process finished with exit code 1내부적으론 어떤 동작이 일어날까요?
동일한 인터페이스를 가진 Bean 이 존재하면 DI 동작이 안된다는 것을 확인하였습니다. 그렇다면 내부적으로 어떤 단계를 통해 에러가 발생할까요? 굉장히 복잡한 과정이라 간단하게만 정리해보았습니다.
Bean(instance)이 Container에 적재되는 과정
저(를 포함한 대부분)는 implementation 'org.springframework.boot:spring-boot-starter-web' 를 사용하고 있으므로, AnnotationConfigServletWebServerApplicationContext 가 내부적으로 동작하며 이는 org.springframework.boot.web.servlet.context 에 위치해 있습니다.
클래스 구조가 좀 복잡해보이는데, AnnotationConfigServletWebServerApplicationContext extends ServletWebServerApplicationContext extends GenericWebApplicationContext extends GenericApplicationContext 까지 올라가면 핵심 역할을 하는 DefaultListableBeanFactory 를 찾을 수 있습니다.
여기서 BeanFactory에 역할은 무엇일까요? 빈을 생성하고 의존관계 설정을 담당하는 가장 기본적인 IoC 컨테이너(클래스) 입니다. 이 BeanFactory 을 포함하여 확장시킨 것이 ApplicationContext 입니다. 즉, 간단히 BeanFactory와 ApplicationContext는 DI를 진행한다는 같은 목적을 가지고 있습니다.
하지만 BeanFactory 는 빈이 요청될 때 해당 빈을 로딩하고, ApplicationContext는 클래스가 초기화되면서 관련 빈들도 함께 로딩된다는 차이점이 있습니다.
Chapter 3. Beans, BeanFactory and the ApplicationContext
ClassPathBeanDefinitionScanner.doScan() 이 @component 들을 불러와 BeanDefinitons(Bean 명세서)를 만듭니다.

protected Set<BeanDefinitionHolder> doScan(String... basePackages) {
Assert.notEmpty(basePackages, "At least one base package must be specified");
Set<BeanDefinitionHolder> beanDefinitions = new LinkedHashSet<>();
for (String basePackage : basePackages) {
Set<BeanDefinition> candidates = findCandidateComponents(basePackage);
for (BeanDefinition candidate : candidates) {
ScopeMetadata scopeMetadata = this.scopeMetadataResolver.resolveScopeMetadata(candidate);
candidate.setScope(scopeMetadata.getScopeName());
String beanName = this.beanNameGenerator.generateBeanName(candidate, this.registry);
if (candidate instanceof AbstractBeanDefinition) {
postProcessBeanDefinition((AbstractBeanDefinition) candidate, beanName);
}
if (candidate instanceof AnnotatedBeanDefinition) {
AnnotationConfigUtils.processCommonDefinitionAnnotations((AnnotatedBeanDefinition) candidate);
}
if (checkCandidate(beanName, candidate)) {
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(candidate, beanName);
definitionHolder =
AnnotationConfigUtils.applyScopedProxyMode(scopeMetadata, definitionHolder, this.registry);
beanDefinitions.add(definitionHolder);
registerBeanDefinition(definitionHolder, this.registry);
}
}
}
return beanDefinitions;
}DefaultListableBeanFactory.preInstantiateSingletons() 에서 getBean()을 호출하여 각 빈의 인스턴스를 생성하게 됩니다. (AutowireCapableBeanFactory에서 조회한 Bean의 instacne를 생성합니다.)
protected Object doCreateBean(String beanName, RootBeanDefinition mbd, @Nullable Object[] args)
throws BeanCreationException {
// Instantiate the bean.
BeanWrapper instanceWrapper = null;
if (mbd.isSingleton()) {
instanceWrapper = this.factoryBeanInstanceCache.remove(beanName);
}
if (instanceWrapper == null) {
instanceWrapper = createBeanInstance(beanName, mbd, args);
}그럼 에러가 발생하는 위치는?
발생하는 에러가 NoUniqueBeanDefinitionException 인 것을 위에서 확인하였습니다. 그럼 실제로 이 에러는 어디서 누가 throw 하는 것 일까요? 해당 exception을 기준으로 breakPoint를 생성하여 확인해 봅시다!
예외는 DefaultListableBeanFactory.doResolveDependency() 에서 descriptor에 resolveNotUnique() 를 호출하여 발생하게됩니다. 참고로 발생하는 NoUniqueBeanDefinitionException은 BeansException을 상속하고 있습니다.
이 예외에 대한 책임은 resolveNotUnique, doResolveDependency, resolveDependency에서 명시적으로 회피하며, ConstructorResolver.resolveAutowiredArgument()에서Chained Exception 됩니다.
다시 돌아와서 DefaultListableBeanFactory.doResolveDependency() 의 코드를 살펴봅시다.
@Nullable
public Object doResolveDependency(DependencyDescriptor descriptor, @Nullable String beanName,
@Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
// ...
String autowiredBeanName;
Object instanceCandidate;
if (matchingBeans.size() > 1) {
autowiredBeanName = determineAutowireCandidate(matchingBeans, descriptor);
if (autowiredBeanName == null) {
if (isRequired(descriptor) || !indicatesMultipleBeans(type)) {
return descriptor.resolveNotUnique(descriptor.getResolvableType(), matchingBeans);
}
// ...autowiredBeanName에 값이 존재하지않아(Null) 문제 상황이 발생하였습니다. 이 말은 autowiredBeanName을 초기화하는 determineAutowireCandidate가 null 값을 리턴하고 있다는 이야기겠죠?
다음과 같이 우선순위를 명시하지 않은 2개의 Service(Component)가 존재하여 null이 return 되고, 최종적으로 Exception을 발생시키게 되는 원인이 되는 것으로 확인했습니다!
@Nullable
protected String determineAutowireCandidate(Map<String, Object> candidates, DependencyDescriptor descriptor) {
Class<?> requiredType = descriptor.getDependencyType();
String primaryCandidate = determinePrimaryCandidate(candidates, requiredType);
if (primaryCandidate != null) {
return primaryCandidate;
}
String priorityCandidate = determineHighestPriorityCandidate(candidates, requiredType);
if (priorityCandidate != null) {
return priorityCandidate;
}
// Fallback
for (Map.Entry<String, Object> entry : candidates.entrySet()) {
String candidateName = entry.getKey();
Object beanInstance = entry.getValue();
if ((beanInstance != null && this.resolvableDependencies.containsValue(beanInstance)) ||
matchesBeanName(candidateName, descriptor.getDependencyName())) {
return candidateName;
}
}
return null;
}해결방법
@primary, @qualifier 명시
-
동일한 구현체가 존재할경우 @primary, @qualifier 애노테이션을 통해 우선순위를 명시하여 해결할 수 있습니다.
[Spring 핵심 원리 기본편 (8) - @Primary, @Qualifier](https://velog.io/@neity16/Spring-핵심-원리-기본편-8-Primary-Qualifier)
-
예를들어 아래와같이 @primary 을 사용할 경우 위 문제상황들에서 정상적으로 해당 빈(서비스)이 주입되어 문제가 해결되는 것을 확인할 수 있어요!
@Service
@Primary
public class DetailHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello " + name + ", this is detail service.";
}
}HTTP/1.1 200
Connection: keep-alive
Content-Length: 33
Content-Type: text/plain;charset=ISO-8859-1
Date: Wed, 12 Apr 2023 05:24:09 GMT
Keep-Alive: timeout=60
Hello aa, this is detail service.Component 없이 해결
- @configuration 클래스를 만들어서 @bean 을 직접 명시하고, 여기서에서 @primary 애노테이션을 부여한다.
- 기존 Service들에 있던 Component 구문을 제거해야합니다.
- 즉, Component로 지정하지 않고 Configuration 을 통해 Bean을 등록함으로써 해결하는 방법입니다.
- 사용할 서비스를 자바 코드로, 외부에서 관리할 수 있는 장점이 있습니다~
@Configuration
public class HelloConfiguration {
@Bean
public HelloService simpleHelloService() {
return new SimpleHelloService();
}
@Bean
@Primary
public HelloService detailsHelloService() {
return new DetailHelloService();
}
}// @Service
// @Primary
public class DetailHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello " + name + ", this is detail service.";
}
}// @Service
public class SimpleHelloService implements HelloService {
@Override
public String sayHello(String name) {
return "Hello " + name;
}
}📌 참고자료, 공유하고 싶은 자료
getBean() 직접 사용을 권장하지 않는다.
Dependecny Injection
[Core Technologies](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-factory-client)
여러녀석들 중에 매핑
[Core Technologies](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-autowired-annotation-primary)
[Spring @Qualifier Annotation](https://www.baeldung.com/spring-qualifier-annotation)








