Auditing — 생성일·수정자를 자동으로 기록하는 방법
모든 엔티티에 생성일과 수정일을 기록하고 싶은데, 매번 수동으로 설정해야 할까요?
대부분의 서비스에서 데이터가 언제, 누구에 의해 생성되고 수정되었는지 추적하는 것은 기본적인 요구사항입니다. Spring Data JPA의 Auditing 기능을 사용하면 이런 메타데이터를 자동으로 기록할 수 있습니다.
개념 정의
Auditing 은 엔티티의 생성 시각, 수정 시각, 생성자, 수정자를 자동으로 관리 해주는 Spring Data의 기능입니다. JPA의 @PrePersist, @PreUpdate 이벤트를 기반으로 동작합니다.
설정 방법
1. @EnableJpaAuditing 활성화
@Configuration
@EnableJpaAuditing
public class JpaConfig {
}
2. BaseTimeEntity 생성
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
3. 엔티티에서 상속
@Entity
public class Member extends BaseTimeEntity {
@Id @GeneratedValue
private Long id;
private String name;
}
이제 Member를 저장하면 createdDate와 lastModifiedDate가 자동으로 채워집니다.
핵심 어노테이션
| 어노테이션 | 역할 | 값 설정 시점 |
|---|---|---|
| @CreatedDate | 생성 시각 기록 | persist() 전 |
| @LastModifiedDate | 최종 수정 시각 기록 | persist() / merge() 전 |
| @CreatedBy | 생성자 기록 | persist() 전 |
| @LastModifiedBy | 최종 수정자 기록 | persist() / merge() 전 |
@MappedSuperclass
@MappedSuperclass는 테이블과 매핑되지 않는 부모 클래스입니다. 이 클래스의 필드는 자식 엔티티의 테이블에 직접 컬럼으로 포함됩니다.
@EntityListeners
@EntityListeners(AuditingEntityListener.class)는 JPA 엔티티 이벤트를 감지하여 Auditing 로직을 실행하는 리스너입니다.
AuditorAware — 생성자/수정자 기록
시각 정보 외에 누가 작업했는지도 기록하려면 AuditorAware를 구현해야 합니다.
@Component
public class AuditorAwareImpl implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
// Spring Security에서 현재 사용자 정보 가져오기
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()) {
return Optional.empty();
}
return Optional.of(authentication.getName());
}
}
BaseEntity 확장
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
설정에 AuditorAware 등록
@Configuration
@EnableJpaAuditing
public class JpaConfig {
@Bean
public AuditorAware<String> auditorProvider() {
return new AuditorAwareImpl();
}
}
실무 패턴: BaseEntity 분리
모든 엔티티가 생성자/수정자 정보를 필요로 하지는 않습니다. 그래서 보통 ** 두 단계로 분리 **합니다.
// 시간 정보만 필요한 엔티티용
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
// 시간 + 사용자 정보가 필요한 엔티티용
@MappedSuperclass
public abstract class BaseEntity extends BaseTimeEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
// 로그성 데이터 — 시간만 필요
@Entity
public class AccessLog extends BaseTimeEntity { ... }
// 비즈니스 데이터 — 시간 + 사용자
@Entity
public class Order extends BaseEntity { ... }
JPA 이벤트를 직접 사용하는 방법
Auditing 없이 JPA 이벤트 콜백을 직접 사용할 수도 있습니다.
@Entity
public class Member {
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
@PrePersist
public void prePersist() {
LocalDateTime now = LocalDateTime.now();
this.createdDate = now;
this.lastModifiedDate = now;
}
@PreUpdate
public void preUpdate() {
this.lastModifiedDate = LocalDateTime.now();
}
}
하지만 Spring Data Auditing을 사용하는 것이 코드 중복을 줄이고 일관성을 유지하는 데 더 효과적입니다.
Hibernate Envers — 변경 이력 추적
생성일/수정일을 넘어서 ** 모든 변경 이력 **을 추적해야 한다면 Hibernate Envers를 고려할 수 있습니다.
설정
// build.gradle
implementation 'org.springframework.data:spring-data-envers'
@Entity
@Audited
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
private int age;
}
@Audited를 붙이면 member_AUD라는 이력 테이블이 자동 생성되고, INSERT/UPDATE/DELETE 시 해당 시점의 데이터 스냅샷이 저장됩니다.
이력 조회
@Repository
public interface MemberRepository extends JpaRepository<Member, Long>,
RevisionRepository<Member, Long, Long> {
// findRevisions(), findLastChangeRevision() 등 사용 가능
}
// 특정 엔티티의 모든 리비전 조회
Revisions<Long, Member> revisions = memberRepository.findRevisions(memberId);
revisions.forEach(revision -> {
Member member = revision.getEntity();
RevisionMetadata<Long> metadata = revision.getMetadata();
System.out.println("리비전: " + metadata.getRevisionNumber()
+ ", 시점: " + metadata.getRevisionInstant()
+ ", 이름: " + member.getName());
});
Envers 사용 시 주의사항
- 이력 테이블이 추가되므로 ** 저장 공간이 증가 **합니다.
- 모든 변경마다 이력이 기록되므로 ** 쓰기 성능에 영향 **을 줄 수 있습니다.
- 특정 필드만 추적하려면
@NotAudited로 제외할 수 있습니다.
주의할 점
@EnableJpaAuditing이 테스트에서 문제를 일으킬 수 있다
@EnableJpaAuditing을 @SpringBootApplication 클래스에 직접 붙이면, @DataJpaTest에서 AuditorAware 빈이 없어서 에러가 발생할 수 있습니다. 별도의 @Configuration 클래스에 분리하는 것이 안전합니다.
@CreatedDate 필드에 updatable = false를 빠트리면 수정 시 덮어씌워진다
@CreatedDate 필드에 @Column(updatable = false)를 설정하지 않으면, 엔티티가 수정될 때 ** 생성일이 현재 시각으로 갱신 **될 수 있습니다.
Envers는 저장 공간과 쓰기 성능에 영향을 준다
@Audited를 붙이면 모든 변경마다 이력 테이블에 INSERT가 추가됩니다. 변경이 잦은 엔티티에 적용하면 ** 이력 테이블이 급격히 커지고 쓰기 성능이 저하 **됩니다.
정리
| 항목 | 설명 |
|---|---|
| @EnableJpaAuditing | AuditingEntityListener로 생성일/수정일 자동 기록 |
| AuditorAware | 생성자/수정자 정보를 제공하는 인터페이스 |
| BaseEntity 분리 | BaseTimeEntity(시간) + BaseEntity(시간+사용자) |
| @MappedSuperclass | 테이블 없이 매핑 정보만 상속 |
| Hibernate Envers | 전체 변경 이력 추적 (저장 공간/성능 트레이드오프) |