Entity Lifecycle, Listener

이종찬·2023년 2월 22일
0
post-custom-banner

📖 Entity Lifecycle?

JPA에서 엔티티는 데이터베이스 테이블의 행을 나타내며 수명주기를 가집니다.

1. New/Transient

해당 단게에는 엔티티가 아직 데이터베이스 테이블과 연결되지 않은 상태입니다. -> 기본키 할당 안된 상태

2. Managed

엔티티가 데이터베이스에 저장된 상태(영속 상태)를 의미합니다. 엔티티에 대한 모든 변경 사항은 영속성 컨텍스트에 의해 추적됩니다.

3. Detached

엔티티가 영속성 컨텍스트에서 제거된 경우이며 분리된 상태입니다. 이 단계에서 엔티티에 대한 변경 사항은 영속성 컨텍스트에 의해 추적되지 않습니다.즉, 엔티티에 대한 변경사항이 JPA에 의해 자동으로 추적되지 않습니다. 왜 Detached상태가 필요한지 조금 더 상세하게 알아보도록 하겠습니다.

Detached(준영속)상태는 개발자가 트랜잭션의 컨텍스트 외부에서 엔티티로 작업할 수 있도록 하기 때문에 JPA에 존재합니다. 데이터베이스에서 엔티티를 로드하고 변경한 다음 나중에 변경 사항을 저장해야 할 수 있습니다. 이러한 작업은 준영속 상태에서 가능합니다.

메모장으로 예시를 들어보겠습니다. 데이터를 편집하기 위해 메모를 열 때 데이터베이스에서 데이터를 로드하고 사용자에게 표시하여 조작 및 저장을 할 수 있습니다. 사용자가 메모장을 로드 한 뒤 내용을 변경한 이후 저장하지 않고 다음날 작업을 하였습니다. 이 때 사용자가 데이터베이스에 저장하는 것을 원치 않는 경우 준영속 상태이기 때문에 데이터베이스에 반영하지 않을 수 있으며, 저장하기로 결정한 경우 변경 사항을 데이터베이스에 저장하여 영속 상태로 만들 수 있습니다.

따라서 준영속상태는 개발자가 유연하게 엔티티와 작업할 수 있도록 함으로 JPA에서 중요한 목적을 수행합니다.

4. Removed

엔티티가 데이터베이스에서 삭제된 상태를 의미합니다.


📖 Entity Listener?

엔티티의 수명주기 동안 발생하며 이벤트에 대한 응답으로 실행되는 메서드를 포함하는 클래스입니다.

JPA에는 두 가지 유형의 Entity Listener가 있습니다.

1.Callback Listener

엔티티가 load, persisted, updated, deleted 와 같은 엔티티의 수명주기 동안 발생하는 특정 이벤트에 대한 응답으로 실행되는 메서드입니다. Callback ListenerEntity, callback method레벨에서 정의할 수 있습니다. 주로 엔티티가 지속되거나 업데이트 되는 시기와 같은 엔티티의 수명 주기 이벤트에 대한 정보를 기록할 수 있습니다. -> 로깅

Callback Listener에서 정의할 수 있는 메서드는 엔티티 수명주기의 다양한 지점에서 JPA Provider에 의해 실행되며 다음과 같은 콜백 메서드가 있습니다.

CallbackMethods

  1. @(Pre/Post)Persist : 엔티티가 데이터베이스에 지속되기 (직전/직후)에 실행됩니다.
  2. @(Pre/Post)Update : 엔티티가 데이터베이스에서 업데이트되기 (직전/직후)에 실행됩니다.
  3. @(Pre/Post)Remove : 엔티티가 데이터베이스에서 삭제되기 (직전/직후)에 실행됩니다.
  4. @PostLoad : 엔티티가 데이터베이스에서 로드된 후 실행됩니다.

엔티티 클래스에서 이러한 콜백 메서드를 정의할 수 있습니다.

