본문 바로가기
Spring Framework/JPA

스프링 Data JPA: Auditing 적용 방법 [Kotlin]

by 스코리아 2024. 1. 13.

안녕하세요, 스코리아입니다.

오늘은 스프링 Data JPA의 Auditing을 적용하는 방법에 대해서 알아보겠습니다. 언어는 코틀린(Kotlin)으로 진행하겠습니다.

 

Spring Data JPA - Auditing

JPA Auditing 소개

어원적인 "Auditing"은 감사, 감사 추적과 관련된 용어로, 어떤 일련의 이벤트나 활동에 대한 기록, 모니터링, 추적을 수행하는 것을 의미합니다.

"JPA Auditing"은 Java Persistence API에서 제공하는 기능 중 하나로, 엔터티의 변경 이력을 추적하고 관리하는 데 사용됩니다. 주로 데이터베이스에서 특정 테이블의 Record가 언제 생성되었고, 언제 마지막으로 수정되었는지를 추적하는 데 활용됩니다. 이를 통해 데이터 변경 내역을 추적하고, 시간에 따른 엔터티의 상태를 확인할 수 있습니다

 

JPA Auditing은 주로 생성일자(Created Date), 수정일자(Last Modified Date), 생성자(Created By), 수정자(Last Modified By) 4가지의 핵심적인 이벤트를 추적하게 됩니다.

 

따로 로그를 남기는 방법도 있겠지만, JPA Auditing을 사용하게 되면 위의 이벤트들을 자동으로 추적하여 DB에 업데이트해 주기 때문에 매우 편리하게 관리할 수 있습니다.

 

 

JPA Auditing 사용 방법

1. AuditConfig.kt

먼저 @EnableJpaAuditing 어노테이션을 사용해야 Auditing 기능을 활성화시킬 수 있습니다.

이를 Application 클래스에 붙여도 되지만, 저는 CustomPrincipal로 캐스팅하는 과정을 거쳐 사용자 이름을 가져오기 위해 @Configuration 어노테이션이 사용된 Config 클래스를 따로 만들어서 활성화시키도록 하겠습니다.

@EnableJpaAuditing
@Configuration
class AuditConfig {
    @Bean
    fun auditorAware(): AuditorAware<String> {
        return AuditorAware<String> {
            Optional.ofNullable(SecurityContextHolder.getContext())
                .map { it.authentication }
                .filter { it.isAuthenticated && !it.name.equals("anonymousUser") }
                .map { it.principal as CustomPrincipal }
                .map { it.username }
        }
    }
}

 

코드를 보시면 Spring Security의 SecurityContextHolder에서 현재 보안 컨텍스를 가져온 후, map을 이용하여 인증객체를 추출합니다. 그리고 그 인증 객체가 인증되었고 익명의 유저(anonymousUser)가 아니라면, 사용자 정보가 포함된 principal을 추출을 하는데, 이때 CustomPrincipal(UserDetials, Oauth2User를 상속한 객체) 클래스로 캐스팅을 합니다. 그 후, CustomPrincipal 객체에서 사용자 이름(username)을 추출하여 return 하게 됩니다.

 

즉, 결과적으로 이 설정은 JPA Auditing이 사용자 정보를 찾을 때 Spring Security를 활용하며, 사용자가 인증되어 있고 익명 사용자가 아닌 경우에만 사용자 이름을 추출하여 JPA Auditing에 return 하는 기능을 수행합니다.

 

만약 사용자가 인증되지 않거나 익명 사용자라면 null이 return 될 것입니다. (예: 사용자가 회원가입 시 생성자 및 수정자는 null)

 

 

2. AuditingFields.kt

- @EntityListeners(AuditingEntityListener::class) : 이 어노테이션은 엔티티의 변화를 감지하여 엔티티와 매핑된 테이블의 데이터를 조작합니다. AuditingEntityListener 클래스는 Spring Data JPA에서 제공하는 Event Listener로, 엔티티의 영속, 수정 이벤트를 감지합니다.

- @MappedSuperClass : 이 어노테이션은 여러 엔티티에서 중복코드를 방지하기 위해서 사용되고, 엔티티의 공통 필드를 정의하게 됩니다. 

@EntityListeners(AuditingEntityListener::class) // auditing
@MappedSuperclass
abstract class AuditingFields {
    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @CreatedDate
    @Column(nullable = false, updatable = false)
    open var createdAt: LocalDateTime? = null // 생성일시
        protected set

    @CreatedBy
    @Column(updatable = false, length = 100)
    open var createdBy: String? = null // 생성자
        protected set

    @DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
    @LastModifiedDate
    @Column(nullable = false)
    open var modifiedAt: LocalDateTime? = null // 수정일시
        protected set

    @LastModifiedBy
    @Column(length = 100)
    open var modifiedBy: String? = null // 수정자
        protected set
}

 

필드를 하나씩 소개해드리겠습니다.

 

