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에 로드된다.
- 어떤 클래스가 해당하는지는 spring-boot 공식문서 에 있음 (ㅠㅠ?!)
- @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으로 자동 등록되기 때문.
(더 자세한 내용은 포스팅 참고!)
[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으로 만들어지고 어떤 클래스가 안 만들어지는지 파악하기 어려워서 실제로 잘 쓰일까 의문이 드는 기능이다.
'데이터베이스' 카테고리의 다른 글
[DB / 테스트] @DataJpaTest (0) | 2024.11.26 |
---|---|
[DB / H2] spring.jpa.properties.hibernate.globally_quoted_identifiers=true 설정 (0) | 2024.11.26 |
[DB / Error] Caused by: org.h2.jdbc.JdbcSQLSyntaxErrorException: Syntax error in SQL statement "\000d\000a (0) | 2024.11.25 |
[DB] Error / engine[*]=InnoDB"; expected "identifier";] (0) | 2024.11.24 |
[DB / MySQL] Index의 동작 원리 (0) | 2024.07.11 |