인프런 커뮤니티 질문&답변

Mpels님의 프로필 이미지

작성한 질문수

스프링 핵심 원리 - 기본편

[섹션 7 - 옵션 처리] 전체 테스트 중 CoreApplicationTests 클래스의 contextLoads 테스트 실패 질문입니다.

작성

·

2.6K

·

수정됨

15

안녕하세요.

게시판을 둘러보니 비슷한 오류가 나시는 분들이 계신것 같은데 해결 되신 분이 없는 것 같아 질문드립니다.

개발 환경

  • Spring Boot : 3.2.0

  • 운영체제 : Mac OS X

  • IDE : IntelliJ IDEA Ultimate 2023.2.5

  • JDK : JDK 17

  • 빌드 툴 : Gradle 8.4

문제

강의를 따라가던 도중 전체 테스트를 진행하는 과정에서 CoreApplicationTests 클래스의 contextLoads 테스트가 NoUniqueBeanDefinitionException 오류를 발생시키며 실패합니다.

org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memoryMemberRepository,memberRepository

특이한점으로 GitHub에 올려놓은 코드를 내려받은 후 실행하면 테스트가 통과하고, 지금까지 했던 프로젝트를 실행하면 테스트가 실패합니다.

테스트가 성공한 프로젝트도 아래처럼 컨텍스트를 주입받아 MemberRepository를 getBean으로 받아오는 테스트를 해보면 오류가 납니다.

@SpringBootTest
class CoreApplicationTests {

    @Autowired ApplicationContext ac;

    @Test
    void contextLoads() {
        MemberRepository bean = ac.getBean(MemberRepository.class);
    }

}
org.springframework.beans.factory.NoUniqueBeanDefinitionException: No qualifying bean of type 'hello.core.member.MemberRepository' available: expected single matching bean but found 2: memoryMemberRepository,memberRepository

로그를 살펴보면 아래와 같습니다.

expected single matching bean but found 2: memoryMemberRepository,memberRepository

컴포넌트 스캔으로 등록한 빈과, AppConfig를 통해 등록한 빈이 겹치는 것 같습니다.

아래는 유추한 내용입니다.

컴포넌트 스캔

스크린샷 2023-11-29 오후 9.04.56.png이름을 변경해서 확인해봤습니다.

스크린샷 2023-11-29 오후 9.06.00.png다시 테스트를 돌려보면 로그가 아래처럼 찍힙니다.

expected single matching bean but found 2: 메모리멤버레포지토리,memberRepository

@Bean

ㅇㅁㅁㄹ.png이름을 변경해서 확인해봤습니다.

스크린샷 2023-11-29 오후 9.09.53.png테스트를 돌려보면 로그가 아래처럼 찍힙니다.

expected single matching bean but found 2: 메모리멤버레포지토리,앱콘피그에있는메모리레포지토리

컴포넌트 스캔을 이용하여 MemberRepository 빈을 등록했는데 AppConfig 에서 @Bean 어노테이션이 붙은 메서드의 반환 객체도 빈으로 중복 등록 되어 발생한 것으로 생각됩니다.

의문점

AutoAppConfig 에서 Configuration 어노테이션이 붙은 클래스는 스캔의 대상에서 제외를 했는데 왜 중복해서 등록이 된 것일까요?

스크린샷 2023-11-29 오후 9.17.34.png검증을 위해 스프링 부트 통합 테스트를 진행해보았더니 AppConfig 가 빈으로 등록되어 있습니다.

@SpringBootTest
class CoreApplicationTests {

    @Autowired ApplicationContext ac;

    @Test
    void contextLoads() {
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println(bean);
    }

}
hello.core.AppConfig$$SpringCGLIB$$0@37df14d1

AppConfig 클래스의 코드입니다.

@Configuration
public class AppConfig {

    @Bean
    public MemberService memberService() {
        System.out.println("Call - AppConfig.memberService");
        return new MemberServiceImpl(memberRepository());
    }

    @Bean
    public MemberRepository memberRepository() {
        System.out.println("Call - AppConfig.memberRepository");
        return new MemoryMemberRepository();
    }

    @Bean
    public OrderService orderService() {
        System.out.println("Call - AppConfig.orderService");
        return new OrderServiceImpl(memberRepository(), discountPolicy());
    }

