spring-data-jpa에서 AbstractAuditable을 이용한 audit 남기기

반응형

안녕하세요, 하마연구소 입니다.

spring-data-jpa에서 entity의 CUD 이력을 남기기 위한 audit 기능을 제공해줍니다.
몇몇 설정한하면 아주 편리하죠.

https://docs.spring.io/spring-data/jpa/reference/auditing.html

 

Auditing :: Spring Data JPA

Spring Data provides sophisticated support to transparently keep track of who created or changed an entity and when the change happened.To benefit from that functionality, you have to equip your entity classes with auditing metadata that can be defined eit

docs.spring.io

 

Audit 데이터는 보통 생성자(createdBy), 생성일시(createdDate), 수정자(lastModifiedBy), 수정일시(lastModifiedDate)를 기록하며, 아래와 같이 추상클래스로 정의하여 사용하기도 합니다.

@MappedSuperclass
@EntityListeners(value = [AuditingEntityListener::class])
@Audited
abstract class BaseEntity(
    @CreatedBy
    @Column(nullable = false, updatable = false)
    var createdBy: String? = null,

    @CreatedDate
    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false, updatable = false)
    var createdDate: Instant? = null,

    @LastModifiedBy
    @Column(nullable = false, updatable = true)
    var lastModifiedBy: String? = null,

    @LastModifiedDate
    @Temporal(TemporalType.TIMESTAMP)
    @Column(nullable = false, updatable = true)
    var lastModifiedDate: Instant? = null,
)

 

스프링에서는 이러한 audit 필드 정의를 해둔 클래스가 존재합니다.
Auditable 인터페이스로 정의하였으며, 구현체로 AbstractAuditable 추상클래스가 있습니다.

package org.springframework.data.domain;

import java.time.temporal.TemporalAccessor;
import java.util.Optional;

/**
 * Interface for auditable entities. Allows storing and retrieving creation and modification information. The changing
 * instance (typically some user) is to be defined by a generics definition.
 *
 * @param <U> the auditing type. Typically some kind of user.
 * @param <ID> the type of the audited type's identifier
 * @author Oliver Gierke
 */
public interface Auditable<U, ID, T extends TemporalAccessor> extends Persistable<ID> {

	/**
	 * Returns the user who created this entity.
	 *
	 * @return the createdBy
	 */
	Optional<U> getCreatedBy();

	/**
	 * Sets the user who created this entity.
	 *
	 * @param createdBy the creating entity to set
	 */
	void setCreatedBy(U createdBy);

	/**
	 * Returns the creation date of the entity.
	 *
	 * @return the createdDate
	 */
	Optional<T> getCreatedDate();

	/**
	 * Sets the creation date of the entity.
	 *
	 * @param creationDate the creation date to set
	 */
	void setCreatedDate(T creationDate);

	/**
	 * Returns the user who modified the entity lastly.
	 *
	 * @return the lastModifiedBy
	 */
	Optional<U> getLastModifiedBy();

	/**
	 * Sets the user who modified the entity lastly.
	 *
	 * @param lastModifiedBy the last modifying entity to set
	 */
	void setLastModifiedBy(U lastModifiedBy);

	/**
	 * Returns the date of the last modification.
	 *
	 * @return the lastModifiedDate
	 */
	Optional<T> getLastModifiedDate();

	/**
	 * Sets the date of the last modification.
	 *
	 * @param lastModifiedDate the date of the last modification to set
	 */
	void setLastModifiedDate(T lastModifiedDate);
}

 

package org.springframework.data.jpa.domain;

import jakarta.persistence.ManyToOne;
import jakarta.persistence.MappedSuperclass;
import jakarta.persistence.Temporal;
import jakarta.persistence.TemporalType;
import java.io.Serializable;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.Optional;
import org.springframework.data.domain.Auditable;
import org.springframework.lang.Nullable;

@MappedSuperclass
public abstract class AbstractAuditable<U, PK extends Serializable> extends AbstractPersistable<PK> implements Auditable<U, PK, LocalDateTime> {
    @ManyToOne
    @Nullable
    private U createdBy;
    @Temporal(TemporalType.TIMESTAMP)
    @Nullable
    private Date createdDate;
    @ManyToOne
    @Nullable
    private U lastModifiedBy;
    @Temporal(TemporalType.TIMESTAMP)
    @Nullable
    private Date lastModifiedDate;

