본문 바로가기

3.구현/Java or Kotlin

[kotlin] JPA 9: Spring Boot에서 엔티티 이벤트 리스너

들어가기

엔티티 이벤트 리스너에 대해서 다룰려고 한다. 여기서 이벤트는 엔티티와 관련된 이벤트로 로딩, 변경, 추가 등이 있다. 이벤트 처리 방식은 비동기 방식에서 자주 사용하는 방식으로 현재 실행 중인 메인 로직에 영향을 주지 않으면서 처리 중인 작업과 연관되서 실행하는 경우에 매우 유용하다. 여기서 사용한 환경은 Spring Data JPA을 사용했고 데이터베이스는 H2를 사용했다.

작성자: http://ospace.tistory.com/ (ospace114@empal.com)

이벤트 어노테이션

JPA에서 엔티티의 생명주기 동안에 여러가지 이벤트를 발생한다. 이를 적절한 핸들러도 처리할 수 있도록 어노테이션을 사용해서 설정할 수 있다. 엔티티에서 발생하는 이벤트에 대해 다음과 같은 어노테이션을 사용해서 실행할 핸들러를 지정한다.

  • @PostLoad: 영속 컨텍스트에서 조회 또는 데이터베이스에서 로딩 후에 호출된다.
  • @PrePersist: persist()에 의해 영속 컨텍스트에 저장 또는 merge()에서 합치기 전에 호출된다.
  • @PostPersist: 데이터베이스에 저장된 후에 호출된다.
  • @PreUpdate: 데이터베이스에 반영되기 전에 호출된다.
  • @PostUpdate: 데이터베이스에 반영된 후에 호출된다.
  • @PreRemove: remove() 호출 전에 호출된다.
  • @PostRemove: 데이터베이스에서 삭제된 후에 호출된다.

각 이벤트는 PostLoad을 제외하고 서로 쌍을 이루면서 Pre와 Post 로 구성된다. Pre는 해당 작업 전을 의미하고 Post는 해당 작업 이후라고 이해할 수 있다.

엔티티 변경에 대한 이벤트 처리 방식은 엔티티내 핸들러, 사용자 정의 리스너, XML 설정으로 나뉜다.

엔티티에서 핸들러 정의

가장 단단한 방법은 앤티티내 핸들러로 어노테이션을 엔티티내에 메소드에 사용해 호출할 핸들러를 직접 지정한다.

@Entity
open class Member {
      //...
      @PostLoad
    fun postLoad() {
        println("Member.postLoad")
    }
    @PrePersist
    fun prePersist() {
        println("Member.prePersist")
    }
    @PostPersist
    fun postPersist() {
        println("Member.PostPersist")
    }
    @PreUpdate
    fun preUpdate() {
        println("Member.preUpdate")
    }
    @PostUpdate
    fun postUpdate() {
        println("Member.postUpdate")
    }
    @PreRemove
    fun preRemove() {
        println("Member.preRemove")
    }
    @PostRemove
    fun postRemove() {
        println("Member.postRemove")
    }
}

이벤트가 발생하면 해당 메소드가 호출된다. 각 핸들러는 별다른 인자가 없이 정의한다. 호출된다면 자체 엔티티가 대상되고 추가 처리를 하면 된다. 이 방식은 특정 엔티티에 특정해서 필요한 핸들러만 추가할 수 있다. 그러나 다른 엔티티에는 적용할 수는 없다. 이를 해결할는 방법은 별도 리스너를 정의해서 사용할 수 있다.

사용자 정의 리스너

엔티티 이벤트용 사용자 정의 리스너로 별도 리스너 클래스를 정의한다. 그리고 사용할 엔티티 타입에서 @EntityListeners 어노테이션으로 정의한 사용자 정의 리스너 클래스를 지정한다.

@Entity
@EntityListeners(MemberListener::class)
open class Member {
    //...
}

어노테이션에 지정한 MemberListener 리스너 클래스 정의를 보자.

class MemberListener {
    @PostLoad
    fun postLoad(member: Member) {
        println("Member.postLoad")
    }
    @PrePersist
    fun prePersist(member: Member) {
        println("Member.prePersist: ${member.getId()}")
    }
    //...
}

