Entity Listener는 엔티티의 변화를 감지하고 데이블의 데이터를 조작하는 일을 한다.
이전에는 Column값이 수정되는 것에 대해서 반복된 코드를 추가해야했으며 개발자가 직접 추가를 하보니 실수가 발생하는 경우가 종종 발생하였다. 하지만 이러한 것은 EntityListener를 사용하면 쉽게 개선할 수 있다.
JPA에서는 아래의 7가지 Event를 제공한다.
@PrePersist : Persist(insert)메서드가 호출되기 전에 실행되는 메서드
@PreUpdate : merge메서드가 호출되기 전에 실행되는 메서드
@PreRemove : Delete메서드가 호출되기 전에 실행되는 메서드
@PostPersist : Persist(insert)메서드가 호출된 이후에 실행되는 메서드
@PostUpdate : merge메서드가 호출된 후에 실행되는 메서드
@PostRemove : Delete메서드가 호출된 후에 실행되는 메서드
@PostLoad : Select조회가 일어난 직후에 실행되는 메서드
@PrePersist
public void prePersist() {
System.out.println(">>> prePersist");
}
@PostPersist
public void postPersist() {
System.out.println(">>> postPersist");
}
@PreUpdate
public void preUpdate() {
System.out.println(">>> preUpdate");
}
@PostUpdate
public void postUpdate() {
System.out.println(">>> postUpdate");
}
@PreRemove
public void preRemove() {
System.out.println(">>> preRemove");
}
@PostRemove
public void postRemove() {
System.out.println(">>> postRemove");
}
@PostLoad
public void postLoad() {
System.out.println(">>> postLoad");
}
Test.java
@Test
void listenerTest() {
User user = new User();
user.setName("martin");
user.setEmail("martin@gmail.com");
userRepository.save(user);
User user2 = userRepository.findById(3L).orElseThrow(RuntimeException::new);
user2.setName("marrrrrrtin");
userRepository.save(user2);
userRepository.deleteById(4L);
}
다음과 같이 7가지 Listener에 대해서 해당 Listener가 실행하는지를 Test하기 위해 Console에 Log를 찍는 코드를 작성 후 새로운 Entity를 생성 및 업데이트, 삭제 순서로 진행을 해보았다. 그 결과는 다음과 같다.
출력 결과를 보면 Data Insert를 하기 전 후에 @PrePersist, @PostPersist가 출력된 것을 확인 할 수 있으며 Entity의 정보를 업데이트 하기위해 데이터를 조회할 때 @PostLoad가 호출된 것을 확인할 수 있다. 또한 뒤에 업데이트를 할 때는 해당 ID를 가진 Entity를 찾기 위해 조회 후 업데이트를 하기에 @PostLoad, @PreUpdate, @PostUpdate순으로 Listener가 불린 것을 확인할 수 있다. 또한 삭제를 할 때는 조회 후 삭제를 하여 @PostLoad, @PreRemove, @PostRemove순으로 Listener가 불린 것을 확인할 수 있었다.
DB를 설계하다보면 보통 Created time과 Updated time을 column을 만들어 저장한다.
하지만 데이터를 생성할 때마다 user.setCreatedAt(LocalDateTime.now());와 같이 직접 넣어주다보면 잊어버리는 일도 발생할 것이다. 그래서 대부분 prePersist를 사용하여 자동으로 Created time과 Updated time값을 넣어준다.
@PrePersist
public void prePersist() {
this.createdAt = LocalDateTime.now();
this.updatedAt = LocalDateTime.now();
}
@PreUpdate
public void preUpdate() {
this.updatedAt = LocalDateTime.now();
}
Test.java
@Test
void prePersistTest() {
User user = new User();
user.setName("Seongwon");
user.setEmail("seongwon@gmail.com");
// @Prepersist를 사용하지 않으면 아래 코드와 같이 직접 변경을 해줘야한다.
// user.setCreatedAt(LocalDateTime.now());
// user.setUpdatedAt(LocalDateTime.now());
userRepository.save(user);
System.out.println(userRepository.findByEmail("seongwon@gmail.com"));
}
- 결과를 확인해보면 @Prepersist annotation이 붙은 method가 data insert전에 수행되어 createdAt, updatedAt의 값이 생성되면서 들어간 것을 확인할 수 있다.
위와 같이 데이터가 업데이트, 생성되는 시간을 기록하는 createdAt, updatedAt와 같은 attribute의 경우는 거의 모든 Entity table에 대해서 적용될 것이며 이러한 Method를 매번 반복되게 추가하는 것은 비효율적이다.
위의 예제와 같은 @PrePersist와 같은 method들을 따로 Class로 생성하고 @EntityListeners를 통해 해당 Listener class를 호출하여 사용하면 코드의 가독성이 증가하고 코드의 중복도 크게 줄일 수 있다.
public class MyEntityListener {
// 객체 내에서 만드는 @PrePersist와 다르게 객체에 대한 정보가 없어서
// Object를 parameter로 받고 Auditable instance인지 확인해야한다.
// Auditable은 EventLister를 구현하기 위해
//반복되어 사용될 method들을 묶어 직접 생성한 interface이다.
@PrePersist
public void prePersist(Object o) {
if (o instanceof Auditable) {
((Auditable) o).setCreatedAt(LocalDateTime.now());
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
@PreUpdate
public void preUpdate(Object o) {
if (o instanceof Auditable){
((Auditable) o).setUpdatedAt(LocalDateTime.now());
}
}
}
해당 MyListener를 Entity객체에 적용하는 방법은 class위에 @EntityListeners(value = MyEntityListener.class) annotation을 추가해주면 된다.
위의 예제로 다뤘던 데이터가 업데이트, 생성됨에 따라 createdAt, updatedAt를 업데이트 하는 것은 대부분의 DB에서 사용하게 된다.
그래서 JPA는 이러한 기능을 기본적으로 제공하고 있다.
마지막으로 Spring JPA에서 제공하는 AuditingEntityListener.class에 대해 알아보고자 한다.
JpaStudyApplication.java
@SpringBootApplication
@EnableJpaAuditing
// 앞서 구현한 CreatedAt, UpdatedAt의 기능은 많이 사용되어서 JPA에서 기본적으로 제공한다.
public class JpaStudyApplication {
public static void main(String[] args) {
SpringApplication.run(JpaStudyApplication.class, args);
}
}
Book.java
@Entity
@NoArgsConstructor
@Data
@EntityListeners(value = AuditingEntityListener.class)
public class Book implements Auditable{
@Id
@GeneratedValue
private Long id;
private String name;
private String author;
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}