2.Lifecycle Listener

Callback Listener를 포함하며 JPA EntityListener인터페이스를 구현하는 클래스입니다. Entity레벨에서 정의할 수 있으며 Callback Listener보다 더 복잡한 작업을 수행하는 데 사용될 수 있습니다. 주로 엔티티가 영속되거나 업데이트될 때 해당 엔티티와 연관된 엔티티를 업데이트 하는 경우가 있습니다. ex) 하위 엔티티 수정 -> listener 작동 -> 상위 엔티티 수정

EntityListener인터페이스는 @PrePersist,@PostPersist와 같은 CallbackMethods를 정의하지만 엔티티 클래스 자체가 아닌 별도의 리스너 클래스에서 정의됩니다.

EntityListener Interface methods

  1. (pre/post)Persist(Object entity) : 엔티티가 데이터베이스에 영속되기(전/후)에 실행됩니다.
  2. (pre/post)Update(Object entity) : 엔티티가 데이터베이스에서 업데이트되기 (전/후)에 실행됩니다.
  3. (pre/post)Remove(Object entity) : 데이터베이스에서 엔티티가 제거되기 직전에 실행됩니다.
  4. postLoad(Object entity) : 엔티티가 데이터베이스에서 로드된 후 실행됩니다.

EntityListener인터페이스를 구현하고 이러한 메서드를 정의하면 다양한 수명 주기 이벤트에 대한 응답으로 엔티티 동작을 사용자 지정할 수 있습니다.

🤔 사용해야 하는 이유는?

Entity Listener는 특정 이벤트에 대한 응답으로 JPA 엔티티의 동작을 사용자가 지정하기 위한 다양한 방법을 제공합니다. 이로 인해 엔티티 클래스 자체를 수정하지 않고 엔티티에 사용자 지정 동작을 추가할 수 있는 장점이 있습니다.

❌ 문제가 되는 경우

1. 일관된 변경 사항 미적용 가능성

EntityListener의 동작을 업데이트 하는 경우 Listener가 사용되는 모든 위치를 확인하고 응용 프로그램 전체에서 변경 사항이 일관되게 적용되도록 보장하기가 어려울 수 있습니다.

2. EntityListener의 필요성

모든 애플리케이션에서 항상 필요하거나 적절한 것은 아닙니다. 어노테이션을 사용하거나 helper 클래스를 만드는 것과 같은 더 가벼운 접근 방식으로 해결이 가능한 경우 EntityListener를 이용한 것 보다 더 좋은 방식일 수 있습니다.

3. Database 의존

일반적으로 영속성 프로세스에 연결되어 있습니다. 테스트를 하려면 데이터베이스에 연결이 필요하여 어려움을 겪을 수 있습니다. 시나리오, 엣지 케이스를 다루는 자동화된 테스트를 전부 작성하기 어려울 수 있습니다.

종합적으로 EntityListener는 JPA 엔티티 동작을 사용자가 지정하기 위한 도구입니다. 문제를 일으킬 수 있는 오버헤드,유지 관리, 테스트 문제 등을 인식하고 사용하는 것이 중요합니다. 요구사항 및 제약조건에 가장 알맞게 선택하는 것이 좋습니다.

👨‍💻 구현

Entity class

@Entity
@NoArgsConstructor
@RequiredArgsConstructor
@Data
public class Book {
    @Id
    @GeneratedValue
    private Long id;
    @NonNull
    private String name;
    @NonNull
    private String author;

    @Column(updatable = false)
    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdated() {
        this.updatedAt = LocalDateTime.now();
    }
}

@PrePersist를 사용하여 데이터베이스에 들어가기 전에 생성시간,업데이트 시간을 넣어줍니다.

@PreUpdate를 사용하여 업데이트가 되는경우 -> save가 되기 직전 -> 업데이트 시간을 변경합니다.

    @Test
    void persistTest() {
        repository.save(new Book("Boook", "Chan"));
        Assertions.assertNotNull(repository.findByName("Boook").getCreatedAt());
        Assertions.assertNotNull(repository.findByName("Boook").getUpdatedAt());
    }

    @Test
    void updateTest() {
        Book book = repository.findByName("Boook");
        book.setName("Khan");
        repository.save(book);
        Assertions.assertNotEquals(book.getCreatedAt(), book.getUpdatedAt());
    }

    @Test
    void printList() {
        persistTest();
        updateTest();
        repository.findAll().forEach(System.out::println);
    }

Book(id=1, name=Khan, author=Chan, createdAt=2023-02-22T22:57:43.684944, updatedAt=2023-02-22T22:57:43.808303)

테스트코드로 확인한 결과 모두 잘 작동되는 것을 확인할 수 있습니다.

이러한 createdAt,updatedAt 의 경우는 대부분의 엔티티에 존재할 것입니다. 그렇다면 엔티티 클래스를 생성할 때 마다 구현해 주어야 하는 경우가 발생할 때 EntityListener를 사용해주면 됩니다.

Interface

public interface Auditable {
    LocalDateTime getCreatedAt();

    LocalDateTime getUpdatedAt();

    void setCreatedAt(LocalDateTime createdAt);

    void setUpdatedAt(LocalDateTime updatedAt);
}

BookEntityListener

public class BookEntityListener {
    @PrePersist
    public void prePersist(Object obj) {
        if (obj instanceof Auditable) {
            ((Auditable) obj).setCreatedAt(LocalDateTime.now());
            ((Auditable) obj).setUpdatedAt(LocalDateTime.now());
        }
    }

    @PreUpdate
    public void preUpdate(Object obj) {
        if (obj instanceof Auditable) {
            ((Auditable) obj).setUpdatedAt(LocalDateTime.now());
        }
    }
}

Book

@Entity
@EntityListeners(value = BookEntityListener.class)
@NoArgsConstructor
@RequiredArgsConstructor
@Data
public class Book implements Auditable {
    @Id
    @GeneratedValue
    private Long id;
    @NonNull
    private String name;
    @NonNull
    private String author;

    @Column(updatable = false)
    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;

//    @PrePersist
//    public void prePersist() {
//        this.createdAt = LocalDateTime.now();
//        this.updatedAt = LocalDateTime.now();
//    }
//
//    @PreUpdate
//    public void preUpdated() {
//        this.updatedAt = LocalDateTime.now();
//    }
}

엔티티 클래스에서 EntityListenerBookEntityListener로 등록하여 줍니다. 또한 Auditable를 상속받음으로 this.createdAt,updateAt = Auditable의 getCreatedAt,getUpdatedAt를 받게 됩니다.

예를 들어 비영속인 값이 db에 들어오면 작동 순서는 다음과 같습니다.

save호출 -> 엔티티 클래스 -> Auditable 호출 -> 리스너 호출 -> 해당 값 넣고 -> db 적재

✅ 요약

  • 엔티티 클래스에는 4개의 수명주기가 있으며 개발자가 데이터베이스의 내용을 꺼내 수정하기 용이하게 되어있다.
  • 엔티티 리스너는 콜백 리스너, 수명주기 리스너로 나뉜다.
  • 콜백 리스너는 총7개로 엔티티 클래스에 작성된다.
  • 수명주기 리스너는 엔티티 클래스에서 엔티티 리스너를 설정, 리스너로 인해 영향을 받는 내용을 인터페이스를 상속받아서 변경한다.
  • 엔티티 리스너는 엔티티 클래스 자체 변경 없이 엔티티 동작을 사용자화 하기 위해 사용한다.
  • 문제점은 전체적으로 일관적인 적용이 어려우며, 데이터베이스 호출에 대한 의존 등이 있다.
profile
왜? 라는 질문이 사라질 때까지
post-custom-banner

0개의 댓글