    @Bean
    public DiscountPolicy discountPolicy() {
//        return new FixDiscountPolicy();
        return new RateDiscountPolicy();
    }

}

(+) 컴포넌트 스캔을 CoreApplication 으로 옮겨도 똑같이 오류가 발생합니다.

스크린샷 2023-12-02 오전 4.13.25.png추가 질문

만약 위 의문이 해결되어 AppConfig 에서 생성한 객체들이 빈으로 등록되지 않고, 컴포넌트 스캔을 통하여 빈을 등록한다면 MemoryMemberRepositorymemoryMemberRepository 이름으로 빈으로 등록됩니다.

 

그렇다면 MemberServiceImpl 에서는 memberRepository 를 주입받아야 하는데, 빈의 이름이 달라 주입이 불가능할 것 같습니다. 이 경우에는 @Component("memberReository") 로 수정해줘야 할까요?

 

감사합니다.

답변 4

13

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. Mpels님

추가로 질문주신 부분에 대한 답을 드릴께요.

 

스프링의 @ComponentScan이 중복 적용된 경우에는 excludeFilters가 적용되지 않습니다.

예를 들어서 다음 com.example.app 패키지에 있는 AppBean을 중복으로 컴포넌트 스캔해볼께요.

 

package com.example.app;

import org.springframework.stereotype.Component;

@Component

public class AppBean {

}

 

그리고 2가지 컴포넌트 스캔이 있습니다.

//@Component를 제외하고 스캔한다. AppBean은 스캔되지 않아야 한다.
@ComponentScan(basePackages = "com.example.app", excludeFilters = @ComponentScan.Filter(type = FilterType.ANNOTATION, classes = Component.class))
//모든 빈을 스캔한다. AppBean이 스캔 되어야 한다.
@ComponentScan(basePackages = "com.example.app")
public class SpringScanApplication {

}

이렇게 중복 정의된 경우 한곳에서 이미 스캔을 하도록 되어 있기 때문에 결과적으로 AppBean은 컴포넌트 스캔의 대상이 됩니다.

 

그러면 이번 예제에서는 어떻게 된 것일까요?

CoreApplicationTests는 스프링 부트를 찾아서 실행하게 됩니다. 테스트 위에 @SpringBootTest라는 애노테이션이 보이실꺼에요.

스프링 부트로 실행하게 되면 @SpringBootApplication 애노테이션이 있는 CoreApplication을 찾아서 설정 파일로 사용하게 됩니다.

그런데 SpringBootApplication 내부에는 @ComponentScan 코드가 있습니다. 참고로 스프링 부트는 편리함을 위해 자동으로 컴포넌트 스캔을 제공합니다.

@ComponentScan은 별도의 코드를 제공하지 않으면 현재 클래스가 있는 패키지 부터 하위 패키지를 모두 컴포넌트 스캔합니다.

따라서 @SpringBootApplication 애노테이션이 있는 곳의 패키지 부터 모든 빈들을 컴포넌트 스캔합니다.

결과적으로 스프링 부트를 통해서 실행하는 경우 이미 @ComponentScan을 통해서 모든 빈들을 읽어버리기 때문에 AutoAppConfig의 컴포넌트 스캔의 excludeFilter 설정은 적용되지 않습니다.

참고로 지금 설명드린 내용은 스프링 부트 강의에서 자세히 설명드리기 때문에 지금은 크게 이해하지 못하셔도 괜찮습니다^^

감사합니다.

Mpels님의 프로필 이미지
Mpels
질문자

감사합니다!

감사합니다. 저도 진짜 궁금했는데 궁금점이 해결됐습니당

감사합니다😃

4

김영한님의 프로필 이미지
김영한
지식공유자

안녕하세요. Mpels님

스프링 부트 3.2 이슈입니다.

다음을 참고해주세요.

자주하는 질문 링크: https://docs.google.com/document/d/1j0jcJ9EoXMGzwAA2H0b9TOvRtpwlxI5Dtn3sRtuXQas/edit#heading=h.b1yk4ued1pxo

 

스프링 부트 3.2 매개변수 이름 인식 문제

스프링 부트 3.2부터 매개변수의 이름을 인식하지 못하는 문제가 있다.

발생하는 예외

스프링 MVC 관련

