본문 바로가기

개인적으로 공부한 것을 정리해 놓은 블로그입니다 틀린 것이 있으면 댓글 부탁 드립니다!


JPA/Spring data JPA

SPRING DATA JPA 공부 5 - EntityListener 활용

반응형

EntityListener는 해당 엔티티 라이프 사이클 중 특점 시점에 원하는 로직을 처리하도록 한다 . 

 

총 7가지가 있으며 각각 엔티티의 라이프 사이클에 맞게 동작한다 

 

@NoArgsConstructor
@AllArgsConstructor
@RequiredArgsConstructor
@Data
@Builder
@Entity
public class User {

    @Id @GeneratedValue
    private Long id;
    
    @NonNull
    private String name;
    
    @NonNull
    private String email;

    private String nickName;

    @Transient
    private String testData;

    @Enumerated(value = EnumType.STRING)
    private Gender gender;

    private LocalDateTime createdAt;
 
    private LocalDateTime updatedAt;

    /*
        Entity Listener 종류
        이벤트가 발생하기 직전 혹은 직후에 해당 이벤트 발생을 감지하고 정의한 로직을 처리한다.
        @PrePersist
        @PreUpdate
        @PreRemove
        @PostPersist
        @PostUpdate
        @PostRemove
        @PostLoad
     */


    @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
    private void postUpdate(){
        System.out.println(">>>postUpdate");
    }

    @PreRemove
    private void preRemove() {
        System.out.println(">>>preRemove");
    }

    @PostRemove
    private void postRemove(){
        System.out.println(">>>postRemove");
    }

    @PostLoad
    private void postLoad(){
        System.out.println(">>>postLoad");
    }



}

 

확인을 위해각 리스너 별로 해당 리스너의 이름이 찍히게 하는 메서드를 만들어 봤다 . 

 

테스트를 작성해 확인해 보자 

 

 

@Test
    void entityListenerTest (){
        User user = new User();
        user.setName("ugo");
        user.setEmail("staasd04@gmail.com");
        
        
        //insert
        userRepository.save(user);

		//update
        User user2 = userRepository.findById(1L).orElseThrow(RuntimeException::new);
        user2.setName("ugo222");
        userRepository.save(user2);
        
        //delete
        userRepository.deleteById(1L);
        
}

 

 

테스트를 돌려보면 각각의 쿼리 이전 이후로 해당 메서드의 로그가 찍힌다 .

 

 

 

postLoad의 경우는 엔티티가 select 되어 로딩될때 찍힌다.

 

 

 

@PrePersist @PreUpdate 두가지를 많이 쓰는데 이유는 보통 해당 엔티티가 언제 생성되었는지 언제 수정되었는지에 대한

 

정보를  갖기 때문에 엔티티에 정보를 넣을때 마다 그 값을 넣는 것 보다는 어노테이션을 사용해 insert 시점이나 update 시점에 들어 가도

 

록 할 수 있기 때문이다 . 

 

 

    @PrePersist
    public void prePersist(){
        this.setCreatedAt(LocalDateTime.now());
        this.setUpdatedAt(LocalDateTime.now());
    }

    @PreUpdate
    public void preUpdate(){
        this.setUpdatedAt(LocalDateTime.now());
    }

 

간단하다 해당 엔티티에 원하는 어노테이션을 붙혀 메서드를 만들면된다. 

 

생성시점에 createdAt , UpdatedAt 이 set 될 것이며 update 시점에 UpdatedAt이 업데이트 될 것이다 .

 

테스트 해보자

 

@Test
    void preUpdateTest(){
        //유저 생성
        User user = new User();
        user.setEmail("ugo@gmail.com");
        user.setName("ugo");

        //insert
        userRepository.save(user);

        //insert된 User의 updatedAt 과 update된 후의 updatedAt 을 비교 해보려 한다.
        LocalDateTime createdAt = userRepository.findByEmail("ugo@gmail.com").getUpdatedAt();
        System.out.println("updateAtWhenCreated = " + createdAt);

        //update
        User byEmail = userRepository.findByEmail("ugo@gmail.com");
        byEmail.setName("ugogogo");
        userRepository.save(byEmail);

        //update된 updatedAt
        LocalDateTime updatedAt = userRepository.findByEmail("ugo@gmail.com").getUpdatedAt();

        System.out.println("updatedAt = " + updatedAt);

        Assertions.assertNotEquals(updatedAt,createdAt);

    }

 