핸들러 인자로는 이벤트가 발생한 대상인 엔티티를 인자를 선언해야 한다. 그리고, 리턴은 void이어야 한다. 위의 예제는 특정 Member 엔티티에만 적용하는 이벤트 리스너이다. 만약 다른 모든 엔티티에도 동일한 엔티티 이벤트 리스터 클래스를 사용하고 싶다면 Any 타입이나 더 상위 부모 클래스를 사용해야 한다. 이 방식은 엔티티와 이벤트 리스너가 분리되어 있기 때문에 유지보수에 유리하다.

추가로 어노테이션을 메소드 개별로 지정해서 사용하고 있다. 어노테이션을 한 메소드에 여러 개를 지정할 수 있다. 예를 들어 아래와 같이 Pre와 Post를 분리해서 핸들러를 지정할 수 있다.

class PrePostListener {
    @PostLoad
    fun postLoad(member: Member) {
        println("postLoad")
    }

    @PrePersist
    @PreUpdate
    @PreRemove
    fun preHandler(member: Member) {
        println("preHandler")
    }

    @PostPersist
    @PostUpdate
    @PostRemove
    fun postHandler(member: Member) {
        println("postHandler")
    }
}

이런 형태로 특정 기준으로 이벤트를 분리해서 하나의 핸들러로 처리할 수 잇다.

XML 설정

이전 방식은 개별 엔티티에서 이벤트 리스너에 대한 추가작업이 필요하다. 한꺼번에 모든 엔티티에 적용하기 위해서는 동일한 작업이 반복될 수 있다. 이를 해결할 수 있는 방법이 XML 설정 방식이다. XML 설정은 META-INF/orm.xml 파일을 사용해서 설정 할 수 있다.

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://xmlns.jcp.org/xml/ns/persistence/orm"
                 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
                 xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence/orm
      http://xmlns.jcp.org/xml/ns/persistence/orm_2_1.xsd"
                 version="2.1">
<persistence-unit-metadata>
    <persistence-unit-defaults>
        <entity-listeners>
            <entity-listener class="com.tistory.ospace.member.entity.PrePostListener">
            </entity-listener>
        </entity-listeners>
    </persistence-unit-defaults>
</persistence-unit-metadata>
</entity-mappings>

이렇게할 경우 모든 엔티티의 기본 리스너가 등록된다. 만약 특정 엔티티에서 이런 기본 리스너를 제외하고 싶다면 해당 엔티티에 @ExcludeDefaultListeners을 사용한다. 또한 부모 엔티티 클래스에 등록된 리스너를 적용하고 싶지 않다면 @ExcludeSuperclassListeners을 사용한다.

@Entity
@EntityListeners(MemberListener::class)
@ExcludeDefaultListeners
@ExcludeSuperclassListeners
open class Member : BaseEntity {
    //...
}

마무리

JPA의 엔티티 이벤트 리스너를 사용하는 방법을 간단하게 살펴보았다. 이벤트 기반 처리는 비동기 작업에 자주 사용하는 방식이다. 이를 사용하면 로깅이나 감사 처리 같은 작업이 쉬워진다. 또한 메인 로직에 영향을 주지 않기 때문에 관심 분리로 코드가 분리되 가독성과 응집성이 좋아진다. 잘 활용하면 이런 장점이 있지만 다른 측면에서느는 코드 분리로 코드 추적이 어렵고, 특정 이벤트로 인해 추가 처리되는 작업을 파악하기 힘들다. 이로 인해 에러가 발생할 경우 원인 분석이 어려워진다. 복잡한 기술일 수록 잘 쓰면 약이 되지만, 그렇지 않으면 독이 될 수 있다.

부족한 글이지만 여러분에게 도움이 되었으면 하네요. 모드 즐거운 코딩생활되세요. ospace.

참고

[1] 최범균, JPA 프로그래밍 입문, 가메출판사, 2017

[2] 김영한, 자바 ORM 표준 JPA 프로그래밍, 에이콘, 2015

[3] Amy DeGregorio, JPA Entity Liefecycle Events, https://www.baeldung.com/jpa-entity-lifecycle-events, 2024.03.11

반응형