프레임워크(Framework)/Kopring(Kotlin + Spring)

[Kopring] JPA Auditing - 생성/수정 시각 설정 및 테스트

잇트루 2023. 11. 21. 00:19
반응형

JPA Auditing

프로젝트 작성 시 트래킹을 목적으로 엔티티의 생성 시간과 수정 시간 등을 테이블에 저장하여 관리해야 한다. 이를 쉽게 구현하기 위해 Spring Data JPA는 Auditing이라는 엔티티의 변경 내역을 추적하고 기록하는 기능을 제공한다. 이를 통해 엔티티의 생성일자, 수정일자 등을 쉽게 관리할 수 있다.

 

다음은 JPA Auditing 기능을 사용하지 않고 수동으로 관리하는 경우의 User 엔티티다.

@Entity
class User(
    ...
) {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    ...

    @Column(nullable = false, updatable = false)
    var createdAt: LocalDateTime = LocalDateTime.MIN
        protected set
    
    @Column(nullable = false)
    var lastModifiedAt: LocalDateTime = LocalDateTime.MIN
        protected set

    @PrePersist
    fun prePersist() {
        createdAt = LocalDateTime.now()
        lastModifiedAt = LocalDateTime.now()
    }

    @PreUpdate
    fun preUpdate() {
        lastModifiedAt = LocalDateTime.now()
    }

    ...

}
  • 생성일자와 수정일자를 엔티티 필드에 작성하여 관리한다.
  • 생성일자는 @PrePersist 어노테이션을 통해 엔티티가 생성될 때 생성시각을 주입한다.
  • 수정일자는 @PrePersist, @PreUpdate 어노테이션을 통해 엔티티가 생성/수정될 때 현재시각을 주입한다.

 

JPA Auditing 기능을 사용하지 않고도 생성/수정 시각을 관리할 수 있지만, 모든 엔티티에 중복 코드를 작성해야 한다. 이를 해결하기 위해 생성/수정 시각을 자동으로 관리하기 위한 코드를 작성해 보자.

 

 

JPA Auditing 적용하기

BaseTimeEntity.kt

다음은 엔티티의 생성/수정 시각을 추적하고 기록하기 위한 추상 클래스다.

@MappedSuperclass
@EntityListeners(AuditingEntityListener::class)
abstract class BaseTimeEntity {

    @CreatedDate
    @Column(nullable = false, updatable = false)
    var createdAt: LocalDateTime = LocalDateTime.MIN
        protected set
    
    @LastModifiedDate
    @Column(nullable = false)
    var modifiedAt: LocalDateTime = LocalDateTime.MIN
        protected set
}
  • @MappedSuperclass : 엔티티에 공통 매핑 정보를 공유하기 위해 사용하는 어노테이션으로 부모 클래스에 선언하고, 엔티티가 이를 상속하여 필드들을 사용할 수 있도록 한다.
  • @EntityListeners : 엔티티의 상태 변화 이벤트를 처리하는 리스너를 지정하는 어노테이션이다. AuditingEntityListener를 통해 Auditing 이벤트를 처리한다.
  • @CreatedDate, @LastModifiedDate : Spring Data JPA에서 제공하는 Auditing 어노테이션으로 각각 엔티티의 생성일자와 수정일자를 추적한다.

 

MainApplication.kt

JPA Auditing 기능을 활성화하기 위해 메인 애플리케이션에 @EnableJpaAuditing 어노테이션을 추가한다.

@EnableJpaAuditing
@SpringBootApplication
class MainApplication

fun main(args: Array<String>) {
    runApplication<MainApplication>(*args)
}

 

User.kt

BaseTimeEntity를 상속받는 것만으로도 처음에 작성한 수동으로 관리하는 User 엔티티와 같은 기능을 하게 된다.

@Entity
class User(
    email: String,
    name: String,
    gender: Gender
) : BaseTimeEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column(nullable = false, unique = true, updatable = false)
    var email = email
        protected set

    @Column(nullable = false)
    var name = name
        protected set

    @Column(nullable = false)
    @Enumerated(EnumType.STRING)
    var gender = gender
        protected set
}

 

Article.kt

이로써 생성일자와 수정일자가 필요한 모든 엔티티에 BaseTimeEntity를 상속하여 적용할 수 있다.

@Entity
class Article(
    title: String,
    content: String
) : BaseTimeEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id: Long? = null

    @Column
    var title = title
        protected set

    @Column
    var content = content
        protected set
}

 

테이블 생성 결과

애플리케이션을 실행한 후 생성된 테이블을 살펴보면 user 테이블과 article 테이블에 created_at, last_modified_at이 생성된다.

 

 

테스트 코드 작성

이제 엔티티를 생성 또는 수정할 때 정상적으로 createdAt과 lastModifiedAt이 의도한 대로 동작하는지 확인해 보자. 다음 코드는 kotest로 작성된 코드다.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class BaseTimeEntityTest(
    @Autowired
    private val userRepository: UserRepository
) : FunSpec({

    test("User 생성 시 createdAt과 lastModifiedAt이 생성된 시각으로 저장되어야 한다.") {
        val user = User(
            email = "test@test.com",
            name = "홍길동",
            gender = Gender.MAN
        )

        val savedUser = userRepository.save(user)

        savedUser.createdAt shouldBe savedUser.lastModifiedAt
    }

    test("User를 수정하면 lastModifiedAt이 변경된 시각으로 수정되어야 한다.") {
        val user = User(
            email = "test2@test.com",
            name = "홍길동",
            gender = Gender.MAN
        )
        val savedUser = userRepository.save(user)

        Thread.sleep(1000)
        savedUser.update("임꺽정", Gender.MAN)
        val updatedUser = userRepository.save(savedUser)

        updatedUser.createdAt shouldNotBe updatedUser.lastModifiedAt
    }
})
  • User 엔티티를 생성하면 createdAt과 lastModifiedAt이 생성된 시점으로 설정되어 서로 같은 값을 가지게 된다.
  • User 엔티티를 생성한 후 값을 수정하여 다시 저장하면, lastModifiedAt이 수정된 시점으로 변경되어 createdAt과 다른 값을 가지게 된다.

 

반응형