[오류]UnsatisfiedDependencyException 해결해보기

Inung_92·2023년 10월 27일
1

오류

목록 보기
6/6
post-thumbnail
>김영한 강사님의 JPA 책에 수록된 실습을 수행하면서 Junit5를 통한 테스트에서 겪은 에러 해결과정에 대해서 기록해보겠다.

상황

JPA를 사용하여 SpringMVC 기반의 웹 애플리케이션을 구현하는 실습을 진행하였다. 실습 코드는 생략하고, 테스트를 수행하기 위한 부분만 코드를 보면서 이야기해보자.

코드

JpaShopApplication.class

@SpringBootApplication
public class JpaShopApplication {

    public static void main(String[] args) {
        SpringApplication.run(JpaShopApplication.class, args);
    }
}

MemberRepository.class

@Slf4j
@Repository
public class MemberRepository {
    @Autowired
    private EntityManager entityManager;

    // 저장(영속화)
    public void save(Member member) {
        entityManager.persist(member);
    }

    // 1건 조회
    public Member findOne(Long memberId) {
        return entityManager.find(Member.class, memberId);
    }

    // 여러건 조회
    public List findAll() {
        return entityManager.createQuery("select m from Member m", Member.class)
                .getResultList();
    }

    // 이름으로 조회
    public List findByName(String memberName) {
        return entityManager.createQuery(
                        "select m from Member m where m.memberName = :memberName", Member.class)
                .setParameter("memberName", memberName) // 파라미터 부여
                .getResultList();
    }
}

MemberService.class

@Service
@RequiredArgsConstructor
@Transactional
public class MemberService {
    private final MemberRepository memberRepository;

    public Long join(Member member) {
        // 중복 체크
        validateDuplicateMember(member);
        memberRepository.save(member);
        return member.getId();
    }

    public List findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }

    private void validateDuplicateMember(Member member) {
        List<Member> findMembers = memberRepository.findByName(member.getMemberName());

        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }
}

MemberServiceTest.class

@ExtendWith(SpringExtension.class)
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
@DataJpaTest
class MemberServiceTest {
		// 에러 발생 위치(시작)
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;
		// 에러 발생 위치(끝)
		
		@Test
    @DisplayName("회원가입 테스트")
    void join() throws Exception {
        // Given
        Member member = new Member();
        member.setMemberName("test");

        // When
        Long saveId = memberService.join(member);

        // Then
        Assertions.assertEquals(member, memberService.findOne(saveId)); // NRE 발생
    }	
}

위 코드들을 순서대로보면 애플리케이션 루트 클래스 > 리포지토리 계층 > 서비스 계층 > 테스트 순으로 이루어져 있다. 여기서 에러가 발생한 부분은 테스트 코드에 명시된 부분이 에러가 발생했다.

에러는 다음과 같았다.

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.jpabook.jpashop.service.MemberServiceTest': Unsatisfied dependency expressed through field 'memberService'; nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.jpabook.jpashop.service.MemberService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

@Autowired 어노테이션을 사용하였지만 그에 해당하는 Bean을 찾지 못해서 발생하는 에러였다. 여기서 내가 의아하게 생각했던 부분들은 다음과 같다.

  • 애플리케이션 루트 클래스에 @SpringBootApplication 어노테이션이 명시되어있고, 해당 어노테이션이 컴포넌트 스캔 기능까지 포함하고 있는데 왜 찾지 못할까?
  • @ExtendWith(SpringExtension.class) 어노테이션을 사용하면 Spring 컨텍스트를 활성화하고 테스트에 스프링의 기능을 통합할텐데 왜 정상적으로 작동하지 않을까?
  • @SpringBootTest 어노테이션을 사용하면 해결은 되겠지만 그럼 애플리케이션 환경 자체가 활성화되기 때문에 자원의 소모도 많고 단위 테스트가 의미가 없어진다고 생각했다.

위와 같은 의문점들 때문에 한참 동안을 코드를 들여다보며 왜 안되는지 생각해보았다. 혹시라도 패키지 구조가 문제일지도 몰라서 패키지 구조를 한번 더 확인해보았다. 패키지 구조는 다음과 같다.

패키지 구조를 보아도 크게 문제가 생길만한 상황은 아니었다. 공부를 한 내용을 토대로 한번 생각해보자면 다음과 같은 의문점이 또 생긴다.

  • @ComponentScan@SpringBootApplication 어노테이션에 내부적으로 포함되어 있어 자동으로 컴포넌트를 스캔한다.
  • 컴포넌트 스캔의 범위는 애플리케이션 루트 클래스가 위치한 패키지와 그 하위 패키지를 모두 스캔한다.
  • 해당 프로젝트는 하나의 루트 패키지에 모든 패키지가 위치하기 때문에 패키지가 분리된 지점이 없다.

이런 의문점들 때문에 도저히 생각을 해봐도 해결이 되지 않았다. 왜 컴포넌트 스캔이 정상적으로 동작하지 않을까?

