Spring Data JPA Auditing — 생성일, 수정일, 작성자를 자동으로 채우는 방법
모든 테이블에
created_at과updated_at컬럼이 있는데, 매번LocalDateTime.now()를 수동으로 넣고 계신가요? 실수로 빠뜨리거나 잘못된 시간이 들어간 적은 없었나요?
Auditing이란
Spring Data JPA의 Auditing은 엔티티가 생성되거나 수정될 때 생성일, 수정일, 작성자, 수정자 를 자동으로 채워주는 기능입니다. 개발자가 직접 시간을 넣는 코드를 작성할 필요가 없습니다.
데이터베이스 설계에서 감사(Audit) 컬럼은 거의 필수입니다. 장애 추적, 데이터 변경 이력 확인, 비즈니스 로직 검증 등 다양한 곳에서 활용됩니다. 이걸 매번 수동으로 관리하면 누락과 실수가 생기기 마련이고, Auditing은 그 문제를 깔끔하게 해결해 줍니다.
기본 설정
Auditing을 사용하려면 두 가지만 설정하면 됩니다.
1. @EnableJpaAuditing 활성화
@EnableJpaAuditing
@Configuration
public class JpaConfig {
}
이 어노테이션이 없으면 아래의 모든 Auditing 어노테이션이 동작하지 않습니다. 설정을 별도 @Configuration 클래스에 분리하는 것을 권장합니다. @SpringBootApplication 클래스에 직접 붙이면 테스트 시 불필요한 Auditing이 활성화될 수 있습니다.
2. @EntityListeners 등록
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Member {
// ...
}
AuditingEntityListener가 JPA 엔티티의 라이프사이클 이벤트를 감지해서 Auditing 필드를 채워줍니다.
핵심 어노테이션 4가지
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Article {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
| 어노테이션 | 시점 | 설명 |
|---|---|---|
@CreatedDate | persist | 엔티티가 처음 저장될 때 시간 기록 |
@LastModifiedDate | persist + update | 엔티티가 저장되거나 수정될 때 시간 갱신 |
@CreatedBy | persist | 엔티티를 처음 생성한 사용자 |
@LastModifiedBy | persist + update | 마지막으로 수정한 사용자 |
여기서 중요한 포인트가 있습니다. @CreatedDate와 @CreatedBy에는 반드시 @Column(updatable = false)를 추가해야 합니다. 이걸 빠뜨리면 엔티티를 수정할 때 생성일이 현재 시각으로 덮어써질 수 있습니다.
공부하다 보니 이 부분을 놓치는 경우가 꽤 많더라고요. 생성일이 자꾸 바뀌는 버그의 원인이 보통 여기에 있습니다.
지원 타입
@CreatedDate, @LastModifiedDate가 지원하는 타입은 다양합니다.
LocalDateTime— 가장 많이 사용Instant— UTC 기반, 글로벌 서비스에 적합ZonedDateTime— 타임존 정보 포함OffsetDateTime— 오프셋 정보 포함Long/long— 밀리초 타임스탬프java.util.Date— 레거시 호환
실무에서는 LocalDateTime이 가장 보편적이고, 다국어/다타임존 서비스라면 Instant를 고려합니다.
BaseEntity 패턴 — 프로젝트의 기반
모든 엔티티에 Auditing 필드를 반복해서 선언하는 것은 비효율적입니다. @MappedSuperclass를 활용한 BaseEntity 패턴으로 공통 필드를 한 곳에 모읍니다.
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
private LocalDateTime createdDate;
@LastModifiedDate
private LocalDateTime lastModifiedDate;
}
작성자 정보까지 필요한 엔티티를 위해 한 단계 더 분리할 수도 있습니다.
@Getter
@MappedSuperclass
public abstract class BaseEntityWithAuthor extends BaseEntity {
@CreatedBy
@Column(updatable = false)
private String createdBy;
@LastModifiedBy
private String lastModifiedBy;
}
// 사용 — 단순 시간만 필요한 엔티티
@Entity
public class Category extends BaseEntity {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
}
// 사용 — 작성자까지 필요한 엔티티
@Entity
public class Article extends BaseEntityWithAuthor {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String title;
private String content;
}
실무 팁 하나 — BaseEntity는 프로젝트 초기에 만들어두세요. 나중에 추가하려면 기존 엔티티를 전부 수정해야 하고, 테이블에 컬럼도 추가해야 합니다. 처음부터 만들어두면 새 엔티티를 만들 때 extends BaseEntity 한 줄이면 끝납니다.
AuditorAware 구현 — 작성자 자동 채우기
@CreatedBy와 @LastModifiedBy를 사용하려면 "현재 사용자가 누구인지" 알려주는 AuditorAware를 구현해야 합니다.
Spring Security 연동
@Component
public class SecurityAuditorAware implements AuditorAware<String> {
@Override
public Optional<String> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(Authentication::getName);
}
}
인증되지 않은 요청(익명 사용자, 스케줄러, 배치 작업 등)에서는 Optional.empty()를 반환하면 @CreatedBy 필드가 null로 남습니다. 시스템 작업에 "SYSTEM" 같은 기본값을 넣고 싶다면 이렇게 처리합니다.
@Override
public Optional<String> getCurrentAuditor() {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (authentication == null || !authentication.isAuthenticated()
|| authentication instanceof AnonymousAuthenticationToken) {
return Optional.of("SYSTEM");
}
return Optional.of(authentication.getName());
}
사용자 ID를 Long으로 저장하고 싶다면
@Component
public class LongAuditorAware implements AuditorAware<Long> {
@Override
public Optional<Long> getCurrentAuditor() {
return Optional.ofNullable(SecurityContextHolder.getContext())
.map(SecurityContext::getAuthentication)
.filter(Authentication::isAuthenticated)
.map(auth -> {
// CustomUserDetails에서 ID 추출
Object principal = auth.getPrincipal();
if (principal instanceof CustomUserDetails userDetails) {
return userDetails.getId();
}
return null;
});
}
}
이 경우 @CreatedBy, @LastModifiedBy 필드의 타입도 Long으로 맞춰야 합니다.
JPA 콜백(@PrePersist, @PreUpdate)과의 비교
Auditing과 비슷한 일을 순수 JPA 콜백으로도 할 수 있습니다.
@Entity
public class Member {
private LocalDateTime createdDate;
private LocalDateTime lastModifiedDate;
@PrePersist
public void prePersist() {
this.createdDate = LocalDateTime.now();
this.lastModifiedDate = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.lastModifiedDate = LocalDateTime.now();
}
}
두 방식을 비교하면 이렇습니다.
| 기준 | Spring Auditing | JPA 콜백 |
|---|---|---|
| Spring 의존 | 있음 | 없음 (순수 JPA) |
| ** 작성자 자동 채우기** | AuditorAware로 가능 | 직접 SecurityContext 접근 필요 |
| ** 설정** | @EnableJpaAuditing + 어노테이션 | 엔티티에 메서드 직접 작성 |
| ** 테스트** | AuditorAware Mock 가능 | 엔티티 내부 로직 테스트 |
| ** 일관성** | 프레임워크가 보장 | 개발자가 관리 |
Spring 프로젝트라면 Auditing을 사용하는 것이 편리하고, Spring에 의존하지 않는 순수 JPA 모듈이라면 @PrePersist를 사용합니다. 두 방식을 동시에 사용하면 시간이 두 번 설정될 수 있으니 하나만 선택하세요.
테스트에서 Auditing 다루기
테스트 환경에서는 Auditing이 의도치 않게 동작하거나, 반대로 필요한데 동작하지 않을 수 있습니다.
@DataJpaTest에서 Auditing 활성화
@DataJpaTest는 JPA 관련 빈만 로드하기 때문에 @EnableJpaAuditing 설정 클래스를 별도로 import해야 합니다.
@DataJpaTest
@Import(JpaConfig.class) // @EnableJpaAuditing이 선언된 설정 클래스
class ArticleRepositoryTest {
@Autowired
private ArticleRepository articleRepository;
@Test
void 저장_시_생성일이_자동으로_설정된다() {
Article article = new Article("테스트 제목", "테스트 내용");
Article saved = articleRepository.save(article);
assertThat(saved.getCreatedDate()).isNotNull();
assertThat(saved.getLastModifiedDate()).isNotNull();
}
}
AuditorAware Mock
작성자 테스트가 필요하면 AuditorAware를 Mock으로 제공합니다.
@TestConfiguration
class TestAuditConfig {
@Bean
public AuditorAware<String> auditorAware() {
return () -> Optional.of("test-user");
}
}
기억해둘 포인트
- Auditing 설정의 삼총사:
@EnableJpaAuditing+@EntityListeners+ Auditing 어노테이션. @CreatedDate에@Column(updatable = false)빠뜨리면 수정 시 생성일이 덮어써집니다 — 흔한 버그 원인입니다.- BaseEntity 패턴 은 프로젝트 초기에 만들어두는 것이 좋습니다. 나중에 추가하면 기존 엔티티 전부 수정해야 합니다.
AuditorAware는 Spring Security와 자연스럽게 연동되며, 시스템 작업용 기본값 처리를 잊지 마세요.@EnableJpaAuditing은@SpringBootApplication이 아닌 별도@Configuration에 선언하면 테스트가 편해집니다.