@Getter
@SuperBuilder
@MappedSuperclass
@NoArgsConstructor(access = PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@EqualsAndHashCode(onlyExplicitlyIncluded = true)
public class BaseEntity {
@CreatedDate
@Column(name = "created_at")
private LocalDateTime createdAt;
@LastModifiedDate
@Column(name = "modified_at")
private LocalDateTime modifiedAt;
}
created_at 컬럼과 수정된 일시를 저장하는 modified_at 컬럼이 별도 코드를 작성하지 않아도 자동으로 세팅 되어 저장됨@CreatedDate와 @LastModifiedDate 덕분에 자동 세팅 되는 것@CreatedDate와 @LastModifiedDate 동작 분석Spring Data JPA의 Auditing 기능은 @CreatedDate와 @LastModifiedDate를 자동으로 감지하고, 엔티티의 생성 및 수정 시 해당 필드 값을 채움
🔍 엔티티 감지 및 이벤트 트리거
package org.springframework.data.jpa.domain.support;
import jakarta.persistence.PrePersist;
import jakarta.persistence.PreUpdate;
import org.aspectj.lang.JoinPoint;
import org.aspectj.runtime.reflect.Factory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Configurable;
import org.springframework.beans.factory.aspectj.AbstractDependencyInjectionAspect;
import org.springframework.beans.factory.aspectj.AnnotationBeanConfigurerAspect;
import org.springframework.beans.factory.aspectj.ConfigurableObject;
import org.springframework.data.auditing.AuditingHandler;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@Configurable
public class AuditingEntityListener implements ConfigurableObject {
// ... 중략
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null");
if (this.handler != null) {
AuditingHandler object = (AuditingHandler)this.handler.getObject();
if (object != null) {
object.markCreated(target); // AuditingHandler의 markCreated() 호출
}
}
}
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null");
if (this.handler != null) {
AuditingHandler object = (AuditingHandler)this.handler.getObject();
if (object != null) {
object.markModified(target); // AuditingHandler의 markModified() 호출
}
}
}
}
AuditingEntityListener 클래스는 JPA 이벤트 리스너AuditingEntityListener는 JPA 엔티티의 변경 사항을 감지하고, @CreatedDate 및 @LastModifiedDate를 자동으로 설정하는 역할 등을 함@PrePersist, @PreUpdate) JPA 이벤트 리스너가 동작하여 @CreatedDate와 @LastModifiedDate를 처리@PrePersist가 호출되고 touchForCreate() 실행 → AuditingHandler의 markCreated() 실행@PreUpdate가 호출되고 touchForUpdate() 실행 → AuditingHandler의 markModified() 실행AuditingHandler를 이용하여 해당 엔티티의 @CreatedDate 및 @LastModifiedDate 값을 설정🔍 AuditingHandler를 통한 필드 값 설정
package org.springframework.data.auditing;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.data.domain.AuditorAware;
import org.springframework.data.mapping.context.MappingContext;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.util.Assert;
public class AuditingHandler extends AuditingHandlerSupport implements InitializingBean {
// ... 중략
public <T> T markCreated(T source) {
Assert.notNull(source, "Entity must not be null");
return this.markCreated(this.getAuditor(), source);
}
public <T> T markModified(T source) {
Assert.notNull(source, "Entity must not be null");
return this.markModified(this.getAuditor(), source);
}
Auditor<?> getAuditor() {
return (Auditor)this.auditorAware.map(AuditorAware::getCurrentAuditor)
.map(Auditor::ofOptional)
.orElse(Auditor.none());
}
}
markCreated(target) 실행 시 → 내부적으로 AuditingHandlerSupport의 markCreated(getAuditor(), target) 호출markModified(target) 실행 시 → 내부적으로 AuditingHandlerSupport의 markModified(getAuditor(), target) 호출getAuditor() 호출을 통해 현재 사용자 정보를 가져옴 (AuditorAware 사용)@CreatedBy), 누가 수정했는지(@LastModifiedBy)’ 를 추적하고 필요하면 해당 필드에 저장하기 위해markCreated() 및 markModified() 내부적으로 touch() 메서드를 실행하여 엔티티 필드 값을 설정🔍 @CreatedDate 및 @LastModifiedDate 필드 값 설정
package org.springframework.data.auditing;
import java.time.temporal.TemporalAccessor;
import java.util.Objects;
import java.util.Optional;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.log.LogMessage;
import org.springframework.data.mapping.context.PersistentEntities;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
public abstract class AuditingHandlerSupport {
// ... 중략
// touch() 호출
<T> T markCreated(Auditor<?> auditor, T source) {
Assert.notNull(source, "Source entity must not be null");
return this.touch(auditor, source, true);
}
// touch() 호출
<T> T markModified(Auditor<?> auditor, T source) {
Assert.notNull(source, "Source entity must not be null");
return this.touch(auditor, source, false);
}
private <T> T touch(Auditor<?> auditor, T target, boolean isNew) {
Optional<AuditableBeanWrapper<T>> wrapper = this.factory.getBeanWrapperFor(target);
return wrapper.map((it) -> {
this.touchAuditor(auditor, it, isNew);
Optional<TemporalAccessor> now = this.dateTimeForNow ? this.touchDate(it, isNew) : Optional.empty();
if (logger.isDebugEnabled()) {
Object defaultedNow = now.map(Object::toString).orElse("not set");
Object defaultedAuditor = auditor.isPresent() ? auditor.toString() : "unknown";
logger.debug(LogMessage.format("Touched %s - Last modification at %s by %s", target, defaultedNow, defaultedAuditor));
}
return it.getBean();
}).orElse(target);
}
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null");
Optional<TemporalAccessor> now = this.dateTimeProvider.getNow();
Assert.notNull(now, () -> {
return String.format("Now must not be null Returned by: %s", this.dateTimeProvider.getClass());
});
Optional var10000 = now.filter((__) -> {
return isNew;
});
Objects.requireNonNull(wrapper);
var10000.ifPresent(wrapper::setCreatedDate);
var10000 = now.filter((__) -> {
return !isNew || this.modifyOnCreation;
});
Objects.requireNonNull(wrapper);
var10000.ifPresent(wrapper::setLastModifiedDate);
return now;
}
}
touch() 실행 시 touchDate() 호출CurrentDateTimeProvider.INSTANCE.getNow()를 통해 현재 시간 가져옴setCreatedDate()를 호출하여 @CreatedDate 값 설정setLastModifiedDate()를 호출하여 @LastModifiedDate 값 설정@CreatedDate와 @LastModifiedDate 동작 요약@CreatedDate : 엔티티가 persist될 때 (@PrePersist) ⇒ 엔티티가 처음 저장될 때 한 번만 설정됨
AuditingEntityListener.touchForCreate() 실행AuditingHandler.markCreated(target) 실행AuditingHandlerSupport.touch() 실행touchDate()를 통해 createdAt과 modifiedAt에 현재 시간 저장@LastModifiedDate : 엔티티가 update될 때 (@PreUpdate) ⇒ 엔티티가 변경될 때마다 업데이트됨
AuditingEntityListener.touchForUpdate() 실행AuditingHandler.markModified(target) 실행AuditingHandlerSupport.touch() 실행touchDate()를 통해 modifiedAt을 현재 시간으로 업데이트