(1) createdAt: 엔터티의 생성일시를 나타내는 필드입니다. @CreatedDate 애노테이션을 통해 JPA Auditing이 이 필드를 업데이트할 것이며, updatable = false로 설정되어 엔터티가 생성된 후에는 수정되지 않도록 합니다. 또한 생성일시는 어떠한 경우에도 생성되어야 하므로, null을 허용하지 않았습니다.

(2) createdBy: 엔터티를 생성한 사용자를 나타내는 필드입니다. @CreatedBy 애노테이션을 통해 JPA Auditing이 이 필드를 업데이트할 것이며, updatable = false로 설정되어 엔터티가 생성된 후에는 수정되지 않도록 합니다. 또한 생성자는 익명의 유저가 등록할 수도 있기 때문에 null을 허용하였습니다.

(3) modifiedAt: 엔터티의 최근 수정일시를 나타내는 필드입니다. @LastModifiedDate 애노테이션을 통해 JPA Auditing이 이 필드를 업데이트할 것이며, nullable = false로 설정되어 항상 값이 존재하도록 합니다.

(4) modifiedBy: 엔터티를 최근에 수정한 사용자를 나타내는 필드입니다. @LastModifiedBy 애노테이션을 통해 JPA Auditing이 이 필드를 업데이트할 것이며, 익명의 유저가 수정할 수도 있기 때문에 null을 허용하였습니다.

 

이러한 설정을 통해 AuditingFields를 상속받는 엔터티들은 자동으로 생성 및 수정 일시 및 사용자 정보가 관리되고 JPA Auditing이 활성화됩니다.

 

 

3. Member.kt

기존에 있던 Member Entitiy 클래스에 방금 만든 AuditingFields 클래스를 상속합니다.

@Entity
@Table(
    uniqueConstraints = [
        UniqueConstraint(name = "uk_member_user_id", columnNames = ["userId"]),
        UniqueConstraint(name = "uk_member_email", columnNames = ["email"]),
        UniqueConstraint(name = "uk_member_nick", columnNames = ["nick"])
    ]
)
class Member(
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    var id: Long? = null,

    @Column(nullable = false, length = 100, updatable = false)
    val userId: String,

    @Column(nullable = false, length = 100)
    val email: String,

    @Column(length = 100)
    var password: String,

    @Column(nullable = false, length = 100)
    var nick: String,

    // .. 다른 필드 생략

): AuditingFields() // Auditing 필드 클래스 상속

 

이렇게 되면 자동으로 생성자, 생성일, 수정자, 수정일에 대한 정보가 관리 및 업데이트됩니다.

 

 

JPA Auditing 테스트

1. 회원가입 시

회원가입을 할 때는 익명의 유저가 Record를 생성하는 것이기 때문에 생성자와 수정자는 null이 나와야 할 것입니다.

아래는 회원가입 서비스 로직 중 일부입니다.

fun signUp(memberDtoRequest: MemberDtoRequest): String {
        checkDuplicateUserId(memberDtoRequest.userId) // userId 중복 검사
        checkDuplicateNick(memberDtoRequest.nick) // nick 중복 검사

        val member: Member = memberDtoRequest.toEntity()
        member.password = passwordEncoder.encode(member.password)

        memberRepository.save(member) // save 시점 (Auditing 기록)

        return "회원가입이 완료되었습니다."
    }

 

memberRepository.save()가 실행되는 시점Auditing 정보와 함께 DB에 저장됩니다. 즉 생성자, 수정자는 null이고 생성일이 현재 시간으로 잘 기록되어야 하며, 생성이 되었다는 것은 수정이 되었다고도 보기 때문에 수정일은 현재 시간으로 기록되어야 합니다.

회원가입 후, created_at과 modified_at이 잘들어간 모습

 

이렇게 Record 생성이 일어나면, 생성자와 생성일은 이후 업데이트가 일어나도 절대로 변하지 않습니다. (AuditingFields에서 updateable을 false로 설정하였기 때문에)

 

 

2. 회원 정보 수정 시

아래는 회원 정보 서비스 로직의 일부입니다.

fun updateMyInfo(userId: String, memberUpdateDtoRequest: MemberUpdateDtoRequest): String {
    val member: Member = memberRepository.findByUserId(userId) ?: throw IllegalArgumentException("회원 아이디(${userId})가 존재하지 않는 유저입니다.")

    memberRepository.save(member) // 수정 시점 (Auditing 기록)
    return "수정이 완료되었습니다."
}

 

memberRepository.save()가 실행되는 시점 Auditing 정보와 함께 DB에 업데이트됩니다.

회원 정보를 수정할 때는 회원이 로그인된 상태에서 Record를 수정하는 것이기 때문에, 수정자는 로그인한 회원의 아이디, 수정일은 현재 시간이 기록되어야 합니다. 이때 생성자와 생성일은 변하지 않습니다.

회원정보 수정 후, modified_by와 modified_at이 업데이트됨

 


 

이상으로 생성자, 생성일, 수정자, 수정일을 자동으로 관리해 주는 Spring Data JPA의 Auditing 기능에 대해서 알아보았습니다. 

읽어주셔서, 감사합니다.