백엔드/JPA

[ JPA ] 4. Entity Listener

Reference. 한 번에 끝내는 Java/Spring 웹 개발 마스터 초격차 패키지 Online

이전 글

5.5. Entity Listener

  • Listener: 이벤트를 관찰하고 있다가 발생하면 특정 동작을 진행

1. Entity Listener Annotation

<method 실행 전>
  • @PrePersist: insert method가 호출되기 전
  • @PreUpdate: merge method가 호출되기 전
  • @PreRemove: delete method가 호출되기 전

<method 실행 이후>

  • @PostPersist: insert method가 호출된 이후
  • @PostUpdate: merge method가 호출된 이후
  • @PostRemove: delete method가 호출된 이후
  • @PostLoad: select 조회가 된 직후
메소드 생성
  • 어노테이션 기반이므로 method명을 일치할 필요없음 (@PrePersist, prePersist)
@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
@Builder
@Entity
@Table(name="user", indexes = {@Index(columnList = "name")}, uniqueConstraints = {@UniqueConstraint(columnNames = "email")})
public class User {

		...
		
		@PrePersist
		public void prePersist(){
		    System.out.println(">>> prePersist");
		}
		
		@PostPersist
		public void postPersist(){
		    System.out.println(">>> postPersist");
		}
		
		...
}
테스트 코드 및 실행
@Test
void listenerTest(){
    User user = new User();
    user.setEmail("test@gmail.com");
    user.setName("sim");

    userRepository.save(user); //insert

    User user2 = userRepository.findById(1L).get();
    user2.setName("kim");

    userRepository.save(user2); //update

    userRepository.deleteById(4L);
}
>>> prePersist
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user
        (created_at, email, gender, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?, ?)
>>> postPersist

Hibernate: 
    select
        user0_.id as id1_1_0_,
        user0_.created_at as created_2_1_0_,
        user0_.email as email3_1_0_,
        user0_.gender as gender4_1_0_,
        user0_.name as name5_1_0_,
        user0_.updated_at as updated_6_1_0_ 
    from
        user user0_ 
    where
        user0_.id=?
>>> postLoad

>>> preUpdate
Hibernate: 
    update
        user 
    set
        email=?,
        gender=?,
        name=?,
        updated_at=? 
    where
        id=?
>>> postUpdate

>>> preRemove
Hibernate: 
    delete 
    from
        user 
    where
        id=?
>>> postRemove

2. Event Listener를 왜 쓰는가?

  • 일반적으로 Auditing하는 용도로 많이 사용
  • user.setCreatedAt(), user.setUpdatedAt() 는 실수로 안넣을 확률이 높지만 중요한 데이터
    → 이런 실수와 반복적인 코드를 줄이기 위해 사용하는 경우가 많음
UserRepository.java (수정 전)
@Test
void prePersistTest(){
    User user = new User();
    user.setEmail("test@gmail.com");
    user.setName("Kim");
    user.setCreatedAt(LocalDateTime.now()); //현재 시간을 등록
    user.setUpdatedAt(LocalDateTime.now()); //현재 시간을 등록

    userRepository.save(user);

    System.out.println(userRepository.findByEmail("test@gmail.com"));
}

User.java (수정 후)
public class User{
		...
		@PrePersist
		public void prePersist(){
		    this.createdAt = LocalDateTime.now();
		    this.updatedAt = LocalDateTime.now();
		}
		...
}
UserRepository.java (수정 후)
@Test
void prePersistTest(){
    User user = new User();
    user.setEmail("test@gmail.com");
    user.setName("Kim");

    userRepository.save(user);

    System.out.println(userRepository.findByEmail("test@gmail.com"));
}

실행결과 (수정 전, 수정 후 동일)
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user
        (created_at, email, gender, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?, ?)


binding parameter [1] as [TIMESTAMP] - [2021-07-23T22:41:58.879792900]
binding parameter [2] as [VARCHAR] - [test@gmail.com]
binding parameter [3] as [INTEGER] - [null]
binding parameter [4] as [VARCHAR] - [Kim]
binding parameter [5] as [TIMESTAMP] - [2021-07-23T22:41:58.879792900]
binding parameter [6] as [BIGINT] - [6]

