데이터베이스

[DB / Error] @DataJpaTest 사용 시 org.springframework.beans.factory.UnsatisfiedDependencyException 에러

syj0522 2024. 11. 26. 15:00

SpringBoot + Jpa + In-memory h2 환경에서 간단한 레포지토리 테스트를 수행하던 중 Bean 의존성 문제가 발생했다.

에러

더보기

org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'QueryDslEventRepositoryTest':
Unsatisfied dependency expressed through field 'jf':
No qualifying bean of type 'cohttp://m.querydsl.jpa.impl.JPAQueryFactory' available:
expected at least 1 bean which qualifies as autowire candidate. Dependency annotations:
{@org.springframework.beans.factory.annotation.Autowired(required=true)}

에러 발생 환경

//Repository Test 
@DataJpaTest
@TestPropertySource(locations = "classpath:application.properties")
class QueryDslEventRepositoryTest {
    @Autowired
    private JPAQueryFactory jf;
    @Autowired
    private EntityManager em;
    @Autowired
    private QueryDslEventRepository queryDslEventRepository;

    @DisplayName("이벤트가 저장된다")
    @Test
    void findAll_nullLastEventId() {
        Event event = Event.of( "Event 1", LocalDateTime.now(), "location1", "address1", "author1", "email1", "content1", 1, 1, Availability.Y);
        queryDslEventRepository.save(event);
    }
}

코드가 실행되려면 JpaQueryFactory를 주입받아야 하는데 Bean으로 등록되어있지 않아서 발생하는 문제인 것 같다.

//JPAConfig
@Configuration
public class JPAConfig {
    @PersistenceContext
    private EntityManager em;

    @Bean
    public JPAQueryFactory queryFactory(){
        return new JPAQueryFactory(em);
    }
}

JPAConfig파일 내에 JpaQueryFactory를 Bean으로 등록해두었는데 왜 이런 에러가 뜨는 걸까?

아래는 엔티티, 레포지토리 코드이다.

//Entity
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
@Builder
public class Event {

    @Id
    @Column(name = "event_id")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "name", length = 20)
    private String name;

    @Column(name = "event_date")
    private LocalDateTime eventDate;

    @Column(name = "location", length = 20)
    private String location;

    @Column(name = "address", length = 45)
    private String address;

    @Column(name = "author", length = 20)
    private String author;

    @Column(name = "email",  length = 40)
    private String email;

    @Column(name = "content", length = 200)
    private String content;

    @Column(name = "capacity")
    private int capacity;

    @Column(name = "applicants")
    private int applicants;

    @Enumerated(EnumType.STRING)
    @Column(name = "availability")
    private Availability availability;

    @Column(name = "created_date")
    private LocalDateTime createdDate;

    @Column(name = "last_modified_date")
    private LocalDateTime lastModifiedDate;

    public static Event of(String name, LocalDateTime eventDate, String location, String address, String author, String email, String content, int capacity, int applicants, Availability availability){
        return new Event(null, name, eventDate, location, address, author, email, content, capacity, applicants, availability, LocalDateTime.now(), LocalDateTime.now());
    }

}
//Repository
@RequiredArgsConstructor
@Repository
@Slf4j
public class QueryDslEventRepository implements EventRepository{
    private final JPAQueryFactory qf;
    private final EntityManager em;

    public void save(Event event){
        em.persist(event);
    }
    //...
}

원인을 찾던 중, 테스트 클래스에서 @DataJpaTest를 지우고 @SpringBootTest @Transectional을 붙이면 테스트가 통과되는 것을 발견했다. @DataJpaTest 문제인 것 같다. 작동 원리를 알고 에러의 원인을 찾아보자.

원인

@DataJpaTest를 사용하면

  • auto-configure 목록에 있는 클래스(JPA, DB 관련 클래스)만 ApplicationContext에 로드된다.
  • @Component로 설정된 클래스는 ApplicationContext에 로드되지 않는다.

결론적으로 @Component로 설정된 JPAQueryFactory, QueryDslEventRepository의 Bean이 생성되지 않았기 때문에 의존주입이 실패하여 발생된 문제이다.

@Autowired
private JPAQueryFactory jf; //Bean으로 등록되어있지 않아서 주입 불가
@Autowired
private EntityManager em; //EntityManager은 왜 등록되어있나?
@Autowired
private QueryDslEventRepository queryDslEventRepository; //Bean으로 등록되어있지 않아서 주입 불가

* 세 객체를 각각 확인해보았는데, EntityManager는 의존 주입이 가능했다. (ㅠㅠ?!)

* auto-configure 목록에 있는 org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration 클래스에 의해 EntityManager가 Bean으로 자동 등록되기 때문.

 

(더 자세한 내용은 포스팅 참고!)

https://oigie.tistory.com/165

 

[DB] @DataJpaTest

@DataJpaTest는 JPA, 데이터베이스 관련 설정만 최소한으로 로드하여 레포지토리/엔티티를 테스트하는 어노테이션이다.  임베디드 데이터베이스를 자동 구성해주기 때문에 외부 데이터베이스를

oigie.tistory.com

해결

테스트할 때 @Import를 사용해서 강제로 필요한 클래스를 ApplicationContext에 로드한다.

//전
@DataJpaTest
@TestPropertySource(locations = "classpath:application.properties")
class QueryDslEventRepositoryTest {
    //...
}
//후
@DataJpaTest
@Import({JPAConfig.class, QueryDslEventRepository.class}) //(1)
@ActiveProfiles("test") //(2)
class QueryDslEventRepositoryTest {
    //...
}

(1)에 의존 주입이 필요한 클래스를 수동으로 Import해주었다. 

-> 에러 해결


(2)는 참고사항인데, 설정 파일이 여러개일 때 각자 프로필을 지정할 수 있다.

  • @ActiveProfiles의 프로필 지정 기능이 @TestPropertySource보다 더 간결하고 직관적이라서 이 부분도 바꾸어보았다.
spring:
    config:
         active:
            on-profile: "test"
  • 이렇게 프로필 이름을 지정해주고 @ActiveProfiles에 무엇을 쓸 건지 명시해주면 더 간단히 구현할 수 있다.
  • 참고로 Spring Boot 2.3 이후로 spring.profiles.active가 spring.config.activate.on-profile로 마이그레이션되었다.

https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-Config-Data-Migration-Guide

 

Spring Boot Config Data Migration Guide

Spring Boot helps you to create Spring-powered, production-grade applications and services with absolute minimum fuss. - spring-projects/spring-boot

github.com

 

spring:

  # 테스트 설정파일 프로필 지정
  config:
    active:
      on-profile: test

  datasource:
    url: jdbc:h2:mem:testdb
    driverClassName: org.h2.Driver
    username: sa
    password:

  jpa:
    hibernate:
      ddl-auto: update
    database-platform: org.hibernate.dialect.H2Dialect
    properties:
      hibernate:
        globally_quoted_identifiers: true
        format_sql: true
    show-sql: true

logging:
  level:
    org.springframework.orm.jpa: DEBUG
    org.springframework.transaction: DEBUG

Note

이번 에러로 인해 @DataJpaTest의 내부적인 동작을 자세히 알게 되었다. 목적은 임베디드 DB를 쓰고, 트랜잭션을 자동화하고, JPA와 DB 관련된 Bean만 스캔해서 테스트를 가볍게 만들자는 취지인데, 어떤 클래스가 Bean으로 만들어지고 어떤 클래스가 안 만들어지는지 파악하기 어려워서 실제로 잘 쓰일까 의문이 드는 기능이다.