JPA에서 엔티티는 데이터베이스 테이블의 행을 나타내며 수명주기를 가집니다.
1. New/Transient
해당 단게에는 엔티티가 아직 데이터베이스 테이블과 연결되지 않은 상태입니다. -> 기본키 할당 안된 상태
2. Managed
엔티티가 데이터베이스에 저장된 상태(영속 상태)를 의미합니다. 엔티티에 대한 모든 변경 사항은 영속성 컨텍스트에 의해 추적됩니다.
3. Detached
엔티티가 영속성 컨텍스트에서 제거된 경우이며 분리된 상태입니다. 이 단계에서 엔티티에 대한 변경 사항은 영속성 컨텍스트에 의해 추적되지 않습니다.즉, 엔티티에 대한 변경사항이 JPA에 의해 자동으로 추적되지 않습니다. 왜 Detached
상태가 필요한지 조금 더 상세하게 알아보도록 하겠습니다.
Detached(준영속)상태는 개발자가 트랜잭션의 컨텍스트 외부에서 엔티티로 작업할 수 있도록 하기 때문에 JPA에 존재합니다. 데이터베이스에서 엔티티를 로드하고 변경한 다음 나중에 변경 사항을 저장해야 할 수 있습니다. 이러한 작업은 준영속 상태에서 가능합니다.
메모장으로 예시를 들어보겠습니다. 데이터를 편집하기 위해 메모를 열 때 데이터베이스에서 데이터를 로드하고 사용자에게 표시하여 조작 및 저장을 할 수 있습니다. 사용자가 메모장을 로드 한 뒤 내용을 변경한 이후 저장하지 않고 다음날 작업을 하였습니다. 이 때 사용자가 데이터베이스에 저장하는 것을 원치 않는 경우 준영속 상태이기 때문에 데이터베이스에 반영하지 않을 수 있으며, 저장하기로 결정한 경우 변경 사항을 데이터베이스에 저장하여 영속 상태로 만들 수 있습니다.
따라서 준영속상태는 개발자가 유연하게 엔티티와 작업할 수 있도록 함으로 JPA에서 중요한 목적을 수행합니다.
4. Removed
엔티티가 데이터베이스에서 삭제된 상태를 의미합니다.
엔티티의 수명주기 동안 발생하며 이벤트에 대한 응답으로 실행되는 메서드를 포함하는 클래스입니다.
JPA에는 두 가지 유형의 Entity Listener가 있습니다.
엔티티가 load
, persisted
, updated
, deleted
와 같은 엔티티의 수명주기 동안 발생하는 특정 이벤트에 대한 응답으로 실행되는 메서드입니다. Callback Listener
는 Entity
, callback method
레벨에서 정의할 수 있습니다. 주로 엔티티가 지속되거나 업데이트 되는 시기와 같은 엔티티의 수명 주기 이벤트에 대한 정보를 기록할 수 있습니다. -> 로깅
Callback Listener
에서 정의할 수 있는 메서드는 엔티티 수명주기의 다양한 지점에서 JPA Provider에 의해 실행되며 다음과 같은 콜백 메서드가 있습니다.
엔티티 클래스에서 이러한 콜백 메서드를 정의할 수 있습니다.
Callback Listener
를 포함하며 JPA EntityListener
인터페이스를 구현하는 클래스입니다. Entity
레벨에서 정의할 수 있으며 Callback Listener
보다 더 복잡한 작업을 수행하는 데 사용될 수 있습니다. 주로 엔티티가 영속되거나 업데이트될 때 해당 엔티티와 연관된 엔티티를 업데이트 하는 경우가 있습니다. ex) 하위 엔티티 수정 -> listener 작동 -> 상위 엔티티 수정
EntityListener
인터페이스는 @PrePersist
,@PostPersist
와 같은 CallbackMethods를 정의하지만 엔티티 클래스 자체가 아닌 별도의 리스너 클래스에서 정의됩니다.
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();
// }
}
엔티티 클래스에서 EntityListener
를 BookEntityListener
로 등록하여 줍니다. 또한 Auditable
를 상속받음으로 this.createdAt,updateAt = Auditable의 getCreatedAt,getUpdatedAt를 받게 됩니다.
예를 들어 비영속인 값이 db에 들어오면 작동 순서는 다음과 같습니다.
save호출 -> 엔티티 클래스 -> Auditable 호출 -> 리스너 호출 -> 해당 값 넣고 -> db 적재