문홍의 공부장

[Java] EntityListeners 를 활용하여 엔티티의 생명 주기에 따른 이벤트 처리하기 (JPA Entity Lifecycle Events) 본문

개발/Java

[Java] EntityListeners 를 활용하여 엔티티의 생명 주기에 따른 이벤트 처리하기 (JPA Entity Lifecycle Events)

moonong 2024. 1. 28. 21:53
반응형

 

JPA를 사용하다보면 여러가지 상황을 캐치하여 작업을 해야할 경우가 생기곤 한다.

 

예를 들어, 데이터의 수정/삭제가 발생하였을 시 그 요청 이력을 관리하여야 한다고 가정하자. 이 때 모든 수정/삭제 로직이 끝날 때 이력 관리 테이블에 데이터를 저장하는 로직을 추가하는 것은 매우 비효율적인 일이다. 하이버네이트에서 제공하는 JPA EntityListeners 를 활용하여 엔티티의 생명 주기에 따른 이벤트를 처리할 수 있다.

Callback Events

  • PrePersist: persist() 를 호출하기 전, 새로운 엔티티가 영속성 컨텍스트에 관리되기 직전에 호출된다.
    • 식별자 생성 전략을 사용한 경우, 엔티티의 식별자는 아직 존재하지 않는 상태이다.
  • PostPersist: persist()를 호출한 후, flush()commit() 을 통해 엔티티를 데이터베이스에 저장한 직후에 호출된다.
    • 식별자가 항상 존재한다.
    • 생성자 전략이 IDENTITY 인 경우, 식별자 생성을 위해 persist() 호출하여 엔티티를 저장하게 된다. 이때에는 persist() 를 호출한 직후에 바로 @PostPersist 가 호출된다.
  • PreRemove: remove() 를 호출하여 엔티티를 영속성 컨텍스트에서 삭제하기 직전에 호출된다.
    • orphanRemoval 에 대하여는 flush()commit() 시에 호출된다.
  • PostRemove: flush()commit() 를 호출하여 엔티티를 데이터베이스에서 삭제한 직후에 호출된다.
  • PreUpdate: flush()commit() 를 호출하여 엔티티를 데이터베이스에서 수정하기 직전에 호출된다.
  • PostUpdate: flush()commit() 를 호출하여 엔티티를 데이터베이스에서 수정한 직후에 호출된다.
    • persist() 시에는 호출되지 않는다
    • 변경감지에 의한 업데이트 시에도 호출된다
  • PostLoad: 엔티티가 영속성 컨텍스트에 로드(조회 또는 refresh())된 후에 호출된다.
    • 2차 캐시에 저장되어 있어도 호출된다.
@Entity
public class User {
    private static Log log = LogFactory.getLog(User.class);

    @Id
    @GeneratedValue
    private int id;

    private String userName;
    private String firstName;
    private String lastName;
    @Transient
    private String fullName;

    // Standard getters/setters
    ...
    @PrePersist
    public void logNewUserAttempt() {
        log.info("Attempting to add new user with username: " + userName);
    }

    @PostPersist
    public void logNewUserAdded() {
        log.info("Added user '" + userName + "' with ID: " + id);
    }

    @PreRemove
    public void logUserRemovalAttempt() {
        log.info("Attempting to delete user: " + userName);
    }

    @PostRemove
    public void logUserRemoval() {
        log.info("Deleted user: " + userName);
    }

    @PreUpdate
    public void logUserUpdateAttempt() {
        log.info("Attempting to update user: " + userName);
    }

    @PostUpdate
    public void logUserUpdate() {
        log.info("Updated user: " + userName);
    }

    @PostLoad
    public void logUserLoad() {
        fullName = firstName + " " + lastName;
    }
}

 

 

위와 같은 콜백 메서드들은, 엔티티마다 일어나야 하는 작업이 다를 때 유용하게 사용될 수 있다. 하지만 모든 엔티티에 대하여 적용되는 경우는 어떨까? 모든 엔티티에 이력 관리를 위한 콜백 이벤트 메서드를 작성하는 것은 결국 코드를 중복 작성하는 일이다. 또한, 유저 정보와 이력 관리라는 두 가지 역할을 효율적으로 관리하기 위하여서도 분리가 필요해보인다.

 

이를 이를 전역적으로 관리하기 위해 EntityListeners 를 활용할 수 있다.

 

 

public class AuditTrailListener {
    private static Log log = LogFactory.getLog(AuditTrailListener.class);

    @PrePersist
    @PreUpdate
    @PreRemove
    private void beforeAnyUpdate(User user) {
        if (user.getId() == 0) {
            log.info("[USER AUDIT] About to add a user");
        } else {
            log.info("[USER AUDIT] About to update/delete user: " + user.getId());
        }
    }

    @PostPersist
    @PostUpdate
    @PostRemove
    private void afterAnyUpdate(User user) {
        log.info("[USER AUDIT] add/update/delete complete for user: " + user.getId());
    }

    @PostLoad
    private void afterLoad(User user) {
        log.info("[USER AUDIT] user loaded from database: " + user.getId());
    }
}

 

 

User 테이블의 로그를 관리하기 위한 AuditTrailListener 클래스를 생성하였다. 그리고 유저 엔티티에 아래와 같이 @EntityListener 를 추가한다.

 

 

@EntityListeners(AuditTrailListener.class)
@Entity
public class User {
    private static Log log = LogFactory.getLog(User.class);

    @Id
    @GeneratedValue
    private int id;

    private String userName;
    private String firstName;
    private String lastName;
    @Transient
    private String fullName;

    // Standard getters/setters
}

 

 

이로써 유저 엔티티의 모든 활동은 AuditTrailListener 을 통해 관리된다.

References.

반응형