    public AbstractAuditable() {
    }

    public Optional<U> getCreatedBy() {
        return Optional.ofNullable(this.createdBy);
    }

    public void setCreatedBy(U createdBy) {
        this.createdBy = createdBy;
    }

    public Optional<LocalDateTime> getCreatedDate() {
        return this.createdDate == null ? Optional.empty() : Optional.of(LocalDateTime.ofInstant(this.createdDate.toInstant(), ZoneId.systemDefault()));
    }

    public void setCreatedDate(LocalDateTime createdDate) {
        this.createdDate = Date.from(createdDate.atZone(ZoneId.systemDefault()).toInstant());
    }

    public Optional<U> getLastModifiedBy() {
        return Optional.ofNullable(this.lastModifiedBy);
    }

    public void setLastModifiedBy(U lastModifiedBy) {
        this.lastModifiedBy = lastModifiedBy;
    }

    public Optional<LocalDateTime> getLastModifiedDate() {
        return this.lastModifiedDate == null ? Optional.empty() : Optional.of(LocalDateTime.ofInstant(this.lastModifiedDate.toInstant(), ZoneId.systemDefault()));
    }

    public void setLastModifiedDate(LocalDateTime lastModifiedDate) {
        this.lastModifiedDate = Date.from(lastModifiedDate.atZone(ZoneId.systemDefault()).toInstant());
    }
}

 

이 AbstractAuditable 추상클래스를 base-entity로 삼아서 사용할 Entity를 정의하고 사용하면 됩니다.
심지어 이 추상클래스에는 @Id 필드도 정의되어 있어 Entity 객체에서 별도로 ID 필드를 정의하지 않아도 됩니다.
아래는 AbstractAuditable을 이용한 MyTableEntity 샘플입니다.

import jakarta.persistence.Entity
import jakarta.persistence.EntityListeners
import jakarta.persistence.Table
import org.hibernate.envers.Audited
import org.springframework.data.jpa.domain.AbstractAuditable
import org.springframework.data.jpa.domain.support.AuditingEntityListener

@Entity
@Table(name = "myTable")
@EntityListeners(value = [AuditingEntityListener::class])
@Audited
data class MyTableEntity(
    var name: String? = null,
    var description: String? = null,
) : AbstractAuditable<MyUser, Long>()

 



AbstractAuditable에서 User 타입을 나타내는 U generic 타입은 일반적으로 String을 많이 사용할 것입니다.
즉, DB 컬럼 값으로 String 타입을 사용하기에 Entity도 String으로 정의합니다.
따라서 다음과 같이 U의 타입을 String으로 사용하겠죠.

@Entity
data class MyTableEntity(
    .
    .
    .
) : AbstractAuditable<String, Long>()

 

그러나...
이러면 컴파일 오류가 발생합니다.
이유는 String 객체가 @Entity가 아니라고 합니다.

Caused by: org.hibernate.AnnotationException: Association 'com.kakao.account.mulang.entity.ApplicationEntity.createdBy' targets the type 'java.lang.String' which is not an '@Entity' type
	at org.hibernate.boot.model.internal.ToOneFkSecondPass.doSecondPass(ToOneFkSecondPass.java:110) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processEndOfQueue(InFlightMetadataCollectorImpl.java:1906) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processFkSecondPassesInOrder(InFlightMetadataCollectorImpl.java:1855) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.boot.internal.InFlightMetadataCollectorImpl.processSecondPasses(InFlightMetadataCollectorImpl.java:1764) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.boot.model.process.spi.MetadataBuildingProcess.complete(MetadataBuildingProcess.java:334) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.metadata(EntityManagerFactoryBuilderImpl.java:1431) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]
	at org.hibernate.jpa.boot.internal.EntityManagerFactoryBuilderImpl.build(EntityManagerFactoryBuilderImpl.java:1502) ~[hibernate-core-6.5.3.Final.jar:6.5.3.Final]

 

현재는 AbstractAuditable의 U 타입은 반드시 @Entity여야 하며, 따라서 String 타입은 사용하지 못합니다.
아쉽네요.
만약 User 데이터가 @Entity로 정의되어 있다면 AbstractAuditable 사용하는 것도 고려할만하겠네요.

감사합니다.

반응형

Designed by JB FACTORY