JPA(Jakarta Persistence API) - Entity Listener

2024. 7. 31. 22:35스프링 (Spring)/스프링 데이터 (Spring Data)


Callback

 

엔티티 인스턴스의 생애주기는 크게 신규(new), 관리(manage), 해제(detach), 삭제(remove)로 나눌 수 있습니다.

(더 자세한 설명은 JPA(Jakarta Persistence API) - Entity Operation 글을 참고해주세요.)

 

이번 글에서 다룰 콜백은 각 생애주기를 만나기 전과 후에 호출되는 함수입니다.

사용하려면 아래 어노테이션을 엔티티 함수 위에 붙여주면 됩니다.

어노테이션 관점 시점 관련 있는 연산
PrePersist 엔티티 매니저 영속 전 persist, merge
PostPersist 엔티티 매니저 영속 후 persist, merge
PreRemove 엔티티 매니저 삭제 전 remove
PostRemove 엔티티 매니저 삭제 후 remove
PreUpdate 데이터베이스 업데이트 전 update
PostUpdate 데이터베이스 업데이트 후 update
PostLoad 영속 컨텍스트 로드 후 persist, merge, refresh

 

이러한 콜백함수는 트랜잭션 내에서 오류가 발생할 시 콜백 대상이 됩니다.

그리고 콜백 함수 내에서 엔티티 매니저를 사용하거나 직접 쿼리를 사용해서 다른 엔티티를 불러와서 작업하지 않도록 주의해야 합니다.

In general, the lifecycle method of a portable application should not invoke EntityManager or query operations, access other entity instances, or modify relationships within the same persistence context.
Note that this caution applies also to the actions of objects that might be injected into an entity listener

 

 

실제 예시를 만들어볼까요?

엔티티 함수로 prePersist(), postPersist() 함수를 만들고, modifedBy를 바꿨습니다.

@Entity
@Table(name = "coconuts")
class Coconut(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val srl: Long = Long.MIN_VALUE,

    var perimeter: Int = 10,

    var modifiedBy: String = ""
) {

    @PrePersist
    fun prePersist() {
        println("PrePersist Coconut")
        this.modifiedBy = "PrePersist"
    }

    @PostPersist
    fun postPersist() {
        println("PostPersist Coconut")
        this.modifiedBy = "PostPersist"
    }

}

 

그리고 영속 후 다시 조회해서 modifiedBy를 출력해봅니다.

val coconut = Coconut()
entityManager.persist(coconut)
entityManager.flush()

val savedCoconut: Coconut = entityManager.find(Coconut::class.java, coconut.srl)
println("Who created coconut? : ${savedCoconut.modifiedBy}")
PrePersist Coconut
[Hibernate] 
    insert 
    into
        coconuts
        (modifiedBy, perimeter) 
    values
        (?, ?)
PostPersist Coconut
[Hibernate] 
    update
        coconuts 
    set
        modifiedBy=?,
        perimeter=? 
    where
        srl=?
Who created coconut? : PostPersist

 

디버깅 내용을 보면 insert 쿼리 전 prePersist() 함수가 실행되고, 후에는 postPersist() 함수가 실행됩니다.

재밌는 점은 영속된 후 modifiedBy는 'PrePersist'지만, postPersist() 함수로 인해 다시 'PostPersist'로 바뀌어서

update 쿼리가 한 번 더 등장한 것 입니다.

 

 

사실 일반적으로 콜백 함수를 추가하는 경우는 흔치 않은데,

오류가 발생하면 근본적인 연산을 진행하지 못하므로 위험성이 매우 크기 때문입니다.

또 대부분의 기능은 프레임워크로 가면 제공받습니다. 스프링만 보더라도 @CreatedBy, @CreatedDate 등이 있죠.

 

그럼에도 불구하고 실무에서 필자는 공통으로 생성자나 생성일시를 특정 아이디를 지정해서 만들어 주어야 할 때 사용했었네요.

