모든 테이블에 created_atupdated_at 컬럼이 있는데, 매번 LocalDateTime.now()를 수동으로 넣고 계신가요? 실수로 빠뜨리거나 잘못된 시간이 들어간 적은 없었나요?

Auditing이란

Spring Data JPA의 Auditing은 엔티티가 생성되거나 수정될 때 생성일, 수정일, 작성자, 수정자 를 자동으로 채워주는 기능입니다. 개발자가 직접 시간을 넣는 코드를 작성할 필요가 없습니다.

데이터베이스 설계에서 감사(Audit) 컬럼은 거의 필수입니다. 장애 추적, 데이터 변경 이력 확인, 비즈니스 로직 검증 등 다양한 곳에서 활용됩니다. 이걸 매번 수동으로 관리하면 누락과 실수가 생기기 마련이고, Auditing은 그 문제를 깔끔하게 해결해 줍니다.

기본 설정

Auditing을 사용하려면 두 가지만 설정하면 됩니다.

1. @EnableJpaAuditing 활성화

JAVA
@EnableJpaAuditing
@Configuration
public class JpaConfig {
}

이 어노테이션이 없으면 아래의 모든 Auditing 어노테이션이 동작하지 않습니다. 설정을 별도 @Configuration 클래스에 분리하는 것을 권장합니다. @SpringBootApplication 클래스에 직접 붙이면 테스트 시 불필요한 Auditing이 활성화될 수 있습니다.

2. @EntityListeners 등록

JAVA
@EntityListeners(AuditingEntityListener.class)
@Entity
public class Member {
    // ...
}

AuditingEntityListener가 JPA 엔티티의 라이프사이클 이벤트를 감지해서 Auditing 필드를 채워줍니다.

핵심 어노테이션 4가지

JAVA
@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;
}
어노테이션시점설명
@CreatedDatepersist엔티티가 처음 저장될 때 시간 기록
@LastModifiedDatepersist + update엔티티가 저장되거나 수정될 때 시간 갱신
@CreatedBypersist엔티티를 처음 생성한 사용자
@LastModifiedBypersist + update마지막으로 수정한 사용자

여기서 중요한 포인트가 있습니다. @CreatedDate@CreatedBy에는 반드시 @Column(updatable = false)를 추가해야 합니다. 이걸 빠뜨리면 엔티티를 수정할 때 생성일이 현재 시각으로 덮어써질 수 있습니다.

공부하다 보니 이 부분을 놓치는 경우가 꽤 많더라고요. 생성일이 자꾸 바뀌는 버그의 원인이 보통 여기에 있습니다.

지원 타입

@CreatedDate, @LastModifiedDate가 지원하는 타입은 다양합니다.

  • LocalDateTime — 가장 많이 사용
  • Instant — UTC 기반, 글로벌 서비스에 적합
  • ZonedDateTime — 타임존 정보 포함
  • OffsetDateTime — 오프셋 정보 포함
  • Long / long — 밀리초 타임스탬프
  • java.util.Date — 레거시 호환

실무에서는 LocalDateTime이 가장 보편적이고, 다국어/다타임존 서비스라면 Instant를 고려합니다.

BaseEntity 패턴 — 프로젝트의 기반

모든 엔티티에 Auditing 필드를 반복해서 선언하는 것은 비효율적입니다. @MappedSuperclass를 활용한 BaseEntity 패턴으로 공통 필드를 한 곳에 모읍니다.

JAVA
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdDate;

    @LastModifiedDate
    private LocalDateTime lastModifiedDate;
}

작성자 정보까지 필요한 엔티티를 위해 한 단계 더 분리할 수도 있습니다.

JAVA
@Getter
@MappedSuperclass
public abstract class BaseEntityWithAuthor extends BaseEntity {

    @CreatedBy
    @Column(updatable = false)
    private String createdBy;

    @LastModifiedBy
    private String lastModifiedBy;
}
JAVA
// 사용 — 단순 시간만 필요한 엔티티
@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 연동

JAVA
@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" 같은 기본값을 넣고 싶다면 이렇게 처리합니다.

JAVA
@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으로 저장하고 싶다면

JAVA
@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 콜백으로도 할 수 있습니다.

JAVA
@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 AuditingJPA 콜백
Spring 의존있음없음 (순수 JPA)
** 작성자 자동 채우기**AuditorAware로 가능직접 SecurityContext 접근 필요
** 설정**@EnableJpaAuditing + 어노테이션엔티티에 메서드 직접 작성
** 테스트**AuditorAware Mock 가능엔티티 내부 로직 테스트
** 일관성**프레임워크가 보장개발자가 관리

Spring 프로젝트라면 Auditing을 사용하는 것이 편리하고, Spring에 의존하지 않는 순수 JPA 모듈이라면 @PrePersist를 사용합니다. 두 방식을 동시에 사용하면 시간이 두 번 설정될 수 있으니 하나만 선택하세요.

테스트에서 Auditing 다루기

테스트 환경에서는 Auditing이 의도치 않게 동작하거나, 반대로 필요한데 동작하지 않을 수 있습니다.

@DataJpaTest에서 Auditing 활성화

@DataJpaTest는 JPA 관련 빈만 로드하기 때문에 @EnableJpaAuditing 설정 클래스를 별도로 import해야 합니다.

JAVA
@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으로 제공합니다.

JAVA
@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에 선언하면 테스트가 편해집니다.
댓글 로딩 중...