티켓 예매 프로젝트의 공통 패키지를 세팅하면서, 모든 엔티티가 상속할 BaseEntity 에 생성일과 수정일을 자동으로 채우도록 JPA Auditing을 설정하게 되었다.
이번 글에서 다룰 개념은 다음과 같다.
@EnableJpaAuditing
@EntityListeners(AuditingEntityListener.class)
@CreatedDate, @LastModifiedDate
JpaAuditingConfig (별도 @Configuration)
@MockitoBean (테스트 쪽 우회 방법)
처음 설정할 때 헷갈리는 지점은 @EnableJpaAuditing 을 어디에 붙이느냐다.
검색하면 메인 클래스에 바로 붙이는 예제가 많고, 애플리케이션도 그대로 잘 실행된다. 그런데 컨트롤러 슬라이스 테스트인 @WebMvcTest 를 돌리면 컨텍스트 로딩부터 실패한다.
핵심은 어노테이션 자체가 아니라, 메인 클래스에 붙은 설정은 모든 슬라이스 테스트에도 함께 따라온다는 점이다.
간단히 정리하면
- @EnableJpaAuditing : 애플리케이션 전체에 JPA Auditing 기능을 켠다.
- AuditingEntityListener : 엔티티 저장/수정 시점에 Auditing 필드 값을 채운다.
- @CreatedDate 등 : 어떤 필드에 어떤 값을 채울지 표시한다.
- JpaAuditingConfig : @EnableJpaAuditing 만 담은 별도 설정 클래스다.
- @MockitoBean : 테스트에서 문제가 되는 빈을 Mock으로 대체한다.
애플리케이션 실행만 고려 → 메인 클래스에 붙여도 동작한다.
@WebMvcTest 를 함께 쓴다 → 별도 @Configuration 으로 분리한다.
@DataJpaTest 에서 필요하다 → @Import(JpaAuditingConfig.class) 로 가져온다.
@EnableJpaAuditing
@EnableJpaAuditing 은 애플리케이션에서 JPA Auditing 기능을 활성화할 때 사용한다.
예를 들어 예매, 공연, 회원 엔티티마다 createdAt = LocalDateTime.now() 를 직접 넣는 대신, 저장 시점에 값을 자동으로 채우고 싶을 때 쓴다.
어노테이션 내부를 보면 다음과 같이 JpaAuditingRegistrar 를 import 해서 Auditing에 필요한 빈을 등록한다.
@Import(JpaAuditingRegistrar.class)
public @interface EnableJpaAuditing {
String auditorAwareRef() default "";
boolean setDates() default true;
boolean modifyOnCreate() default true;
String dateTimeProviderRef() default "";
}
setDates 는 생성일/수정일을 채울지, modifyOnCreate 는 생성 시점도 수정으로 볼지 정한다. 둘 다 기본값이 true 라서 날짜만 쓴다면 속성을 따로 줄 필요가 없다.
auditorAwareRef 는 @CreatedBy, @LastModifiedBy 에 넣을 사용자 정보를 제공하는 AuditorAware 빈이 여러 개일 때, 어떤 빈을 쓸지 지정한다.
즉, @EnableJpaAuditing 은 Auditing을 "켜는 스위치"이고, 엔티티에서 값을 채우는 시점은 리스너가 잡아 준다.
@EntityListeners와 @CreatedDate
AuditingEntityListener 는 엔티티가 저장되거나 수정되는 시점에 Auditing 필드를 채울 때 사용한다.
Spring Data JPA 공식 문서는 이 리스너를 orm.xml 에 전역으로 등록하거나, 엔티티에 @EntityListeners(AuditingEntityListener.class) 로 등록하는 두 가지 방법을 안내한다.
프로젝트에서는 다음과 같이 공통 부모 클래스에 엔티티 단위로 등록했다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(name = "created_at", nullable = false, updatable = false)
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "updated_at", nullable = false)
private LocalDateTime updatedAt;
}
@MappedSuperclass 로 선언했기 때문에 이 클래스를 상속한 엔티티는 두 컬럼과 리스너를 함께 물려받는다. createdAt 은 updatable = false 로 두어 수정 쿼리에서 바뀌지 않게 했다.
즉, @EnableJpaAuditing 이 기능을 켜고, @EntityListeners 가 엔티티에 리스너를 연결하고, @CreatedDate 가 채울 필드를 표시한다.
중요한 점은 공식 문서가 이 세 가지를 한 세트로 안내한다는 것이다. 필드에 @CreatedDate 만 붙인다고 Auditing이 동작한다고 기대하면 안 된다.
JpaAuditingConfig
JpaAuditingConfig 는 Auditing 설정을 메인 클래스에서 떼어 내, 슬라이스 테스트에 어떤 설정이 올라갈지 제어할 때 사용한다.
예를 들어 처음에는 다음과 같이 메인 클래스에 붙이는 방식을 떠올렸다.
@EnableJpaAuditing
@SpringBootApplication
public class TicketRunApplication {
// ...
}
애플리케이션 실행에는 문제가 없다. 하지만 이 상태에서 컨트롤러 하나만 올리는 @WebMvcTest 를 돌리면 컨텍스트 로딩이 실패한다.
아직 예매 도메인 컨트롤러가 없어서, 확인용으로 /ping 만 응답하는 최소 컨트롤러를 만들어 실행했다.
@WebMvcTest(PingController.class)
class PingControllerTest {
// 컨텍스트 로딩 단계에서 실패한다.
}
Spring Boot 4.1.1 에서 실행하면 다음과 같은 원인이 찍힌다.
Error creating bean with name 'jpaAuditingHandler':
Cannot resolve reference to bean 'jpaMappingContext'
Error creating bean with name 'jpaMappingContext':
JPA metamodel must not be empty
Auditing 빈은 JPA 메타모델(엔티티 정보)을 필요로 하는데, @WebMvcTest 는 웹 계층만 올리기 때문에 JPA 관련 구성이 없어서 생기는 에러다.
그렇다면 왜 웹 테스트에서 Auditing 빈을 만들려고 했을까?
Spring Boot 공식 문서의 User Configuration and Slicing 항목이 이 상황을 설명한다. 메인 클래스는 테스트의 설정 소스이기 때문에, 거기에 붙은 @Enable* 설정은 모든 슬라이스 테스트에서도 활성화된다.
문서는 @EnableMongoAuditing 을 예로 들고, 해당 설정을 메인 클래스와 같은 레벨의 별도 @Configuration 클래스로 옮기는 방법을 권장한다.
// @SpringBootApplication 에 두면 JPA 빈이 없는 @WebMvcTest 슬라이스 테스트가 실패하므로 별도 설정으로 분리한다.
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}
분리하고 나면 같은 @WebMvcTest 가 통과한다. 같은 문서에 "Test slices exclude @Configuration classes from scanning."이라고 적혀 있듯이, 슬라이스 테스트는 일반 @Configuration 클래스를 스캔하지 않기 때문이다.
@WebMvcTest 가 스캔하는 대상은 @Controller, @ControllerAdvice 와 Filter, WebMvcConfigurer, SecurityFilterChain 같은 웹 관련 빈으로 제한된다.
즉, 메인 클래스는 피할 수 없이 항상 읽히지만, 별도 설정 클래스는 슬라이스 테스트가 골라서 뺄 수 있다.
다만, @DataJpaTest 처럼 Auditing이 필요한 슬라이스 테스트도 있다. @DataJpaTest 도 컴포넌트 스캔을 JPA 리포지토리와 @Entity 로 제한하므로, 공식 문서가 안내하는 대로 @Import 로 가져와야 할 것으로 보인다.
@DataJpaTest
@Import(JpaAuditingConfig.class)
class ReservationRepositoryTest {
// 필요한 테스트에서만 Auditing 설정을 가져온다.
}
@MockitoBean
추가적으로 메인 클래스에 그대로 두고 테스트 쪽에서 막는 방법도 알아보려고 한다!
검색해 보면 이 방식을 해결책으로 소개하는 글도 많아서, 분리 방식과 비교하려고 함께 실행해 보았다.
에러 원인이 jpaMappingContext 빈이므로, 테스트에서 이 빈을 Mock으로 대체하면 컨텍스트가 올라간다. Spring Boot 4.0 에서 @MockBean 이 제거되었기 때문에 @MockitoBean 을 쓴다.
@WebMvcTest(PingController.class)
@MockitoBean(types = JpaMetamodelMappingContext.class)
class PingControllerTest {
// 메인 클래스에 @EnableJpaAuditing 이 있어도 통과한다.
}
4.1.1 에서 실행해 보니 이 방식도 통과했다. 다만 이 Mock은 테스트 대상과 상관없는 우회 코드이고, @WebMvcTest 를 새로 만들 때마다 잊지 않고 붙여야 한다.
즉, 동작은 하지만 원인(설정 위치)을 그대로 두고 증상만 막는 방법에 가깝다고 생각한다.
정리
세 가지 방식은 모두 엔티티의 생성일/수정일을 자동으로 채우기 위한 것이다.
차이는 설정을 어디에 두느냐에 따라 테스트에 미치는 범위가 달라진다는 점이다.
| 방식 | 애플리케이션 실행 | @WebMvcTest |
테스트별 선택 |
메인 클래스에 @EnableJpaAuditing |
정상 | 실패 | 불가 (모든 슬라이스에 적용) |
메인 클래스 + 테스트마다 @MockitoBean |
정상 | 통과 (매번 추가) | 불가 (모든 슬라이스에 적용) |
별도 JpaAuditingConfig |
정상 | 통과 | 가능 (@Import) |
나는 설정을 어디에 둘지 고민될 때 다음 기준으로 판단한다.
애플리케이션 전역 설정인가? → 메인 클래스
특정 영역(JPA, 보안) 설정인가? → 별도 @Configuration 으로 분리
특정 테스트에서만 필요한가? → @Import 로 가져온다'Back-End > Spring' 카테고리의 다른 글
| [OAuth 2.0] OAuth 2.0은 무엇이고 왜 사용할까 (0) | 2026.10.23 |
|---|---|
| [Spring Security] Spring Security는 왜 사용할까? (0) | 2026.10.23 |
| [JPA] N+1 문제란 무엇일까? (0) | 2025.12.02 |
| @RequestParam, @ModelAttribute, @RequestBody는 언제 사용할까? (0) | 2025.11.24 |
| [JPA] @ManyToOne, @OneToMany 연관관계 정리 (0) | 2025.11.22 |