이는 각 엔티티 클래스마다 콜백 함수를 만들지 않고, 상속을 통해 공통으로 정의했었습니다.

@Entity
@Table(name = "dark_coconuts")
class DarkCoconut(
    var color: String = "dark"
):
    Coconut()


@Entity
@Table(name = "white_coconuts")
class DarkCoconut(
    var color: String = "white"
):
    Coconut()

 

 

 

 

 

 


Listener

 

콜백에 대해서 이해했다면 사실 리스너도 별건 없습니다.

각 엔티티 클래스마다 정의하지 않고, 공통으로 적용하고 싶으면 상속을 이용하면 되지만

이와 별개로 여러 개의 콜백 함수를 적용하고 싶다던지, 상속과 별개로 만들고 싶을 수 있습니다.

이때 리스너를 개별적으로 만들어 원하는 엔티티에 등록해주면 됩니다.

 

아래 라임 엔티티 클래스를 예시로 들어보겠습니다.

@Entity
@Table(name = "limes")
class Lime(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val srl: Long = Long.MIN_VALUE,

    var acidity: Int = -1,

    var color: String = "none",

    var price: Int = -1,
)

 

그리고 acidity에 따라서 색깔 또는 가격을 결정하는 엔티티 리스너 클래스를 만듭니다.

class LimeColorEntityListener {

    @PrePersist
    fun makeColor(lime: Lime) {
        when (lime.acidity) {
            in 0 until 10  -> lime.color = "yellow"
            in 10 until 20 -> lime.color = "green"
            else           -> lime.color = "black"
        }
    }

}
class LimePriceEntityListener {

    @PrePersist
    fun makePrice(lime: Lime) {
        when (lime.acidity) {
            in 0 until 10  -> lime.price = 1000
            in 10 until 20 -> lime.price = 2000
            else           -> lime.price = 3000
        }
    }

}

 

리스너를 추가하지 않고 한 번 생성 후 조회해봅니다.

val lime = Lime(acidity = 10)
entityManager.persist(lime)
entityManager.flush()

val savedLime: Lime = entityManager.find(Lime::class.java, lime.srl)
println("Lime Acidity : ${savedLime.acidity}")
println(" - Color : ${savedLime.color}")
println(" - Price : ${savedLime.price}")
Lime Acidity : 10
 - Color : none
 - Price : -1

 

결과는 예상한 대로 기본 인자 그대로 적용되어 있습니다.

 

이번에는 Color 리스너만 등록해보죠.

@Entity
@Table(name = "limes")
@EntityListeners(value = [LimeColorEntityListener::class])
class Lime(...)
Lime Acidity : 10
 - Color : green
 - Price : -1

 

가격은 그대로지만, 색깔은 'green'으로 변경되었습니다!

마찬가지로 Price 리스너도 등록해보면,

@Entity
@Table(name = "limes")
@EntityListeners(value = [LimeColorEntityListener::class, LimePriceEntityListener::class])
class Lime(...)
Lime Acidity : 10
 - Color : green
 - Price : 2000

 

이제 가격도 바뀐 것을 알 수 있습니다.

 

 

 

 

 

 


Reference

 

본 글은 아래 Jakarta Persistence 문서를 참고하여 작성했습니다.

Copyright (c) 2019, 2024 Eclipse Foundation. This document includes material copied from or derived from https://jakartaee.github.io/persistence/latest/draft.html

 

Jakarta Persistence

The following are reserved identifiers: ABS, ALL, AND, ANY, AS, ASC, AVG, BETWEEN, BIT_LENGTH, BOTH, BY, CASE, CEILING, CHAR_LENGTH, CHARACTER_LENGTH, CLASS, COALESCE, CONCAT, COUNT, CURRENT_DATE, CURRENT_TIME, CURRENT_TIMESTAMP, DELETE, DESC, DISTINCT, EL

jakartaee.github.io