도입
JPA 엔티티를 다루다 보면 거의 모든 테이블에 비슷한 칼럼이 반복됩니다. 대표적으로 created_at, updated_at, created_by, updated_by 같은 필드들입니다.
이 값을 서비스 코드에서 매번 수동으로 세팅하면 금방 중복이 생기고, 일부 엔티티에서는 누락되기 쉽습니다. Spring Data JPA의 auditing 기능은 바로 이 반복을 줄이기 위해 존재합니다. 그 중심에 있는 것이 AuditingEntityListener이고, 엔티티에 이 리스너를 연결하는 가장 흔한 표현이 바로 @EntityListeners(AuditingEntityListener.class)입니다.
필요성
감사 메타데이터를 서비스 코드에서 직접 채우면 중복이 빠르게 늘어납니다. 신규 엔티티에서는 누락되기 쉽고, 수정 로직이 여러 서비스에 흩어져 있으면 규칙도 금방 어긋납니다.
반면 auditing 리스너를 쓰면 “엔티티가 저장되거나 수정될 때 감사 필드를 채운다”는 규칙을 persistence 계층으로 끌어내릴 수 있습니다. 이 덕분에 비즈니스 로직은 핵심 도메인 규칙에 더 집중하고, 감사 정보는 공통 인프라로 다룰 수 있습니다.
- 모든 테이블에 생성일/수정일 칼럼이 거의 공통으로 들어가는 경우
- 작성자와 수정자를 보안 컨텍스트에서 자동 추출하고 싶은 경우
- BaseEntity 패턴으로 공통 필드를 묶어 재사용하려는 경우
- 감사 메타데이터를 서비스 코드가 아니라 JPA 계층에서 일관되게 처리하고 싶은 경우
정의
먼저 `@EntityListeners`는 JPA 표준 어노테이션입니다. Jakarta Persistence 문서는 이 어노테이션이 엔티티 클래스나 매핑 슈퍼클래스에 적용될 수 있고, 콜백 리스너 클래스를 지정한다고 설명합니다.
여기에 Spring Data JPA가 제공하는 `AuditingEntityListener`를 연결하면, 해당 엔티티는 감사 메타데이터를 자동 채울 수 있는 대상이 됩니다. `AuditingEntityListener` Javadoc도 이 클래스를 “persisting and updating entities” 시점의 auditing 정보를 포착하는 JPA entity listener라고 설명합니다.
@EntityListeners(AuditingEntityListener.class)는 “이 엔티티의 생명주기 이벤트에 감사 메타데이터 자동 채우기 로직을 붙인다”는 선언으로 이해하면 가장 정확합니다.핵심 원리
Spring Data JPA auditing 문서는 `@CreatedBy`, `@LastModifiedBy`, `@CreatedDate`, `@LastModifiedDate`를 사용해 누가 만들었는지, 누가 수정했는지, 언제 변경되었는지를 기록할 수 있다고 설명합니다.
즉, 이 리스너를 붙인다고 해서 magically 모든 게 끝나는 것은 아니고, 엔티티에 어떤 감사 메타데이터를 둘지 먼저 선언해야 합니다. 리스너는 그 선언된 필드들을 적절한 시점에 채워 주는 실행 엔진에 가깝습니다.
기본 구조
| 구성 요소 | 역할 | 실무 포인트 |
|---|---|---|
| `@EntityListeners` | 엔티티에 리스너 연결 | JPA 표준, 엔티티/매핑 슈퍼클래스에 적용 가능 |
| `AuditingEntityListener` | persist/update 시 감사 정보 채우기 | Spring Data JPA가 제공하는 구현체 |
| `@CreatedDate`, `@LastModifiedDate` | 생성일, 수정일 추적 | Instant, Date, Calendar, long/Long 등에 적용 가능 |
| `@CreatedBy`, `@LastModifiedBy` | 생성자, 수정자 추적 | `AuditorAware` 구현이 필요 |
| `@EnableJpaAuditing` | 감사 인프라 활성화 | 설정 클래스에서 켜 줘야 함 |
| `AuditorAware` | 현재 사용자/주체 제공 | Spring Security와 자주 연결됨 |
| `DateTimeProvider` | 현재 시각 제공 | 테스트나 타임존 제어가 필요할 때 커스터마이징 가능 |
기본 구현
@Entity
@EntityListeners(AuditingEntityListener.class)
public class Post {
@Id
@GeneratedValue
private Long id;
@CreatedDate
private Instant createdAt;
@LastModifiedDate
private Instant updatedAt;
@CreatedBy
private String createdBy;
@LastModifiedBy
private String updatedBy;
}
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.of("system");
}
}
이 구조가 가장 기본입니다. 엔티티는 “어떤 필드를 감사 대상으로 볼지”를 선언하고, 설정은 “감사 인프라를 실제로 켠다”는 책임을 집니다.
패턴 1. persist / update 시점 동작
`AuditingEntityListener` Javadoc은 이 클래스가 persisting and updating entities의 auditing 정보를 포착한다고 설명합니다. 메서드 설명도 `touchForCreate`는 persist events에서 생성일과 수정일, auditor를 설정하고, `touchForUpdate`는 update events에서 수정 관련 정보를 설정한다고 설명합니다.
즉, 실무 감각으로 보면 생성 시점에는 created 계열과 modified 계열이 함께 채워질 수 있고, 수정 시점에는 last modified 계열이 갱신됩니다. 이 동작 덕분에 애플리케이션 서비스가 `entity.setUpdatedAt(now())` 같은 코드를 매번 쓸 필요가 줄어듭니다.
저장 시점 흐름
1) 애플리케이션이 엔티티를 persist/save 한다
2) JPA 생명주기 안에서 auditing listener가 호출된다
3) created/modified 관련 필드를 채운다
4) DB에 반영된다
수정 시점 흐름
1) 엔티티를 update/save 한다
2) auditing listener가 수정 시점 메타데이터를 갱신한다
3) DB에 반영된다
패턴 2. AuditorAware 와 사용자 추적
공식 문서는 `@CreatedBy`와 `@LastModifiedBy`를 사용하는 경우 auditing infrastructure가 현재 principal을 알아야 한다고 설명하고, 이를 위해 `AuditorAware<T>` SPI를 구현해야 한다고 설명합니다.
반대로 생성일과 수정일만 추적하는 경우에는 `AuditorAware`가 필수는 아닙니다. Spring Data JPA 문서도 “creation and modification dates만 추적하는 애플리케이션은 AuditorAware가 필수는 아니다”라고 설명합니다. 그래서 단순히 `createdAt`, `updatedAt`만 자동화할 때는 더 가볍게 시작할 수 있습니다.
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
패턴 3. BaseEntity 와 임베디드 메타데이터
`@EntityListeners`는 엔티티뿐 아니라 `@MappedSuperclass`에도 붙일 수 있습니다. 그래서 실무에서는 공통 필드를 가진 BaseEntity를 만들고, 거기에 auditing 설정을 몰아넣는 방식이 자주 쓰입니다.
또한 공식 문서는 auditing metadata가 반드시 루트 엔티티 최상위 필드에 있을 필요는 없고, embedded one에도 둘 수 있다고 설명합니다. 즉, `AuditMetadata` 같은 값 객체를 두고 그 안에 `@CreatedDate`, `@CreatedBy`를 모으는 설계도 가능합니다.
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
protected Instant createdAt;
@LastModifiedDate
protected Instant updatedAt;
}
@Embeddable
public class AuditMetadata {
@CreatedBy
private String createdBy;
@CreatedDate
private Instant createdAt;
}
이 패턴의 장점은 감사 메타데이터 구조를 더 깔끔하게 재사용할 수 있다는 점입니다. 특히 large domain model에서는 auditing 필드를 흩뿌리기보다 묶어서 보는 편이 훨씬 낫습니다.
`@EnableJpaAuditing`와 세부 옵션
`@EnableJpaAuditing`은 말 그대로 JPA auditing을 어노테이션 기반 설정으로 활성화하는 스위치입니다. 이 어노테이션을 빼먹으면 엔티티에 리스너와 감사 필드를 선언해도 기대대로 동작하지 않는 경우가 많습니다.
공식 API 문서는 이 어노테이션의 optional elements로 `auditorAwareRef`, `dateTimeProviderRef`, `setDates`, `modifyOnCreate`를 제공합니다. 즉, 현재 사용자 빈을 명시적으로 지정할 수도 있고, 날짜 생성 전략을 바꿀 수도 있으며, 생성 시 수정일도 함께 찍을지 같은 정책을 조정할 수 있습니다.
| 옵션 | 의미 | 기본값/의도 |
|---|---|---|
| `auditorAwareRef` | 현재 사용자 조회용 `AuditorAware` 빈 지정 | 명시적으로 어떤 빈을 쓸지 지정 |
| `dateTimeProviderRef` | 시간 제공자 빈 지정 | 테스트나 시간 정책 통제에 유용 |
| `setDates` | 생성/수정 날짜 자동 세팅 여부 | 기본 true |
| `modifyOnCreate` | 생성 시 수정 필드도 함께 표시할지 | 기본 true |
한계와 주의점
이 기능은 “누가 만들고 언제 수정했는가” 정도의 기술적 메타데이터에는 매우 잘 맞습니다. 하지만 실제 감사(audit) 요구사항이 더 무거운 경우, 예를 들어 변경 전/후 값 전체를 기록하거나 승인 이력까지 남기고 싶다면 별도 감사 로그 구조가 필요할 수 있습니다.
또한 `createdBy`와 `updatedBy`는 현재 보안 컨텍스트나 시스템 계정에 의존합니다. 배치 작업, 스케줄러, 비동기 이벤트 처리처럼 사용자 문맥이 약한 환경에서는 기본 구현만으로는 기대와 다를 수 있습니다.
- 감사 메타데이터 자동화와 완전한 감사 로그 시스템을 같은 것으로 보지 않기
- 비동기/배치 실행에서 현재 사용자 문맥이 어떻게 채워지는지 확인하기
- 시간대, 테스트 시간 고정, 운영 환경 시계 기준을 함께 고려하기
- BaseEntity를 무작정 모든 도메인에 강제하지 않기
자주 하는 실수
- 리스너는 붙였지만 auditing 인프라는 활성화하지 않음
@CreatedBy,@LastModifiedBy를 쓰면서AuditorAware를 제공하지 않음- 감사 필드를 BaseEntity에 넣었지만 실제 엔티티가 그 구조를 상속/포함하지 않음
- 생성 시 수정일 필드가 함께 채워지는 기본 동작을 모르고 버그로 오해함
- 시간 필드 타입과 DB 컬럼 타입 매핑을 충분히 확인하지 않음
- 이 기능만으로 변경 이력 전체 추적이 된다고 착각함
실무 루틴
- 먼저 날짜만 추적할지, 사용자까지 추적할지 범위를 정한다.
- 설정 클래스에
@EnableJpaAuditing를 켠다. - 사용자 추적이 필요하면
AuditorAware를 구현한다. - 엔티티별 선언인지 BaseEntity인지 임베디드 메타데이터인지 구조를 결정한다.
- 시간 타입과 DB 컬럼 매핑 정책을 점검한다.
- 테스트에서 실제로 created/modified 필드가 채워지는지 검증한다.
디버깅
@EnableJpaAuditing가 실제 설정 클래스에 켜져 있는지 먼저 확인한다.@EntityListeners(AuditingEntityListener.class)가 선언되어 있는지 본다.@CreatedBy / @LastModifiedBy를 쓴다면 AuditorAware가 실제 값을 주는지 확인한다.DateTimeProvider와 시스템 시계, 타임존을 함께 본다.체크리스트
- auditing 인프라 활성화 여부
- 엔티티 리스너 선언 여부
- 감사 필드 어노테이션 선언 여부
- AuditorAware 값 공급 여부
- 실제 persist/update 이벤트 발생 여부
- 시간 공급자와 타입 매핑 상태
요약
- ✅ `@EntityListeners`는 JPA 표준 리스너 연결 어노테이션이다.
- ✅ `AuditingEntityListener`는 Spring Data JPA가 제공하는 감사용 JPA listener다.
- ✅ 생성일/수정일은 `@CreatedDate`, `@LastModifiedDate`로 선언한다.
- ✅ 생성자/수정자는 `@CreatedBy`, `@LastModifiedBy`와 `AuditorAware`로 연결한다.
- ✅ `@EnableJpaAuditing`을 켜지 않으면 기대대로 동작하지 않을 수 있다.
- ✅ 감사 메타데이터는 루트 엔티티 필드나 임베디드 객체에 둘 수 있다.
- ✅ BaseEntity 패턴과 함께 쓰면 반복 선언을 줄이기 좋다.
- ✅ 이 기능은 편리하지만, 완전한 변경 이력 감사 시스템을 대체하는 것은 아니다.