Post

Auditing 어노테이션

Auditing 어노테이션

1. Auditing이란


엔티티가 생성되거나 수정된 시간, 그 주체를 자동으로 기록하는 것이 Auditing(감사)이다. 모든 테이블에 created_at, updated_at을 넣는 것은 운영 시 문제 추적을 위해 실무에서 거의 필수로 사용된다. Spring Data JPA의 Auditing 기능을 사용하면 이를 자동으로 처리할 수 있다.


2. 기본 설정


📌 @EnableJpaAuditing 활성화

1
2
3
4
5
6
7
8
// JpaStudyApplication.java
@SpringBootApplication
@EnableJpaAuditing  // 반드시 추가해야 Auditing이 동작함
public class JpaStudyApplication {
    public static void main(String[] args) {
        SpringApplication.run(JpaStudyApplication.class, args);
    }
}

@EnableJpaAuditing이 없으면 @CreatedDate, @LastModifiedDate가 붙은 필드에 값이 채워지지 않는다.

💡 @SpringBootApplication이 붙은 메인 클래스에 @EnableJpaAuditing을 붙이면, @WebMvcTest 같은 슬라이스 테스트에서 JPA 관련 빈을 로딩하지 않아 오류가 발생할 수 있다. 이를 피하려면 별도의 @Configuration 클래스에 @EnableJpaAuditing을 분리하는 것이 좋다.

1
2
3
4
5
// global/config/JpaAuditingConfig.java
@Configuration
@EnableJpaAuditing
public class JpaAuditingConfig {
}

📌 BaseEntity 작성

1
2
3
4
5
6
7
8
9
10
11
12
13
// global/entity/BaseEntity.java
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class) // JPA 이벤트 감지
public abstract class BaseEntity {

    @CreatedDate
    @Column(updatable = false)  // 최초 저장 이후 변경 불가
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

@EntityListeners(AuditingEntityListener.class)가 JPA 이벤트(persist, merge 등)를 감지해서 시간을 자동으로 채워준다. 엔티티가 최초 저장될 때 createdAtupdatedAt이 모두 설정되고, 이후 수정될 때는 updatedAt만 갱신된다.


3. @CreatedDate / @LastModifiedDate 동작 원리


Spring Data JPA의 AuditingEntityListener가 JPA의 콜백 어노테이션(@PrePersist, @PreUpdate)을 내부적으로 활용한다.

  • 엔티티가 처음 저장될 때 (@PrePersist 호출 시점): createdAtupdatedAt을 현재 시간으로 설정
  • 엔티티가 수정될 때 (@PreUpdate 호출 시점): updatedAt만 현재 시간으로 갱신

@Column(updatable = false)createdAt에 붙이는 이유는 Hibernate가 생성하는 UPDATE SQL에 created_at 컬럼이 포함되지 않도록 막기 위해서다.


4. @CreatedBy / @LastModifiedBy


누가 생성하고 수정했는지도 자동으로 기록할 수 있다. 현재 로그인한 사용자 정보를 JPA에 제공하는 AuditorAware 구현체가 필요하다.

📌 AuditorAware 구현

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// global/config/AuditConfig.java
@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
public class AuditConfig {

    @Bean
    public AuditorAware<String> auditorProvider() {
        // Spring Security를 사용하는 경우
        return () -> Optional.ofNullable(SecurityContextHolder.getContext()
                .getAuthentication())
            .filter(Authentication::isAuthenticated)
            .filter(auth -> !(auth instanceof AnonymousAuthenticationToken))
            .map(Authentication::getName);
    }
}

Spring Security를 사용하지 않는 경우는 ThreadLocal에 현재 사용자를 저장하고 꺼내는 방식으로 구현한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Spring Security 없이 ThreadLocal로 구현하는 예시
@Component
public class ThreadLocalAuditorAware implements AuditorAware<String> {

    private static final ThreadLocal<String> currentUser = new ThreadLocal<>();

    public static void setCurrentUser(String userId) {
        currentUser.set(userId);
    }

    public static void clear() {
        currentUser.remove();
    }

    @Override
    public Optional<String> getCurrentAuditor() {
        return Optional.ofNullable(currentUser.get());
    }
}

📌 BaseEntity에 추가

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {

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

    @LastModifiedDate
    private LocalDateTime updatedAt;

    @CreatedBy
    @Column(updatable = false, length = 50)
    private String createdBy;   // 생성한 사용자 ID

    @LastModifiedBy
    @Column(length = 50)
    private String lastModifiedBy; // 마지막으로 수정한 사용자 ID
}

5. JPA 콜백 어노테이션 — @PrePersist, @PostLoad 등


Auditing 외에도 JPA가 제공하는 콜백 어노테이션을 직접 활용할 수 있다. AuditingEntityListener를 쓰지 않고 직접 시간을 채우거나, 엔티티 저장 전후 특정 로직을 실행할 때 사용한다.

어노테이션실행 시점
@PrePersistem.persist() 호출 직전
@PostPersistem.persist() 완료 직후 (flush 후)
@PreUpdateUPDATE SQL 실행 직전
@PostUpdateUPDATE SQL 실행 직후
@PreRemoveem.remove() 호출 직전
@PostRemoveem.remove() 완료 직후
@PostLoad엔티티가 1차 캐시 또는 DB에서 로딩된 직후
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// AuditingEntityListener 없이 직접 콜백 사용하는 예시
@Entity
public class Member extends BaseEntity {
    ...

    @PrePersist
    public void prePersist() {
        // em.persist() 직전에 실행됨
        // createdAt, updatedAt을 직접 세팅하거나
        // 저장 전 유효성 검증 등에 활용
        System.out.println("Member persist 직전 실행");
    }

    @PostLoad
    public void postLoad() {
        // DB에서 로딩된 직후 실행
        // @Transient 필드 초기화 등에 활용
        this.displayLabel = "[" + this.name + "]"; // @Transient 필드
    }
}

6. Auditing 적용 확인


1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
@SpringBootTest
@Transactional
class AuditingTest {

    @Autowired
    private MemberRepository memberRepository;

    @Test
    @DisplayName("저장 시 createdAt, updatedAt 자동 채워짐")
    void auditingTest() throws InterruptedException {
        // 저장
        Member member = new Member("홍길동", 30, "hong@test.com");
        memberRepository.save(member);

        assertThat(member.getCreatedAt()).isNotNull();
        assertThat(member.getUpdatedAt()).isNotNull();
        System.out.println("createdAt: " + member.getCreatedAt());
        System.out.println("updatedAt: " + member.getUpdatedAt());

        LocalDateTime beforeUpdate = member.getUpdatedAt();

        Thread.sleep(100); // 시간 차이를 주기 위해

        // 수정
        member.updateName("김철수");
        memberRepository.saveAndFlush(member);

        // updatedAt만 변경됨
        assertThat(member.getCreatedAt()).isEqualTo(member.getCreatedAt()); // 변경 없음
        assertThat(member.getUpdatedAt()).isAfter(beforeUpdate);             // updatedAt 갱신됨
        System.out.println("수정 후 updatedAt: " + member.getUpdatedAt());
    }
}

참고 자료

This post is licensed under CC BY 4.0 by the author.