엔티티가 생성/수정될 때 시간을 자동으로 기록해주는 JPA Auditing 기능을 이해하자.
매번 createdAt = LocalDateTime.now()를 직접 쓰지 않아도 자동으로 처리되는 원리를 익히자.
| 어노테이션 | 동작 시점 | 이후 변경 여부 |
|---|---|---|
@CreatedDate | 최초 INSERT 시 | 변경 안 됨 |
@LastModifiedDate | INSERT + UPDATE 시 | 수정될 때마다 갱신 |
JPA는 엔티티 생명주기마다 전/후로 실행할 수 있는 이벤트 훅을 제공한다.
| 훅 | 실행 시점 |
|---|---|
@PrePersist | persist() 직전 |
@PostPersist | persist() 직후 |
@PreUpdate | update 직전 |
@PostUpdate | update 직후 |
@PreRemove | remove() 직전 |
@PostRemove | remove() 직후 |
앱 시작 시 JPA가 리스너를 스캔하면서 어떤 훅 메서드가 있는지 전부 기억해뒀다가, 해당 시점이 되면 자동으로 실행한다.
우리가 훅을 직접 구현하지 않아도 되는 이유는, Spring Data JPA가 AuditingEntityListener 안에 이미 구현해뒀기 때문이다.
// AuditingEntityListener 내부 (Spring Data JPA가 만들어둔 것)
public class AuditingEntityListener {
@PrePersist
public void touchForCreate(Object target) {
// @CreatedDate 필드를 찾아서 현재 시간 주입
}
@PreUpdate
public void touchForUpdate(Object target) {
// @LastModifiedDate 필드를 찾아서 현재 시간 주입
}
}
AuditingEntityListener는 6개의 훅 중 @PrePersist와 @PreUpdate만 사용한다.
우리는 @EntityListeners(AuditingEntityListener.class)로 "이 리스너 써줘" 라고 등록만 하면 된다.
앱 시작
→ JPA가 엔티티 클래스 스캔
→ @EntityListeners 확인 → AuditingEntityListener 등록
→ 리스너 안의 @PrePersist, @PreUpdate 붙은 메서드도 기억해둠
→ 이후 해당 시점마다 자동 실행 (매번 확인 X)
em.persist(member) 호출
→ JPA: "PrePersist 시점이다!"
→ 기억해둔 touchForCreate() 실행
→ @CreatedDate 필드에 현재 시간 자동 주입
→ INSERT 쿼리에 시간 포함되어 DB 저장
① 엔티티에 리스너 등록
@EntityListeners(AuditingEntityListener.class)
② 스프링에 Auditing 활성화
@EnableJpaAuditing
@SpringBootApplication
public class Application { ... }
둘 중 하나라도 없으면 시간이 null로 저장된다.
@EntityListeners 없으면 → 리스너 자체가 연결 안 됨@EnableJpaAuditing 없으면 → 리스너가 Bean으로 등록 안 돼서 동작 안 함엔티티마다 @EntityListeners를 붙이는 건 번거롭기 때문에, BaseEntity를 하나 만들어서 상속하는 방식을 사용한다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass // DB 테이블은 안 만들고 필드만 자식 엔티티에 포함
public abstract class BaseEntity {
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
@Entity
public class Member extends BaseEntity { // 상속만 하면 자동 적용
...
}
@Entity
public class Order extends BaseEntity { // 얘도 자동 적용
...
}
@MappedSuperclass는 DB 테이블을 별도로 만들지 않고, 필드만 자식 엔티티 테이블에 포함시켜 주는 어노테이션이다.
@GeneratedValue를 사용하면 JPA가 ID를 자동 생성하기 때문에 새 엔티티인지 판단이 쉽다.
하지만 ID를 직접 할당하는 경우 문제가 생긴다.
// ID를 직접 세팅하는 경우
Member member = new Member();
member.setId("user-001"); // ID 직접 할당
em.persist(member);
JPA는 save() 호출 시 새 엔티티인지 판단하기 위해 ID가 null인지 확인한다.
ID == null → 새 엔티티 → INSERT
ID != null → 기존 엔티티 → SELECT 후 merge (불필요한 SELECT 발생!)
ID를 직접 할당하면 ID가 null이 아니므로, JPA가 기존 엔티티로 오판해서 불필요한 SELECT 쿼리를 날린다.
Persistable 인터페이스를 구현해서 새 엔티티 판단 기준을 직접 정의한다.
@EntityListeners(AuditingEntityListener.class)
@MappedSuperclass
public abstract class BaseEntity implements Persistable<String> {
@CreatedDate
private LocalDateTime createdAt;
@Override
public boolean isNew() {
// createdAt이 null이면 새 엔티티 (아직 한 번도 저장 안 됨)
return createdAt == null;
}
}
save(member) 호출
→ isNew() 실행
→ createdAt == null ? → true (아직 저장 안 된 새 엔티티)
→ SELECT 없이 바로 INSERT 실행
→ @PrePersist → createdAt에 현재 시간 주입
→ 이후 save() 호출 시 createdAt != null → 기존 엔티티로 판단
ID 직접 할당 전략을 쓴다면 Persistable을 구현해서 불필요한 SELECT를 방지하자.
@CreatedDate와 함께 사용하면 isNew() 판단 기준으로 딱 맞아 떨어진다.
@PrePersist 등)은 앱 시작 시 한 번 스캔해서 기억해뒀다가 해당 시점에 자동 실행된다.AuditingEntityListener는 6개 훅 중 @PrePersist, @PreUpdate만 사용하며, Spring이 이미 구현해뒀다. 우리는 등록만 하면 된다.@GeneratedValue) → 기본 동작으로 충분하다.Persistable 구현해서 불필요한 SELECT를 막자.