본문 바로가기
✨ 프로젝트/ATWOZ

[ATWOZ] @DataJpaTest와 No qualifying bean of type '...' available: expected at least 1 bean which qualifies as autowired 오류의 상관관계

by dev_writer 2024. 3. 14.

문제 상황

프로젝트를 하던 중, 리포지터리 단계의 테스트를 할 때 다음과 같은 문제가 발생했었습니다.

 
No qualifying bean of type '...' available: expected at least 1 bean which qualifies as autowired.. 와 같은 문제가 발생한 것인데요, 코드를 어떻게 짰었는지 알려드리겠습니다.
 

MemberMissionsJpaRepositoryTest

문제가 발생한 지점인 MemberMissionsJpaRepositoryTest 코드입니다. @DisplayNameGeneration은 언더 바(_)를 공백으로 치환할 때 썼고, @SuppressWarnings은 한글에 대해 경고 줄이 뜨지 않도록 하기 위해 사용하였습니다.
 
@DataJpaTest는 JPA에 관한 빈만을 띄우기 위해 사용하였습니다.

// import 표현은 생략

@DisplayNameGeneration(DisplayNameGenerator.ReplaceUnderscores.class)
@SuppressWarnings("NonAsciiCharacters")
@DataJpaTest
public class MemberMissionsJpaRepositoryTest {

    @Autowired
    private MemberMissionsRepository memberMissionsRepository;

    @Test
    void 회원_미션_목록_생성() {
        // given
        MemberMissions memberMissions = 멤버_미션들_생성();

        // when
        MemberMissions result = memberMissionsRepository.save(memberMissions);

        // then
        assertThat(result).usingRecursiveComparison()
                .ignoringFields("createdAt")
                .ignoringFields("updatedAt")
                .isEqualTo(memberMissions);
    }

    @Test
    void 회원_미션_목록_조회() {
        // given
        Long memberId = 1L;
        MemberMissions memberMissions = 멤버_미션들_생성();
        MemberMissions saveMemberMissions = memberMissionsRepository.save(memberMissions);

        // when
        Optional<MemberMissions> result = memberMissionsRepository.findByMemberId(memberId);

        // then
        assertSoftly(softly -> {
            softly.assertThat(result).isPresent();
            softly.assertThat(result.get()).usingRecursiveComparison()
                    .ignoringFields("createdAt")
                    .ignoringFields("updatedAt")
                    .isEqualTo(saveMemberMissions);
        });
    }
}

 

MemberMissionsRepository

MemberMissionsRepository를 추상화하였습니다. 실제 운영에서 쓰일 구현체와 테스트 환경에서 쓰일 구현체 (mock)가 다를 것이기 때문입니다.

// import 표현은 생략

public interface MemberMissionsRepository {

    MemberMissions save(MemberMissions memberMissions);
    Optional<MemberMissions> findByMemberId(Long memberId);
}

 
 

MemberMissionsRepositoryImpl

데이터베이스와 맞물려 있는 MemberMissionsRepository의 구현체 중 하나입니다. 빈 등록을 하였고 (@Repository), 아래에 작성한 MemberMissionsJpaRepository를 필드로 가져 JPA의 로직을 실행하도록 하였습니다.

// import 표현은 생략

@RequiredArgsConstructor
@Repository
public class MemberMissionsRepositoryImpl implements MemberMissionsRepository {

    private final MemberMissionsJpaRepository memberMissionsJpaRepository;

    @Override
    public MemberMissions save(final MemberMissions memberMissions) {
        memberMissionsJpaRepository.save(memberMissions);

        return memberMissions;
    }

    @Override
    public Optional<MemberMissions> findByMemberId(final Long memberId) {
        return memberMissionsJpaRepository.findByMemberId(memberId);
    }
}

 

MemberMissionsJpaRepository

스프링 데이터 JPA 인터페이스를 의미합니다.

// import 표현은 생략

public interface MemberMissionsJpaRepository extends JpaRepository<MemberMissions, Long> {

    MemberMissions save(final MemberMissions memberMissions);

    Optional<MemberMissions> findByMemberId(final Long memberId);
}

 

기존의 제 생각

@DataJpaTest로 리포지터리에 대한 빈을 불러왔고, 그렇기 때문에 @Autowired 또한 문제가 없는 것으로 당연하게 생각했었습니다.
 

문제점 파악

하지만, @DataJpaTest를 잘못 이해했었음을 파악하였습니다.
 
지금 MemberMissionsJpaRepositoryTest에서 불러온 것을 보면, MemberMissionsRepository 인터페이스를 의존합니다. 그리고 이것의 구현체인 MemberMissionsRepositoryImpl이 호출된다고 생각을 했었으나, @DataJpaTest는 완전히 JPA에 대한 컴포넌트만 불러오기 때문에 MemberMissionsRepository와는 적합하지 않은 조합인 것이었습니다.
 

DataJpaTest의 문서

 
일례로 지금 MemberMissionsRepositoryImpl은 필드로 MemberMissionsJpaRepository 인터페이스 (스프링 데이터 JPA 인터페이스)를 이용하여 메서드들을 수행하고 있지만, 만약 JPA가 아닌 방식으로 리포지터리 메서드를 구현할 경우에는 JPA를 전혀 사용하지 않겠죠? 그래서 @DataJpaTest를 MemberMissionsRepository에 접목시키는 행위는 아예 맞지 않는 것이었습니다.
 

해결 방법

해결 방법으로는 JPA가 아닌 방식으로 바뀔 수도 있는 MemberMissionsRepository를 불러오는 게 아니라, 스프링 데이터 JPA인 MemberMissionsJpaRepository를 불러오도록 해야 합니다. 그래야 공식 문서에 기술되어 있듯이 "JPA에 관한 컴포넌트"를 불러오는 것과 의미가 같기 때문입니다.

JPA와 연결된 컴포넌트인 MemberMissionsJpaRepository로 바꿉니다.

 
@DataJpaTest를 사용할 때 이렇게 빈 관련 예외가 발생하면 혹시 JPA와 관련 없는 컴포넌트를 불려 오려고 했던 것은 아닐지 고민해 보도록 합시다!
 

Reference