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

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

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

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


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>()
출처: https://hippolab.tistory.com/73 [하마연구소:티스토리]


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]
출처: https://hippolab.tistory.com/73 [하마연구소:티스토리]

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

감사합니다.

0 0 votes
Article Rating
Subscribe
Notify of
guest

0 Comments
Most Voted
Newest Oldest
Inline Feedbacks
View all comments