훈훈훈

Spring boot :: JPA @EntityListeners 정리 본문

Spring Framework/개념

Spring boot :: JPA @EntityListeners 정리

훈훈훈 2021. 3. 22. 02:31

이번에는 JPA Entity에서 이벤트가 발생할 때마다 특정 로직을 실행시킬 수 있는 @EntityListeners를 정리해보려고 한다.

예제 코드는 Spring boot와 Kotlin을 사용하여 작성하였다.

JPA EntityListeners 란?

하이버네이트 문서에서는 JPA Entity에 이벤트가 발생할 때 콜백을 처리하고 코드를 실행하는 방법이라고 소개하고 있다.

 

이제 하이버네이트에서 지원하는 콜백 메서드들을 살펴보자.

EntityListeners는 JPA Entity에 Persist, Remove, Update, Load에 대한 event 전과 후에 대한 콜백 메서드를 제공한다.

 

이번 글에서는 간단한 예제로 Update 하는 코드를 작성해보려고 한다.

예제 코드

@Component
class UserListeners {

    @PreUpdate
    fun preUpdate(user: User) {
        println("======= preUpdate =======")
        println("user: $user")
    }

    @PostUpdate
    fun postUpdate(user: User) {
        println("======= postUpdate =======")
        println("user: $user")
    }
}

 

위 코드는 Entity에 업데이트 이벤트 발생 전후로 user를 출력해주는 코드이다.

@Entity
@EntityListeners(value= [UserListeners::class])
data class User(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    var name: String,
    val createdAt: LocalDateTime = LocalDateTime.now(),
    var updatedAt: LocalDateTime? = null
)

 

위 코드는 테스트할 User Entity Class 이다.

테스트 용이라 간단하게 이름만 갖도록 생성하였다.

 

이제 테스트를 해보자.

 

어플리케이션이 실행이 될 때 @EventListener 를 사용하여 유저를 생성 후 업데이트 하고 이때 설정한 이벤트들이 발생하는지 확인해보았다.

@Component
@Transactional
class StartTest {

    @Autowired
    private lateinit var userRepository: UserRepository

    @EventListener
    fun onApplicationEvent(event: ApplicationStartedEvent) {
        val user1 = User(name = "user1")
        userRepository.save(user1)

        val user = userRepository.findById(1L).orElseThrow { throw Exception("user not found, userId: 1") }
        user.name = "testEntitytListener"
        userRepository.save(user)
    }
}

 

어플리케이션 실행 결과는 아래와 같다.

 

Entity에 Update 이벤트 발생 전후 각각 정상적으로 실행된 것을 확인할 수 있다.

하지만 뭔가 이상하다. user name이 업데이트 전후 모두 똑같이 출력 되었다.

 

정상적인 로직이였다면, preUpdate 메서드가 실행될 때, name은 user1이였어야 한다.

 

관련 내용을 찾아보니 Baeldung 사이트에서 아래와 같은 내용을 확인할 수 있었다.

즉 @PreUpdate는 실질적으로 Update SQL문이 실행되었을 때 실행이 된다.

해당 글을 읽고 영속성 컨텍스트 메커니즘을 떠올려보니 @PreUpdate가 메서드 이름 처럼 동작하지 이해가 조금 되는 것 같다.

 

간단하게 이해한 내용을 정리하자면, 영속성 컨텍스트에서 관리되는 객체들을 변경할 때 개발자가 Update() 메서드를 호출해서 변경하지 않는다. 해당 메서드는 존재하지도 않다.

 

개발자는 단순히 객체의 값만 변경해주면 트랙잭션이 끝나는 시점이나 flush 하는 시점에 엔티티와 스냅샷을 비교하는 변경 감지(Dirty Checking)를 수행 후 변경된 부분이 있다면 Update 쿼리를 생성하여 쓰기 지연 SQL 저장소에 저장 후 최종적으로 DB로 날리게 된다.

 

정리하자면 @PreUpdate가 동작하는 시점은 어쨋든 트랜잭션 종료 혹은 flush 하는 시점이고, 트랙잭션 바운더리안에서 실행되기 때문에 영속성 컨텍스트에 의해 관리된다. 따라서 만약 @PreUpdate가 동작할 때 디비에서 변경되는 값을 조회할지라도 이미 변경된 값으로 조회가 될 것이니 결과적으로 행위 자체는 @PostUpdate와 동일한 결과를 보이게 된다. 

 

 

참고

docs.jboss.org/hibernate/stable/entitymanager/reference/en/html/listeners.html

 

Chapter 6. Entity listeners and Callback methods

@PostPersistExecuted after the entity manager persist operation is actually executed or cascaded. This call is invoked after the database INSERT is executed.

docs.jboss.org

www.baeldung.com/jpa-entity-lifecycle-events

 

JPA Entity Lifecycle Events | Baeldung

Explore what the JPA entity lifecycle callbacks are and when they're called.

www.baeldung.com

stackoverflow.com/questions/12097485/why-does-a-jpa-preupdate-annotated-method-get-called-during-a-query

 

Why does a JPA @PreUpdate-annotated method get called during a query?

I have a named query that returns a Collection of entities. These entities have a @PreUpdate-annotated method on them. This method is invoked during query.getResultList(). Because of this, the e...

stackoverflow.com

 

Comments