AuditingEntityListener 등록
AuditingEntityListener
이 동작하기 위해서는 아래와 같은 설정이 필요하다.
@Configuration
@EnableJpaAuditing
class ApplicationConfig {}
@Documented
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(JpaAuditingRegistrar.class)
public @interface EnableJpaAuditing
EnableJpaAuditing
를 통해 JpaAuditingRegistrar
가 임포트 된다.
JpaAuditingRegistrar
는 ImportBeanDefinitionRegistrar
를 확장하고 있다.ImportBeanDefinitionRegistrar
는 @Configuration
클래스를 처리할 때 추가적인 빈 정의를 등록하고자 하는 경우 구현할 수 있는 인터페이스로 이 인터페이스를 구현한 클래스는 @Configuration
, @ImportSelector
와 함께 @Import
어노테이션에 지정할 수 있다.
@Override
public void registerBeanDefinitions(AnnotationMetadata annotationMetadata, BeanDefinitionRegistry registry) {
Assert.notNull(annotationMetadata, "AnnotationMetadata must not be null");
Assert.notNull(registry, "BeanDefinitionRegistry must not be null");
registerBeanConfigurerAspectIfNecessary(registry);
super.registerBeanDefinitions(annotationMetadata, registry);
registerInfrastructureBeanWithId(
BeanDefinitionBuilder.rootBeanDefinition(AuditingBeanFactoryPostProcessor.class).getRawBeanDefinition(),
AuditingBeanFactoryPostProcessor.class.getName(), registry);
}
registerBeanDefinitions
에서 어노테이션 정보를 기반으로 AuditListenerBean
이 등록된다.
@Override
protected void registerAuditListenerBeanDefinition(BeanDefinition auditingHandlerDefinition,
BeanDefinitionRegistry registry) {
if (!registry.containsBeanDefinition(JPA_MAPPING_CONTEXT_BEAN_NAME)) {
registry.registerBeanDefinition(JPA_MAPPING_CONTEXT_BEAN_NAME, //
new RootBeanDefinition(JpaMetamodelMappingContextFactoryBean.class));
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.rootBeanDefinition(AuditingEntityListener.class);
builder.addPropertyValue("auditingHandler",
ParsingUtils.getObjectFactoryBeanDefinition(getAuditingHandlerBeanName(), null));
registerInfrastructureBeanWithId(builder.getRawBeanDefinition(), AuditingEntityListener.class.getName(), registry);
}
registerAuditListenerBeanDefinition
에서는 AuditListenerBean
를 auditingHandler
라는 이름으로 인프라스트럭쳐 빈으로 등록한다.
이후 registerBeanDefinitions
에서는 @CreatedBy
, @LastModifiedDate
와 같은 어노테이션이 제대로 작동하도록 JPA 엔티티에 적용되는 후처리기인AuditingBeanFactoryPostProcessor
를 인프라스트럭쳐 빈으로 등록한다.
AuditingEntityListener
@Configurable
public class AuditingEntityListener {
private @Nullable ObjectFactory<AuditingHandler> handler;
public void setAuditingHandler(ObjectFactory<AuditingHandler> auditingHandler) {
Assert.notNull(auditingHandler, "AuditingHandler must not be null");
this.handler = auditingHandler;
}
@PrePersist
public void touchForCreate(Object target) {
Assert.notNull(target, "Entity must not be null");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markCreated(target);
}
}
}
@PreUpdate
public void touchForUpdate(Object target) {
Assert.notNull(target, "Entity must not be null");
if (handler != null) {
AuditingHandler object = handler.getObject();
if (object != null) {
object.markModified(target);
}
}
}
}
AuditingEntityListener
를 확인해 보면 JpaAuditingRegistrar
를 통해 등록한 auditingHandler
란 이름의 AuditListenerBean
을 주입 받고 있는 것을 확인할 수 있다. 그리고 @PrePersist
와 @PreUpdate
를 통해 엔티티 저장/수정 시점에서의 행위들을 정의하고 있다.
<T> T markCreated(Auditor<?> auditor, T source) {
Assert.notNull(source, "Source entity must not be null");
return touch(auditor, source, true);
}
<T> T markModified(Auditor<?> auditor, T source) {
Assert.notNull(source, "Source entity must not be null");
return touch(auditor, source, false);
}
markCreated
와 markModified
는 공통적으로 touch
메서드를 통해 수행된다.
private <T> T touch(Auditor<?> auditor, T target, boolean isNew) {
Optional<AuditableBeanWrapper<T>> wrapper = factory.getBeanWrapperFor(target);
return wrapper.map(it -> {
touchAuditor(auditor, it, isNew);
Optional<TemporalAccessor> now = dateTimeForNow ? 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);
}
기본 설정에서는 auditor
가 존재하지 않아 touchDate
를 통해 @CreatedDate
와 @LastModifiedDate
로 설정된 칼럼의 값을 업데이트한다.
private Optional<TemporalAccessor> touchDate(AuditableBeanWrapper<?> wrapper, boolean isNew) {
Assert.notNull(wrapper, "AuditableBeanWrapper must not be null");
Optional<TemporalAccessor> now = dateTimeProvider.getNow();
Assert.notNull(now, () -> String.format("Now must not be null Returned by: %s", dateTimeProvider.getClass()));
now.filter(__ -> isNew).ifPresent(wrapper::setCreatedDate);
now.filter(__ -> !isNew || modifyOnCreation).ifPresent(wrapper::setLastModifiedDate);
return now;
}
이를 통해 알 수 있는 것은 엔티티 칼럼에 생성/업데이트 시점에 값을 추가하더라도 touchDate
가 수행되는 시점에서의 값이 데이터베이스에 업데이트된다는 것이다.
엔티티에서의 값과 데이터 베이스에서의 값 불일치 가능성
touchDate
에서 setCreatedDate
와 setLastModifiedDate
를 통해 새로운 값을 할당하기 때문에 엔티티를 생성/수정할 때 @CreatedDate
와 @LastModifiedDate
이 마크된 칼럼에 임의의 값을 설정하여 사용한다면 해당 값과 데이터베이스에 저장되는 값이 달라질 수 있다 판단하였습니다. 이에 엔티티에서 생성/수정을 하더라도 별도의 값을 설정하는 것이 아닌 데이터베이스에 저장하고 이후 조회를 통해 데이터베이스에 저장된 값을 사용하는 방향으로 기존 구현을 변경하였습니다.
'스프링' 카테고리의 다른 글
프로토타입 스코프 빈 도입을 고민해볼 수 있는 경우 (0) | 2025.07.18 |
---|---|
문맥에 따른 의존성 룩업(CDL) (0) | 2025.07.14 |
Page와 Slice (0) | 2025.06.09 |
HTTP 압축 요청 처리 및 API 단위 응답 압축 구현 (0) | 2025.05.29 |
afterCommit과 afterCompletion (0) | 2025.05.27 |