Backend/JPA

Entity Listener의 활용 (AuditingEntityListener)

anxi 2023. 11. 3. 02:02

Listener : 이벤트를 관찰하고 있다가 이벤트가 발생하면 콜백 메서드를 통해 특정 동작을 수행한다.

 

JPA에서는 아래의 7가지 콜백 메서드를 제공한다.

@PrePersist : insert 메소드 수행 전 시행
@PreUpdate : merge 메소드 호출 전 시행
@PreRemove : delete 메소드 호출 전 시행
@PostPersist : insert 메소드 수행 후 시행
@PostUpdate : merge 메소드 수행 후 시행
@PostRemove : delete 메소드 수행 후 시행
@PostLoad : select 조회가 일어난 직후에 실행

 

보통 @PrePersist@PreUpdate를 많이 사용한다. 특히 auditing(감시)할 때 사용한다.

 

예시

엔티티의 생성 시간(createdAt)과 수정 시간(updatedAt)을 설정할 때, setter를 이용하여 날짜 데이터를 입력할 수 있다. 

그러나 단순한 날짜 데이터를 등록 / 수정하는 코드가 반복되기 때문에

Listener를 사용함으로써 DRY(Dont Repeat yourself) 원칙에 따라 중복을 최소화할 수 있고 코드의 재사용성을 높일 수 있다.

 

기존 (setter)

User user = new User();
user.setEmail("han@abc.com");
user.setName("han");
user.setCreatedAt(LocalDateTime.now());
user.setUpdatedAt(LocalDateTime.now());

 

보완 (Listener)

@Entity
@EntityListeners(value = AuditingEntityListener.class)
public class User {

	...
    
    @Column(updatable = false)
    @CreatedDate // 자동으로 시간 값을 지정
    private LocalDateTime createdAt;

    @Column
    @LastModifiedDate // 자동으로 시간 값을 지정
    private LocalDateTime updatedAt;
}

Spring에서 제공해주는 기본 EntityListener인 AuditingEntityListener 클래스를 이용한다.

생성 시간에는 @CreatedDate, 수정 시간에는 @LastModifiedDate 어노테이션을 붙여준다.

(생성과 수정에 누가 관여했는지 보여주는 @CreatedBy, @LastModifiedBy 또한 존재한다.)

 

@SpringBootApplication
@EnableJpaAuditing // Auditing 활성화
public class BookmanagerApplication {

    public static void main(String[] args) {
       SpringApplication.run(BookmanagerApplication.class, args);
    }

}

Main 클래스에 @EnableJpaAuditing을 사용하면 활성화된다.

 

결과

@Test
void userHistoryTest(){
    User user = new User();
    user.setEmail("han@abc.com");
    user.setName("han");

    // insert
    userRepository.save(user);

    // update
    user.setName("New Han");
    userRepository.save(user);

    userHistoryRepository.findAll().forEach(System.out::println);
}

엔티티가 생성되거나 수정될 때 setter를 이용해 반복해서 수행하지 않고 AuditingEntityListener를 이용하여 자동으로 시간이 설정되게 할 수 있다.

 

*추가 보완*

여러 엔티티에 @CreatedDate, @LastModifiedDate를 각각 다는 것 또한 중복되기 때문에,

 

1. 기본 엔티티를 만든다.

2. 기본 엔티티에 엔티티리스너를 연결한다.

3. 각 엔티티에서 기본 엔티티를 상속 받는다.

위와 같은 방식을 진행하는 것을 추천한다.

 

@Data
@MappedSuperclass // 해당 클래스의 필드를 상속받는 엔티티의 컬럼으로 포함시키는 어노테이션
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity {
    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

기본 엔티티(BaseEntity)를 만들고 엔티티리스너(AuditingEntityListener)를 연결한다.

각 엔티티에서의 createdAt과 updatedAt이 중복되는 것을 막기 위해 기본 엔티티에 시간을 추가하고,

@MappedSuperclass를 사용하여 해당 클래스의 필드를 상속받는 엔티티의 컬럼으로 포함시킨다.

 

@Entity
@NoArgsConstructor
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
//@EntityListeners(value = AuditingEntityListener.class)
public class UserHistory extends BaseEntity implements Auditable {

    @Id
    @GeneratedValue
    private Long id;

    private Long userId;

    private String name;

    private String email;

//    @CreatedDate
//    private LocalDateTime createdAt;
//
//    @LastModifiedDate
//    private LocalDateTime updatedAt;
}

 

BaseEntity를 상속받기 때문에 @EntityListener와 createdAt 그리고 updatedAt은 사용하지 않는다.

또한 상속 받을 때 @Data의 에러가 발생한다. 

 

(이 클래스가 java.lang.Object를 확장하지 않은 경우에도 상위 클래스를 호출하지 않고 equals/hashCode 구현을 생성합니다. 의도적인 경우에는 타입에 '(call Super=false)'를 추가하세요. )

 

따라서 @ToString과 @EqualsAndHashcode 어노테이션에 (callSuper = true)라는 조건을 설정하여 상속을 통한 에러를 처리한다.

 

결과