개발도구/Spring, Spring Boot

빈 충돌 문제해결 (NoUniqueBeanDefinitionException, UnsatisfiedDependencyException, @ComponentScan, @Bean)

redsiwon 2023. 6. 23. 15:41

인프런, <스프링 핵심 원리 - 기본편> "섹션 6. 중복 등록과 충돌"에서 스프링 부트 앱을 실행할 때 발생하는 빈 충돌 문제해결을 위해 작성한 글입니다.


스프링부트 앱 실행 및 테스트로 확인한 주요 에러 메시지 정리

스프링부트 앱 실행시 발생하는 에러 메시지

MemberServiceImpl의 생성자의 파라미터 0이 싱글빈일 것이 요구되는데, 2개의 후보가 있다고 한다.

 

더 자세하게 살펴보기 위해 테스트로 핵심 에러 메시지를 뽑아보면 다음과 같다.

  1. java.lang.IllegalStateException: Failed to load ApplicationContext
  2. Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'memberServiceImpl' defined in file [/Users/joonheejeong/IdeaProjects/inflearn-spring-kimyounghan/core/out/production/classes/hello/core/member/MemberServiceImpl.class]: Unsatisfied dependency expressed through constructor parameter 0; nested exception is org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memoryMemberRepository,memberRepository
  3. Caused by: org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memoryMemberRepository,memberRepository

정리하면 hello.core.member.MemberRepository의 후보로 AutoAppConfig의 memoryMemberRepository와 AppConfig의 memberRepository가 충돌되어 발생하는 문제인 것을 알 수 있다.


작성한 코드를 통하여 문제 원인 분석

관련 코드를 살펴보면서 문제의 정확한 원인을 분석해보자.

package hello.core;

...

@Configuration
public class AppConfig {

    ...

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("call only onetime -- AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
    
    ...
}
package hello.core;

...

@Configuration
@ComponentScan(excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Configuration.class))
public class AutoAppConfig {

    @Bean("memoryMemberRepository")
    public MemberRepository memberRepository() {
        System.out.println("call only onetime -- AutoAppConfig.memberRepository");
        return new MemoryMemberRepository();
    }
}
spring.main.allow-bean-definition-overriding=true

 

원인 후보는 다음과 같다.

Q1. 검색 기준 패키지 설정

A1. basePackages 문제는 아니다. 별도의 설정을 주지 않았고, 둘 다 모두 같은 패키지인 hello.core에 존재해서 문제가 없어야 한다.

 

Q2. 오버라이딩

A2. 오버라이딩의 문제도 아니다. application.properties 파일에서 오버라이딩을 허용하도록 설정했다. 에러메시지도 오버라이딩에서 발생하는 메시지가 아니다.

 

위의 두 가지는 문제가 없는 것으로 보인다. 다음 두 가지가 핵심 문제다.

 

Q3. 강의 내용대로 컴포넌트 스캔에서 제외필터로 @Configuration을 설정하여 Configuration을 선언한 다른 클래스인 AppConfig가 스캔 대상에서 제외되어야 할텐데, 그렇게 되지 않았다.

 

A3. 스프링부트 앱을 실행할 경우, AutoAppConfig에서 제외필터로 AppConfig를 제외해도, 스프링부트 앱 차원에서는 hello.core 패키지 전체를 스캔하기 때문에 제외필터가 소용이 없는 것이다. 따라서 AppConfig 내부에서 선언한 빈들도 결국 모두 생성되고, 주입 후보로 간주된다.

 

그리고...

Q4. @Autowired를 통한 의존관계 자동 주입 시, 빈 선택 기준

@Autowired 충돌이 발생하고 있는 MemberServiceImpl

@Autowired 에러 메시지를 보면 MemberRpository 타입의 빈으로 하나 이상의 빈이 존재하여 자동 주입을 할 수 없다고 한다.

 

@Autowired는 다음과 같은 빈 선택 기준을 갖고 있다.

1. 타입에 해당하는 빈을 찾는다. -- 당연히 상속관계를 고려한다. 즉 하위타입의 빈도 같이 검색된다.
2. 만약 1번의 결과가 유일하면 해당 빈을 주입한다.
3. 그러나 1번의 결과가 2개 이상일 경우, 먼저는 후보들 중에서 @Primary를 찾아서 있으면 해당 빈을 주입한다.
4-a. 없으면 후보들 중에서 이름을 기준으로 검색한다. "검색할 이름"의 기준은 다음과 같다.
  - 기본으로 필드/파라미터의 이름으로 검색한다.
  - @Qualifier를 통해 각 필드/파라미터 이름 대신 다른 이름을 검색할 수 있다. 이때 기존 이름은 검색 기준에서 무시된다.
4-b. 생성되는 빈의 이름, 즉 "검색되는 이름"은 다음과 같은 기준으로 설정된다.
  - @Component 선언된 클래스는 클래스명에서 첫글자만 소문자로 바꿔서 빈 이름으로 삼는다.
  - @Bean 선언된 팩터리 메서드는 메소드 이름이 기본으로 빈 이름이 된다.
  - @Component 및 @Bean의 파라미터로 이름을 별도 지정하면 기본 빈 이름을 무시하고 해당 이름이 빈 이름이 된다.
  - 추가적으로, @Qualifier를 통해 이름을 별도 설정했다면, 빈 이름은 그대로 두고 입력한 값을 검색되는 기준으로 삼는다.

(빈 선택 기준은 말로만으로는 상당히 복잡하게 느껴져서 코드를 통한 설명이 필요하지만 일단 넘어간다..)

 

앞서 설명한 기준에 따라 바로 위 코드 이미지에서는 스프링부트애플리케이션과 AutoAppConfig의 @ComponentScan에 의해 @Component가 선언된 MemberServiceImpl 클래스의 인스턴스를 빈으로 등록하려고 할 때

1. @Autowired가 선언된 생성자에서 파라미터의 타입인 MemberRepository를 검색하는데, 검색 결과 해당 타입의 빈이 2개이고,

2. 파라미터 이름이 repository라 2개 중에 하나로 한정할 수 없어서 에러가 발생하는 것이다.

 

A4. 따라서 문제를 해결하려면 해당 생성자의 파라미터 이름을 "memoryMemberService" (AutoAppConfig에서 정의) 또는 "memberService" (AppConfig에서 정의)로 설정해서 단 하나의 빈만 한정할 수 있도록 해야 한다. (물론 에러 메시지처럼 @Primary를 사용하거나, 빈 이름을 별도로 설정하거나 @Qualifer를 이용할 수도 있다.)

 

영한님 강의와 비교해서 보자면, 영한님은 필드 타입에 맞게 이름을 memberRepository로 설정하셨는데, 나는 그냥 repository라고 설정해서 문제가 발생한 것이다..


문제 해결

위에서 언급했듯 MemberServiceImpl의 생성자 파라미터의 이름 문제인데, 이 이름을 repository에서 AutoAppConfig에서 선언한 빈을 사용할 수 있도록 memoryMemberRepository로 바꿔본다.

 

생성자 파라미터의 이름이 수정된 MemberServiceImpl

다음은 수정 후 스프링부트 앱을 실행해본다.

수정 후 스프링부트 앱 실행 성공!

에러 없이 잘 출력되는 것을 볼 수 있다.