Skip to content

[최현식] DI프레임워크는 인터페이스 구현체가 여러개일 때 어떤걸 주입해줄까? #13

@chhs2131

Description

@chhs2131

그림을 많이 넣으니깐 복잡하네요 ㅠㅠ,,, 세미나 때 말씀드렸던 내용인데 정리해서 공유드립니당 bb

🤔 왜 이슈를 생성했나요?

DI프레임워크는 인터페이스 구현체가 여러개일 때 어떤걸 주입해줄까? 궁금증이 들어서 직접 확인해봤습니다. 오류가 날까요? 아니면 내부적으로 우선순위 조건이 존재하여 그에 맞게 부여될까요?

SpringFramework에 기본 흐름을 이용하지 않고 ApplicationContext 를 직접 선언하였을 때와, SpringBoot 위에서 필드 주입, 생성자 주입을 하였을 때 어떻게 되는지 각각 확인해보았습니다.

사용된 클래스: HelloController, HelloService(Interface), SimpleHelloService(impl), DetailHelloService(impl)

확인할 항목들


😁 공유하고 싶은 내용

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 more

Case3,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 1
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 more

image

Case3. @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 1

Case4. @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 에 위치해 있습니다.

image

image


클래스 구조가 좀 복잡해보이는데, 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 명세서)를 만듭니다.
image

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를 생성합니다.)

image

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를 생성하여 확인해 봅시다!

image


예외는 DefaultListableBeanFactory.doResolveDependency() 에서 descriptor에 resolveNotUnique() 를 호출하여 발생하게됩니다. 참고로 발생하는 NoUniqueBeanDefinitionException은 BeansException을 상속하고 있습니다.

이 예외에 대한 책임은 resolveNotUnique, doResolveDependency, resolveDependency에서 명시적으로 회피하며, ConstructorResolver.resolveAutowiredArgument()에서Chained Exception 됩니다.


  • DefaultListableBeanFactory.doResolveDependency()
    image

  • DependencyDescriptorresolveNotUnique()
    image


다시 돌아와서 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을 발생시키게 되는 원인이 되는 것으로 확인했습니다!

image

@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 명시

@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() 직접 사용을 권장하지 않는다.

image

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)

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions