[실전 지식] JpaAudit이란 무엇인가

이세준·2025년 12월 2일
post-thumbnail

? JpaAudit 에 대해서 기초부터 알려줄래?


1. JpaAudit이란 무엇인가

JpaAudit(JPA Auditing) 은 엔티티가 언제 생성되고 수정되었는지, 그리고 누가 만들고 수정했는지를 자동으로 기록해주는 기능이다.

보통 서비스 개발에서는 거의 모든 테이블에 다음 컬럼이 공통으로 존재한다.

  • created_at
  • updated_at
  • created_by
  • updated_by

이걸 매번 서비스 로직에서 일일이 넣어줬다면, 코드가 지저분해지고 유지보수가 어렵다.
이를 JPA가 자동으로 처리하도록 하는 기능이 Auditing이다.


2. 왜 필요한가

(1) 중복 코드 제거

서비스, 컨트롤러에서 매번 LocalDateTime.now()를 넣을 필요가 없다.

(2) 실수 방지

업데이트할 때 updated_at을 깜빡한다든지, created_by를 잘못 넣는 실수를 줄일 수 있다.

(3) 일관성 유지

프로젝트 전반에서 동일한 규칙으로 생성/수정 정보를 기록할 수 있다.


3. 어떻게 동작하는가

JPA Auditing은 크게 두 가지 요소로 구성된다.

  1. Auditing을 활성화하는 설정
  2. Auditing 전용 어노테이션이 붙은 엔티티

3-1. Auditing 활성화

Spring Boot에서 보통 다음 설정 하나면 끝난다.

@EnableJpaAuditing
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

여기서 @EnableJpaAuditing 이 중요하다.
이걸 통해 JPA가 엔티티의 생성·수정 시점을 감지하고 값을 넣어줄 수 있다.


3-2. Audit 정보를 담는 필드 선언

보통 추상 클래스를 하나 만들어 상속한다.

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {

    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

여기서 중요한 어노테이션:

  • @CreatedDate: 엔티티가 처음 저장될 때 자동으로 값 삽입
  • @LastModifiedDate: 엔티티가 변경될 때마다 자동으로 갱신
  • @MappedSuperclass: 이 클래스를 상속하는 엔티티가 컬럼을 가져감
  • @EntityListeners(AuditingEntityListener.class): JPA가 변경 이벤트를 감지하기 위함

3-3. 엔티티에 적용

예를 들면 Product 엔티티에 적용하면 다음과 같다.

@Entity
public class Product extends BaseTimeEntity {

    @Id @GeneratedValue
    private Long id;

    private String name;
}

이제 Product를 저장하면:

  • created_at → 자동 입력
  • updated_at → created_at과 동일하게 자동 입력

수정하면:

  • updated_at 만 갱신

4. created_by / updated_by까지 넣으려면?

유저 정보까지 넣으려면 @CreatedBy, @LastModifiedBy를 사용한다.

@CreatedBy
private String createdBy;

@LastModifiedBy
private String updatedBy;

하지만 스프링이 알아서 사용자 정보를 알 수는 없으므로,
반드시 AuditorAware 구현체를 만들어서 “현재 로그인한 사용자”를 알려줘야 한다.

예시:

@Component
public class AuditorAwareImpl implements AuditorAware<String> {
    @Override
    public Optional<String> getCurrentAuditor() {
        // 예: Spring Security에서 username 가져오기
        return Optional.of("hello22433");
    }
}

5. 실무에서의 일반적인 구조

5-1. 시간을 위한 기본 클래스

@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTime {
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

5-2. 사용자 정보까지 포함한 클래스

@MappedSuperclass
public abstract class BaseEntity extends BaseTime {
    @CreatedBy
    private String createdBy;

    @LastModifiedBy
    private String updatedBy;
}

5-3. 엔티티에서 상속

@Entity
public class Order extends BaseEntity {
    ...
}

6. 주의할 점

(1) @EnableJpaAuditing 은 반드시 있어야 한다.

설정하지 않으면 어노테이션이 있어도 자동 입력이 안 된다.

(2) created_by, updated_by는 Security와 연동해야 진짜 유용해진다.

(3) @CreatedDate는 최초 1번만 입력된다. 수정해도 바뀌지 않는다.

반면 @LastModifiedDate는 수정할 때마다 바뀐다.

(4) DB에서는 대부분 datetime, timestamp와 매핑된다.


7. 요약

  • JpaAudit은 엔티티의 생성·수정 시간 및 작성자 정보 자동 기록 기능
  • @EnableJpaAuditing + Base Entity 클래스 + 어노테이션으로 동작
  • created_at, updated_at 은 거의 모든 테이블에서 필수
  • 실무에서 중복 코드 제거, 변경 추적, 실수 방지에 매우 유용

? 좀더 심화내용 알려줄래?

아래는 실무에서 바로 써먹을 수 있는 JPA Auditing 심화 내용이다.
기본 개념을 이미 이해한 상태라 가정하고, 실제 프로젝트에서 중요한 포인트 중심으로 정리해줄게.


1. createdAt/updatedAt의 정확한 시점과 JPA 이벤트 흐름

JPA Auditing은 JPA의 Entity Lifecycle Callback을 사용한다.

즉 다음 시점에 각각 호출된다.

어노테이션동작 시점
@CreatedDate@PrePersist 직전에 호출
@LastModifiedDate@PreUpdate 직전에 호출
@CreatedBy@PrePersist
@LastModifiedBy@PreUpdate

따라서 중요한 특징이 생긴다.

(1) update 쿼리 없이 업데이트되지 않음

엔티티를 수정했더라도 변경 감지가 일어나지 않으면 update 쿼리가 안 나가므로
@LastModifiedDate도 갱신되지 않는다.

예)

order.setStatus(order.getStatus());

값이 같으면 dirty checking이 발생하지 않아 updatedAt도 갱신되지 않는다.

(2) flush 시점에 반영

Auditing 값은 영속성 컨텍스트에 세팅되지만, DB 반영은 flush 시점에 일어난다.
트랜잭션 경계를 명확히 알고 있어야 한다.


2. @EnableJpaAuditing 등록 위치에 따른 동작 차이

많은 실수 중 하나가 @EnableJpaAuditing을 @Configuration 클래스에 등록하는 경우인데,
그 자체는 문제 없지만 테스트 환경에서는 다음 문제가 발생한다.

문제 1) @DataJpaTest 에서는 자동 스캔이 안 될 수 있음

@DataJpaTest는 Application 전체를 로드하지 않기 때문에
Auditing 설정을 따로 넣어야 한다.

해결:

@DataJpaTest
@Import(TestJpaConfig.class)
class RepositoryTest { }

3. createdBy / updatedBy와 Security 연동 심화

가장 실무적인 방식: SecurityContextHolder에서 username 꺼내기

@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        Authentication auth = SecurityContextHolder.getContext().getAuthentication();

        if (auth == null || !auth.isAuthenticated()) {
            return Optional.empty();
        }
        return Optional.of(auth.getName());
    }
}

하지만 실제 서비스에서는 다음 문제가 발생한다.


문제 A. 비동기 요청에서는 SecurityContext가 전달되지 않음

ThreadLocal 기반이라 @Async에서는 SecurityContextHolder가 비어있다.

해결

SecurityContextPropagation을 해야 한다.

@EnableAsync
@Configuration
public class AsyncConfig implements AsyncConfigurer {

    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextExecutor(Executors.newFixedThreadPool(8));
    }
}

문제 B. JWT 인증에서는 username을 항상 꺼낼 수 있는게 아님

이 경우 Request Filter에서 직접 SecurityContext에 넣어줘야 한다.


문제 C. Batch / Scheduler 같은 환경에서는 createdBy가 null이 될 수 있음

보통 시스템 계정(system)으로 설정한다.

return Optional.of("SYSTEM");

4. Soft Delete(삭제 상태 컬럼)와 Auditing 결합하기

대부분의 실제 서비스는 delete 쿼리를 사용하지 않고,
deleted_at, deleted_by, is_deleted 같은 컬럼을 사용한다.

예)

@Entity
public class Post extends BaseEntity {

    private boolean deleted = false;

    private LocalDateTime deletedAt;

    private String deletedBy;

    public void softDelete(String username) {
        this.deleted = true;
        this.deletedAt = LocalDateTime.now();
        this.deletedBy = username;
    }
}

Auditing으로 삭제 시간과 삭제자를 자동화하고 싶다면?

JPA Auditing만으로는 delete 이벤트를 감지하는 기능이 없다.
@PreRemove는 엔티티가 삭제되기 직전에만 호출된다.
Soft Delete는 remove를 호출하지 않으므로 동작하지 않는다.

해결 방법

  1. 직접 Soft Delete 메서드 제공
  2. @SQLDelete + @Where 적용하는 Hibernate 기능 활용
  3. Querydsl BooleanBuilder로 필터링

5. LocalDateTime 말고 OffsetDateTime/Instant 쓰는 이유

실무에서는 LocalDateTime은 권장하지 않는다.

이유

타임존 정보가 없기 때문이다.

문제 예시)
서버는 UTC, DB는 Asia/Seoul, 애플리케이션은 로컬 LocalDateTime을 사용한다면
병합 시 혼란을 일으킬 수 있다.

추천 방식

  • AWS, 클라우드 환경: Instant 또는 OffsetDateTime(UTC)
  • 여러 국가 서비스: OffsetDateTime 필수

Auditing 적용 예)

@CreatedDate
private Instant createdAt;

@LastModifiedDate
private Instant updatedAt;

6. DB 트리거 vs JPA Auditing 비교

구분JPA AuditingDB Trigger
관리 위치애플리케이션데이터베이스
장점코드로 관리, 테스트 가능앱 로직과 상관없이 항상 보장
단점앱 밖에서 변경된 데이터는 감지 못함DB마다 설정 방식이 다름
선호도MSA 환경에서는 선호금융권·레거시는 트리거 선호

결론

단일 서비스 → JPA Auditing
DB가 여러 곳에서 수정될 수 있는 환경 → 트리거


7. Immutable 엔티티에서 Auditing 사용하기

예를 들어 아래처럼 값 객체 스타일 엔티티는 setter가 없다.

@Entity
public class Post {

    @Id @GeneratedValue
    private Long id;

    private final String title;

    @CreatedDate
    private LocalDateTime createdAt;
}

JPA Auditing은 필드에 직접 접근하므로 setter 없어도 괜찮다.
하지만 final 필드에는 값을 대입할 수 없기 때문에
Audit 필드는 final로 만들면 안 된다.


8. Bulk update 시 Auditing 무시됨

중요한 문제.

다음 코드는 updated_at이 전혀 갱신되지 않는다.

@Modifying
@Query("UPDATE Product p SET p.status = :status")
void bulkUpdateStatus(String status);

이유
Bulk Update는 JPA가 관리하는 영속성 컨텍스트를 건너뛴다.
따라서 @LastModifiedDate가 동작할 수 없다.

해결 전략

  1. 직접 updatedAt 값도 업데이트하도록 쿼리에 포함
  2. Bulk 작업을 최소화
  3. Batch Job에서는 Auditing을 포기하고 작업 시간 자체를 기록

9. BaseEntity를 나누는 전략

실무에서는 BaseEntity를 여러 개로 분리한다.

기본 시간만 필요한 경우

BaseTimeEntity
(createdAt, updatedAt)

시간 + 사용자 정보까지 필요한 경우

BaseEntity
(createdAt, updatedAt, createdBy, updatedBy)

SoftDelete까지 필요한 경우

DeletableEntity
(+ deletedAt, deletedBy, isDeleted)


10. 정리

Auditing은 단순히 createdAt과 updatedAt을 넣어주는 기능을 넘어서서
실제 서비스에서는 다음을 고려해야 한다.

  • update 시점의 dirty checking 규칙
  • SecurityContext 전달 문제
  • Scheduler/Batch 환경의 Auditor 처리
  • Soft Delete와 결합 구조
  • TimeZone/Instant 사용
  • Bulk Update에서 Auditing이 무시되는 문제
  • BaseEntity 계층화 전략
  • DB 트리거와의 역할 분배

모두 제대로 알고 있어야 문제 없이 실무에서 활용할 수 있다.

profile
기술정리

0개의 댓글