java.lang.IllegalArgumentException: Name for argument of type [java.lang.String] not specified, and parameter name information not found in class file either.

스프링 핵심 원리 기본편 관련

No qualifying bean of type 'com.example.demo.MemberRepository' available: expected single matching bean but found 2: memoryMemberRepository,memberRepository

스프링 부트 3.2부터 자바 컴파일러에 -parameters 옵션을 넣어주어야 애노테이션의 이름을 생략할 수 있다.

주로 다음 두 애노테이션에서 문제가 발생한다.

@RequestParam, @PathVariable, @Autowired

 

@RequestParam 관련

애노테이션에 username이라는 이름이 명확하게 있다. 문제 없이 작동한다.

@RequestMapping("/request")
public String request(
@RequestParam("username") String username) {
  ...
}

 

애노테이션에 이름이 없다. -parameters 옵션 필요

@RequestMapping("/request")
public String request(
@RequestParam String username) {
  ...
}

 

애노테이션도 없고 이름도 없다. -parameters 옵션 필요

@RequestMapping("/request")
public String request(String username) {
 
...
}

 

@PathVariable 관련

애노테이션에 userId라는 이름이 명확하게 있다. 문제 없이 작동한다.

public String mappingPath(@PathVariable("userId") String userId) {
    ...
}

 

애노테이션에 이름이 없다. -parameters 옵션 필요

@RequestMapping("/request")
public String request(
@RequestParam String username) {
  ...
}

 

@Autowired 관련

MemberRepository를 구현한 빈이 여러개 있다면 다음 코드에서 MemberRepository를 주입 받을 때 변수 이름인 memberRepository를 통해서 memberRepository라는 이름의 스프링 빈을 찾는다. 하지만 매개변수 이름을 사용할 수 없기 때문에 이름으로 대상을 찾을 수 없다.

@Component
public class MemberServiceImpl implements MemberService {

   
private final MemberRepository memberRepository;

   
@Autowired
   
public MemberServiceImpl(MemberRepository memberRepository) {
       
this.memberRepository = memberRepository;
    }
}

 

해결 방안1(권장)

애노테이션에 이름을 생략하지 않고 다음과 같이 항상 적어준다. 이 방법을 권장한다.

@RequestParam("username") String username
@PathVariable("userId") String userId
@Qualifier("memberRepository") MemberRepository memberRepository

 

해결 방안2

컴파일 시점에 -parameters 옵션 적용

1. IntelliJ IDEA에서 File -> Settings를 연다. (Mac은 IntelliJ IDEA -> Settings)
2. Build, Execution, Deployment → Compiler → Java Compiler로 이동한다.
3. Additional command line parameters라는 항목에 다음을 추가한다.
-parameters
4. out 폴더를 삭제하고 다시 실행한다. 꼭 out 폴더를 삭제해야 다시 컴파일이 일어난다.

 

해결 방안3

Gradle을 사용해서 빌드하고 실행한다.

참고로 이 문제는 Build, Execution, Deployment -> Build Tools -> Gradle에서
Build and run using를 IntelliJ IDEA로 선택한 경우에만 발생한다. Gradle로 선택한 경우에는 Gradle이 컴파일 시점에 해당 옵션을 자동으로 적용해준다.

 

문제 원인

공식 링크: https://github.com/spring-projects/spring-framework/wiki/Upgrading-to-Spring-Framework-6.x#parameter-name-retention

자바를 컴파일할 때 매개변수 이름을 읽을 수 있도록 남겨두어야 사용할 수 있다. 컴파일 시점에 -parameters 옵션을 사용하면 매개변수 이름을 사용할 수 있게 남겨둔다.

스프링 부트 3.2 전까지는 바이트코드를 파싱해서 매개변수 이름을 추론하려고 시도했다. 하지만 스프링 부트 3.2 부터는 이런 시도를 하지 않는다.

 

참고로 지금은 해결방안 3번을 선택하시는 것을 권장합니다.

1번 해당 방안을 선택할 경우 다음과 같이 수정해야 합니다.

@Component
public class MemberServiceImpl implements MemberService {

    private final MemberRepository memberRepository;