insert 시점의 updateAt과 update 시점의 updateAt이 달라야한다. 

 

테스트가 잘 통과 됬고 결과는 아래 그림과 같다 .

 

 

 

 

위와 같은 경우 다른 엔티티에도 똑같이 사용되는 것이기 때문에 리스너를 만들어서 해당 기능을 원하는 엔티티에서 사용할 수 있게 만들어 주는 것이 좋다 . 

 

 

리스너를 만들어 사용하는 엔티티에서 @EntityListener(리스너클래스.class) 을 지정하면 해당 기능이 사용가능하다 . 

 

리스너를 만들어보자

 

package com.ugo.jpatest.domain.entitylistener;


import javax.persistence.PrePersist;
import javax.persistence.PreUpdate;
import java.time.LocalDateTime;

public class TimeStampEntityListener {

    //리스너는 여러 곳에서 사용되고 파라미터로 들어오는 것이 어떤타입인지 모르기 떄문에 Object로 받아야 한다.
    //현재 리스너를 사용하는 것은 Book 과 user 이다 .
    //여러 곳에서 사용하고 있기 떄문에 인터페이스를 만들어 사용하는 곳에서 인터페이스를 구현하게 하면 코드를 줄일 수 있다.
    //만약 인터페이스를 사용하지 않는다면 if ( instanceof )로 타입검사를 하는 것이 리스너를 사용하는 클래스 수만큼 늘어날 것이다.
    @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());
        }
    }
}

 

TimeStampEntityListener 클래스를 만들었다 해당 클래스에 메서드를 정의해놓으면 사용하는 엔티티에서는 해당 기능을 직접

 

구현하지 않아도 기능이 동작한다 . 

 

리스너에 @Prepersist @PreUpdate가 달린 메서드를 만들었다 . 

 

메서드는 여러 곳에서 사용될꺼고 리스너는 누가 어디서 사용하는지 알 수 없다 

 

이를 위해서 파라미터로 Object  타입으로 받게 강제되어있다.

 

파라미터를 Object 로  받아서 메서드 안에서 instanceof로 타입 검사 후에 캐스팅하여 사용하면 된다 . 

 

여기서 문제는 해당 리스너를 사용하는 클래스 갯수만큼 타입 검사를 해야해 반복적인 코드를 작성해야 한다. 

 

이럴떄 인터페이스를 사용하면 좋다 . 

 

Auditable 이라는 인터페이스를 만들었다 

 

package com.ugo.jpatest.domain.entitylistener;

import java.time.LocalDateTime;

public interface Auditable {
    LocalDateTime getCreatedAt();
    LocalDateTime getUpdatedAt();

    void setCreatedAt(LocalDateTime localDateTime);
    void setUpdatedAt(LocalDateTime localDateTime);
}

 

 

Auditable 인터페이스를 구현하는 객체는 다형성을 통해 Auditable 타입으로 사용할 수 있으니

 

auditable 인터페이스에 리스너에서 사용될 메서드들만 정의해 놓으면 타입 검사는 auditable 한가지로만 하면된다 . 

 

리스너에서는 createdAt과 updatedAt 두가지를 set해주는 기능이 필요하다.

 

auditable이 갖어야할 메서드는 setCreatedAt(LocalDateTime ldt), setUpdatedAt(LocalDateTime ldt) 두 가지면 될 것 같다 . 

 

 리스너로 돌아와 보자 

 

@PrePersist를 단 메서드를 만들고  파라미터를 오브젝트로 받는다 , auditable 인터페이스로 타입 검사를 한다. 

 

Auditable 타입이면 if문 안으로 들어올 것이다  . if 문안에서 Auditable로 캐스팅하고 createdAt과 updatedAt을 set 해준다 . 

 

user와 book에 있던 @Prepersist @PreUpdate 메서드를 지우고 동작을 확인해 보자 

 

잘된다 . 

 

한 군데에서 사용될 리스너기능 같은경우에는 해당엔티티에 직접 정의하고  

 

여러군데에서 사용될 리스너기능은 리스너를 직접 구현해 사용하는 것이 좋을 것 같다 .  

 

 

 

반응형