3. 여러 객체에서 공통 Entity Listener 쓰는 법

  • Entity 객체에 @EntityListeners를 이용하여 공통화 가능
    - 다형성 처리가 가능한 interface 생성 필요 (Auditable.java)
    - Listener 공통화 할 EntityListener 생성 (MyEntityListener.java)
Auditable.java
public interface Auditable {
    LocalDateTime getCreatedAt();
    LocalDateTime getUpdatedAt();

    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);
}
MyEntityListener.java
public class MyEntityListener {
    @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());
        }
    }
}
User.java
@Entity
@NoArgsConstructor
@Data
@EntityListeners(value = {MyEntityListener.class})
public class User implements Auditable{
		...

		@Column(updatable = false)
    private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}
Book.java
@Entity
@NoArgsConstructor
@Data
@EntityListeners(value ={ MyEntityListener.class })
public class Book implements Auditable{
		...
		
		private LocalDateTime createdAt;

    private LocalDateTime updatedAt;
}
  • 작업 전 Entity와 작업 후 Entity의 차이는 @PrePersist, @PreUpdate 메소드가 각각의 Entity 안에 존재하지 않음
    MyEntityListener 안에 메소드들은 어떤 Entity를 받는지 알기 위해 Object 파라미터를 받음
    → Listener에선 Object가 Auditable을 상속받은 class인지 체크 후 실행
//MyEntityListener 
@PrePersist
public void prePersist(Object o){ //Object로 해당 객체를 받음
    if(o instanceof Auditable){
        ((Auditable) o).setCreatedAt(LocalDateTime.now());
        ((Auditable) o).setUpdatedAt(LocalDateTime.now());
    }
}

//User.java
@PrePersist
public void prePersist(){ //this를 통해 해당 파라미터를 참조
    this.createdAt = LocalDateTime.now();
    this.updatedAt = LocalDateTime.now();
}

4. History 용도로 Entity Listener 쓰는 법

UserHistoryRepository.java 생성
public interface UserHistoryRepository extends JpaRepository<UserHistory, Long> {
}
UserEntityListener.java 생성
  • jpa에 Listener는 @Component를 사용 못함, 즉 @Autowired를 할 수 없음
    → 그래서 BeanUtils를 통해 bean을 주입받아 사용 [ BeanUtils.getBean() ]
