문제 상황
프로젝트를 하던 중, 리포지터리 단계의 테스트를 할 때 다음과 같은 문제가 발생했었습니다.
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와는 적합하지 않은 조합인 것이었습니다.
일례로 지금 MemberMissionsRepositoryImpl은 필드로 MemberMissionsJpaRepository 인터페이스 (스프링 데이터 JPA 인터페이스)를 이용하여 메서드들을 수행하고 있지만, 만약 JPA가 아닌 방식으로 리포지터리 메서드를 구현할 경우에는 JPA를 전혀 사용하지 않겠죠? 그래서 @DataJpaTest를 MemberMissionsRepository에 접목시키는 행위는 아예 맞지 않는 것이었습니다.
해결 방법
해결 방법으로는 JPA가 아닌 방식으로 바뀔 수도 있는 MemberMissionsRepository를 불러오는 게 아니라, 스프링 데이터 JPA인 MemberMissionsJpaRepository를 불러오도록 해야 합니다. 그래야 공식 문서에 기술되어 있듯이 "JPA에 관한 컴포넌트"를 불러오는 것과 의미가 같기 때문입니다.
@DataJpaTest를 사용할 때 이렇게 빈 관련 예외가 발생하면 혹시 JPA와 관련 없는 컴포넌트를 불려 오려고 했던 것은 아닐지 고민해 보도록 합시다!
Reference
'✨ 프로젝트 > ATWOZ' 카테고리의 다른 글
[ATWOZ] FCM 알림 기능 개발 기록기 (3) - 안드로이드 에뮬레이터로 알림 검증하기 (0) | 2024.08.01 |
---|---|
[ATWOZ] FCM 알림 기능 개발 기록기 (2) - FCM 토큰 관리 방법 및 스프링 코드 설명 (0) | 2024.07.31 |
[ATWOZ] FCM 알림 기능 개발 기록기 (1) - FCM 도입 이유와 아키텍처 구조 (0) | 2024.07.30 |
[ATWOZ] Call to 'list.containsAll(collection)' may have poor performance 문제 개선하기 (1) | 2024.05.10 |