    @Autowired
    public MemberServiceImpl(@Qualifier("memberRepository") MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

 

@Component
//@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final MemberRepository memberRepository;
    private final DiscountPolicy discountPolicy;

    @Autowired
    public OrderServiceImpl(@Qualifier("memberRepository") MemberRepository memberRepository,
                            @MainDiscountPolicy DiscountPolicy discountPolicy) {
        this.memberRepository = memberRepository;
        this.discountPolicy = discountPolicy;
    }

 

감사합니다.

Mpels님의 프로필 이미지
Mpels
질문자

안녕하세요 영한님 답변 감사드립니다.

우선, 답변 주신 해결방안3을 이용하면 통합 테스트가 성공합니다.

하지만 여전히 의문이 있습니다.

@SpringBootTest
class CoreApplicationTests {

    @Autowired
    ApplicationContext ac;

    @Test
    void contextLoads() {
        AppConfig bean = ac.getBean(AppConfig.class);
        System.out.println(bean);
    }
}

image설정을 하더라도 여전히 AppConfig 는 빈으로 등록되어 출력이 됩니다.

AutoAppConfig 클래스에서 excludeFilters 를 이용하여 컴포넌트 스캔의 대상에서 제외 하였는데, AppConfig 가 빈으로 등록되는 이유를 알 수 있을까요?

안녕하세요 저도 똑같은 문제가 발생하고 질문자님과 똑같이 시도하여 질문남깁니다. 저도 자꾸 AppConfig가 등록되길래 @SpringbootApplication에 내장되어있는 ComponentScan이 AppConfig를 읽는 거 아닐까 생각했는데 AutoAppConfig에서 Exclude해주면 SpringbootApplication은 상관없는건가요??

0

저도 이분과 같은 문제로 되지 않고 있는데 해결방안 가르쳐주시면 감사하겠습니다!

0

안녕하세요, 인프런 AI 인턴입니다.

Mpels님, 스프링 부트에서 NoUniqueBeanDefinitionException 오류는 ApplicationContext에서 요청한 타입의 빈이 2개 이상 있을 때 발생합니다. 오류 메시지에 따르면 MemberRepository 타입의 빈이 2개 존재한다는 것을 알 수 있습니다.

Mpels님의 경우, AppConfig에 정의된 @Bean으로 등록된 MemberRepository와 컴포넌트 스캔을 통해 등록된 MemoryMemberRepository 두 곳에서 MemberRepository 타입의 빈이 생성되었기 때문에 이러한 오류가 발생한 것으로 보입니다.

Mpels님께서 이미 유추하신 것처럼 AppConfig에서 @Bean으로 등록된 객체와 스캔을 통해 자동으로 등록된 객체가 중복되어 발생하는 문제입니다.

AppConfig에서 @Bean으로 정의된 객체를 컴포넌트 스캔에서 제외하는 방법 중 하나는 @ComponentScanexcludeFilters 옵션을 사용하는 것입니다. 하지만, AutoAppConfig에 있는 @ComponentScan에 정의된 excludeFilters에서 오타가 있는 것으로 보여집니다. “xcudeFilters” 대신 “excludeFilters”를 사용하여 올바른 프로퍼티 이름으로 바꾸어야 할 것입니다.

올바르게 설정된 예는 다음과 같습니다.

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

이렇게 설정하면 @Configuration이 붙은 클래스들은 컴포넌트 스캔 대상에서 제외되어 @Bean으로 등록된 객체와 충돌을 피할 수 있습니다.

Mpels님의 추가 질문과 관련하여, MemoryMemberRepositorymemoryMemberRepository로 빈 이름이 변경되면 MemberServiceImpl에서 memberRepository 빈을 주입받으려면 이름을 기반으로 하는 주입(@Qualifier)을 사용하거나, @Primary 어노테이션을 사용해 주입받을 빈을 명시할 수 있습니다. 컴포넌트 이름을 바꾸는 것은 권장하지 않습니다.

이러한 문제를 해결하는 방법은 여러 가지가 있으니, 강의의 코드와 설정을 잘 살펴보고 참고하여 적용해보시기 바랍니다.

Mpels님의 프로필 이미지
Mpels
질문자

AI가 빨강 박스에 겹친 걸 보고 오타로 착각했나보네요.

excludeFilters로 적혀져있습니다.

image

image

Mpels님의 프로필 이미지

작성한 질문수

질문하기