public class UserEntityListener {
    @PrePersist
    @PreUpdate
    public void prePersistAndPreUpdate(Object o){
        UserHistoryRepository userHistoryRepository = BeanUtils.getBean(UserHistoryRepository.class);

        User user = (User) o;
        UserHistory userHistory = new UserHistory();
        userHistory.setUserId(user.getId());
        userHistory.setName(user.getName());
        userHistory.setEmail(user.getEmail());

        userHistoryRepository.save(userHistory);
    }
}
BeanUtils.java 생성
@Component
public class BeanUtils implements ApplicationContextAware {
    private static ApplicationContext applicationContext;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        BeanUtils.applicationContext = applicationContext;
    }

    public static <T> T getBean(Class<T> clazz){
        return applicationContext.getBean(clazz);
    }
}
테스트 코드 및 실행결과
@Test
void userHistoryTest(){
	    User user = new User();
	    user.setEmail("test-new@gmail.com");
	    user.setName("Hong");
	
	    userRepository.save(user);
	
	    user.setName("Hong-new");
	
	    userRepository.save(user);
	
	    userHistoryRepository.findAll().forEach(System.out::println);
}
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user_history
        (created_at, email, name, updated_at, user_id, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        user
        (created_at, email, gender, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?, ?)

Hibernate: 
    select
        user0_.id as id1_2_0_,
        user0_.created_at as created_2_2_0_,
        user0_.email as email3_2_0_,
        user0_.gender as gender4_2_0_,
        user0_.name as name5_2_0_,
        user0_.updated_at as updated_6_2_0_ 
    from
        user user0_ 
    where
        user0_.id=?

Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user_history
        (created_at, email, name, updated_at, user_id, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    update
        user 
    set
        email=?,
        gender=?,
        name=?,
        updated_at=? 
    where
        id=?

UserHistory(id=6, userId=null, name=Hong, email=test-new@gmail.com, createdAt=2021-07-24T08:58:16.331828, updatedAt=2021-07-24T08:58:16.331828)
UserHistory(id=8, userId=7, name=Hong-new, email=test-new@gmail.com, createdAt=2021-07-24T08:58:16.489824, updatedAt=2021-07-24T08:58:16.489824)

5. 일반적으로 많이 사용하는 Listener 사용법(날짜, 등록자)

  • MyEntityListener처럼 등록날짜, 수정날짜, 등록자, 수정자는 많은 곳에서 사용 필요
  • JPA, Spring Boot에선 @EnableJpaAuditing, AuditingEntityListener.class, annotation을 통해 사용 가능
  • @CreatedDate: 등록날짜, @LastModifiedDate: 수정날짜
  • @CreatedBy: 등록자, @LastModifiedBy: 수정자
  • AuditingEntityListener의 실행 결과는 MyEntityListener의 결과 동일
BookmanagerApplication.java
@SpringBootApplication
@EnableJpaAuditing
public class BookmanagerApplication {

    public static void main(String[] args) {
        SpringApplication.run(BookmanagerApplication.class, args);
    }
}
User.java
...
@EntityListeners(value = {AuditingEntityListener.class, UserEntityListener.class})
public class User implements Auditable {
		...
		@Column(updatable = false)
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}

6. 코드 리팩터링 작업

  • createdAt, updatedAt은 여러 Entity에 공통으로 포함되어 있음
    BaseEntity를 만들어 그 곳에서 관리하는 것으로 수정
  • @MappedSuperclass: 상속받는 Entity에 컬럼으로 포함할 때 사용
  • @ToString(callSuper = true) : 부모 Entity도 toString에 포함할 때 사용
  • @EqualsAndHashCode(callSuper = true) : 부모 Entity도 동등 비교에 포함할 때 사용
BaseEntity.java
@Data
@MappedSuperclass
@EntityListeners(value = AuditingEntityListener.class)
public class BaseEntity {
    @Column(updatable = false)
    @CreatedDate
    private LocalDateTime createdAt;

    @LastModifiedDate
    private LocalDateTime updatedAt;
}
User.java
...
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
@EntityListeners(value = {UserEntityListener.class})
public class User extends BaseEntity implements Auditable {
		...

//    @Column(updatable = false)
//    @CreatedDate
//    private LocalDateTime createdAt;
//
//    @LastModifiedDate
//    private LocalDateTime updatedAt;
	
	...
}
Book.java
...
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class Book extends BaseEntity implements Auditable {
		...

//    @CreatedDate
//    private LocalDateTime createdAt;
//
//    @LastModifiedDate
//    private LocalDateTime updatedAt;
}
UserHistory.java
@Data
@ToString(callSuper = true)
@EqualsAndHashCode(callSuper = true)
public class UserHistory extends BaseEntity implements Auditable {
	...

//    @CreatedDate
//    private LocalDateTime createdAt;
//
//    @LastModifiedDate
//    private LocalDateTime updatedAt;
}
실행 결과
Hibernate:     
    create table book (
       id bigint not null,
        created_at timestamp,
        updated_at timestamp,
        author varchar(255),
        name varchar(255),
        primary key (id)
    )

...

//@ToString, @EqualsAndHashCode 선언 전
UserHistory(id=6, userId=null, name=Hong, email=test-new@gmail.com)
UserHistory(id=8, userId=7, name=Hong-new, email=test-new@gmail.com)

//@ToString, @EqualsAndHashCode 선언 후
UserHistory(super=BaseEntity(createdAt=2021-07-24T09:09:04.973194, updatedAt=2021-07-24T09:09:04.973194), id=6, userId=null, name=Hong, email=test-new@gmail.com)
UserHistory(super=BaseEntity(createdAt=2021-07-24T09:09:05.123189, updatedAt=2021-07-24T09:09:05.123189), id=8, userId=7, name=Hong-new, email=test-new@gmail.com)