우선 결론부터 말하자면 정확한 이유는 찾지 못했다. @ComponentScan 어노테이션이 내부적으로 동작하는데 방해가 되는 부분이 있는 것 같아 모듈을 재구성해보고, 패키지 구성도 변경해보았지만 소용이 없었다. 그래서 나는 에러에 대한 원인은 찾지 못했지만 다음과 같은 방법으로 문제를 해결했다.

해결

@SpringBootTest 사용

아래 코드는 테스트가 수행되도록 변경한 코드이다.

@ExtendWith(SpringExtension.class) // 생략가능
@SpringBootTest
@Transactional
class MemberServiceTest {
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;

위와 같이 코드를 작성하니 테스트가 가능해졌다. 각 어노테이션의 설명을 보자.

  • @ExtendWith(SpringExtension.class) : 테스트에 Spring의 컨텍스트를 활성화하고 Spring의 기능을 Junit5 테스트에 통합시키는 역할을 한다.
  • @SpringBootTest : 애플리케이션의 통합 테스트를 위한 설정을 제공한다. 즉 애플리케이션 컨텍스트를 활성화하고 필요한 빈들을 자동으로 로드한다.

하지만 이렇게 @SpringBootTest 어노테이션을 사용하면 애플리케이션 컨텍스트 전체를 활성화하기 때문에 비용이 많이 든다. 나는 Service와 연계된 Repository 계층까지만 테스트를 하고 싶었기에 내가 원하는 결과는 아니었다.

@ComponentScan 직접 사용

가장 원초적인 문제인 컴포넌트 스캔을 가능하게 한다면 모든게 해결되지 않을까?

그래서 애플리케이션 루트 클래스에 어노테이션을 직접 명시하기로 했다.

@SpringBootApplication
@ComponentScan(basePackages = "com.example.jpashop") // 명시
public class JpaShopApplication {

    public static void main(String[] args) {
        SpringApplication.run(JpaShopApplication.class, args);
    }
}

그렇다. 이렇게하면 정상적으로 테스트가 수행된다. 근데 내가 왜 이렇게 진작하지 않았느냐?

@SpringBootApplication에서 내부적으로 수행된다고 했기 때문에 다른 부분에서 해결책을 찾았던 것이다. 다른 사람들의 블로그를 수도 없이 들락날락하고, 인프런 질문도 둘러보고, 챗 GPT와 아무리 대화를 해도 @SpringBootApplication@ComponentScan을 같이 사용하는 경우는 프로젝트 패키지가 2개 이상인 경우 밖에 없었다.

근데 왜 나는 프로젝트 패키지가 1개 임에도 불구하고 이렇게 작동하는지 아직도 이유를 모른다. 어쨌든 이렇게 컴포넌트 스캔으로 인해 발생한 에러를 해결하고 테스트를 정상적으로 수행할 수 있었다.

여담으로 혹시라도 Service 계층의 비즈니스 로직만을 테스트하고 싶다면 아래 설명을 참고하자.

@ExtendWith(MockitoExtension.class) // Mokito의 확장 기능 통합
public class MemberServiceTest {
	@Mock
	MemberRepository memberRepository; // 연관이 있는 가짜 객체
	@InjectMocks
	MemberService memberservice; // 테스트 대상

	@Test
    @DisplayName("회원가입 테스트")
    void join() throws Exception {
        // Given
        Member member = new Member();
        member.setMemberName("test");

        // When
        Long saveId = memberService.join(member);

        // Then
        Assertions.assertEquals(member, memberService.findOne(saveId));
    }
}

위 코드와 같이 테스트를 수행하면 MemberRepository가 내부적으로 보유한 save() 메소드는 실행되지 않고, Service 계층의 코드들만 실행된다.


마무리

JPA를 공부하고 Spring과 연계하여 사용하는데에 어려움을 느꼈다. 그래도 차근차근 이어나가고 있는데 Junit5을 사용하다보니 또 다른 어려움을 맞닥들이게 되었다. 그래도 또 꾸역꾸역 이어나가고 있다.

이런 과정에서 잘못된 부분을 유추해보고 찾아보고 개선해보면서 조금씩은 성장하고 있다는 생각이 든다. 하지만 원인에 대해서 핵심 내용을 파악하지 못하고 있는 것은 아닌지 하는 의구심이 드는 경우들이 정말 많다.

이번 포스팅처럼 왜 안되는지 정확한 원인은 찾지 못한채 해결이 된 것만으로도 만족하며 넘어가기에는 상당히 찝찝한 부분이 많다.

그래서 혹시라도 왜 컴포넌트 스캔이 제대로 동작하지 않는건지 알려주실 유능한 개발자가 계시다면 댓글로 알려주셨으면 좋겠다.😂

그럼 이만.👊🏽

profile
서핑하는 개발자🏄🏽

0개의 댓글