JPA EntityListener

이종윤·2022년 2월 6일
0

Spring JPA

목록 보기
8/23

🤔EntityListener??

엔티티 리스너란? 이벤트를 관찰하고 있다가, 이벤트가 발생하면 특정 동작을 진행하는 것. Entity리스너 이기 때문에 엔티티가 동작하는 몇가지 방법에 이벤트를 리스닝 하고 있다.

✅ Entity에 Listener설정

    @PrePersist     // Insert호출 전 실행
    @PreUpdate      // marge 호출 전 실행
    @PreRemove      // Delete호출 전 실행
    @PostPersist    // Insert호출 후 실행
    @PostUpdate     // marge 호출 후 실행
    @PostRemove     // Delete호출 후 실행
    @PostLoad       // select호출 후 실행
UserEntity
...
...
    @PrePersist
    public void prePersist(){
        System.out.println(">>> prePersist");
    }
    @PreRemove
    public void preRemove(){
        System.out.println(">>> preRemove");
    }
    @PostPersist
    public void postPersist(){
        System.out.println(">>> postPersist");
    }
    @PostUpdate
    public void postUpdate(){
        System.out.println(">>> postUpdate");
    }
    @PostRemove
    public void postRemove(){
        System.out.println(">>> postRemove");
    }
    @PostLoad
    public void postLoad(){
        System.out.println(">>> PostLoad");
   }
...
...

위와같이 Entity에 설정해 두면 trigger처럼 쿼리가 실행된다.

✅ 생성시간과 수정시간 (Default설정??)
현업에서는 User같은 Entity를 만들때 생성 일, 수정 일 을 넣는게 국룰이다.
그럼

    @Test
    void prePersistTest() {
        User user = new User();
        user.setEmail("martin2@fast.com");
        user.setName("martin");
        user.setCreatedAt(LocalDateTime.now());
        user.setUpdatedAt(LocalDateTime.now());
        userRepository.save(user);

        System.out.println(userRepository.findByEmail("martin2@fast.com"));

    }

🤔🤔 위의 코드 처럼 일일이 setCreatedAt,setUpdatedAt 을 해줘야 할까?
Dont Repeat Yourself DRY 법칙에 어긋난다. 그리고 개발자가 실수로 CreatedAt을 넣지 않으면 데이터 정확성이 떨어지게 된다.
그래서 Entity에 prePersist 설정을 해서 자동으로 해당 값을 Set하는 방법을 실행하는게 좋다.

    @PrePersist
    public void prePersist(){
        System.out.println(">>> prePersist");
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }
    @PreRemove
    public void preUpdate(){
        System.out.println(">>> preUpdate");
        this.updatedAt = LocalDateTime.now();
    }

TEST 해보자

    @Test
    void prePersistTest() {
        User user = new User();
        user.setEmail("martin2@fast.com");
        user.setName("martin");
        userRepository.save(user);

        System.out.println(userRepository.findByEmail("martin2@fast.com"));

    }

    @Test
    void preUpdateTest(){
        User user = userRepository.findById(1L).orElseThrow(RuntimeException::new);

        System.out.println("as - is (기존 유저값) : " + user);

        user.setName("marttttin2");
        userRepository.save(user);

        System.out.println("to - be (변경된 값) : " + userRepository.findAll().get(0));
    }
>>> prePersist
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user
        (created_at, email, gender, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.created_at as created_2_0_0_,
        user0_.email as email3_0_0_,
        user0_.gender as gender4_0_0_,
        user0_.name as name5_0_0_,
        user0_.updated_at as updated_6_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
Hibernate: 
    select
        user0_.id as id1_0_0_,
        user0_.created_at as created_2_0_0_,
        user0_.email as email3_0_0_,
        user0_.gender as gender4_0_0_,
        user0_.name as name5_0_0_,
        user0_.updated_at as updated_6_0_0_ 
    from
        user user0_ 
    where
        user0_.id=?
>>> preUpdate
Hibernate: 
    update
        user 
    set
        created_at=?,
        email=?,
        gender=?,
        name=?,
        updated_at=? 
    where
        id=?
Hibernate: 
    select
        user0_.id as id1_0_,
        user0_.created_at as created_2_0_,
        user0_.email as email3_0_,
        user0_.gender as gender4_0_,
        user0_.name as name5_0_,
        user0_.updated_at as updated_6_0_ 
    from
        user user0_
to - be (변경된 값) : User(id=1, name=marttttin, email=martin@fast.com, gender=null, createdAt=2022-02-06T17:50:52.948, updatedAt=2022-02-06T17:50:53.213)

EntityListener 사용

🤔 그럼 엔티티리스너는 언제 나오냐?

먼저 setting 하자

Book(Entity)와

@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Book {

    @Id
    @GeneratedValue
    private Long id;

    private String name;
    private String author;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;

    @PrePersist
    public void prePersist() {
        System.out.println(">>> prePersist");
        this.createdAt = LocalDateTime.now();
        this.updatedAt = LocalDateTime.now();
    }

    @PreUpdate
    public void preUpdate() {
        System.out.println(">>> preUpdate");
        this.updatedAt = LocalDateTime.now();
    }

}

BookRepository와

public interface BookRepository extends JpaRepository<Book, Long> {


}

BookRepositoryTest를 만들고

@SpringBootTest
public class BookRepositoryTest {
    @Autowired
    private BookRepository bookRepository;

    @Test
    void bookTest() {
        Book book = new Book();
        book.setName("Jpa 초격차 패키지");
        book.setAuthor("패스트캠퍼스");
        bookRepository.save(book);
        System.out.println(bookRepository.findAll());
    }
}

🤚 자 그럼 여기서 Entity에 직접 @PrePersist같은 어노테이션을 계속 써야할까? createdAt updatedAt은 계속 써야하기때문에 Entity에 적어도 결국 코드 중복이다. 이럴때 사용하는 것이 EntityListener이다.

Auditable 인터페이스 하나 만들어준다. 그럼 User와 Book은 interface를 받아주면된다.

Auditable

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

    void setCreatedAt(LocalDateTime createdAt);
    void setUpdatedAt(LocalDateTime updatedAt);

}

Listener를 써보자.

MyEntityListener

public class MyEntityListener {
    @PrePersist
    public void prePersist(Object o) { //해당 오브젝트를 파라미터로 받아야하기 때문에 오브젝트를 강제한다
        if (o instanceof Auditable) {  //받아오 오브젝트가 Auditable 객체인지 확인하고
            ((Auditable) o).setCreatedAt(LocalDateTime.now());
            ((Auditable) o).setUpdatedAt(LocalDateTime.now());
        }
    }

    @PreUpdate
    public void preUpdate(Object o){
        if (o instanceof Auditable) {  //받아오 오브젝트가 Auditable 객체인지 확인하고
            ((Auditable) o).setUpdatedAt(LocalDateTime.now());
        }

    }
}

User,Book클래스위에 리스너를 사용하겠다고 붙인다.

@EntityListeners(value = MyEntityListener.class)

이놈들은 다 주석 처리하고

//    @PrePersist
//    public void prePersist() {
//        System.out.println(">>> prePersist");
//        this.createdAt = LocalDateTime.now();
//        this.updatedAt = LocalDateTime.now();
//    }
//
//    @PreUpdate
//    public void preUpdate() {
//        System.out.println(">>> preUpdate");
//        this.updatedAt = LocalDateTime.now();
//    }

TEST한번 해보자

@SpringBootTest
public class BookRepositoryTest {
    @Autowired
    private BookRepository bookRepository;

    @Test
    void bookTest() {
        Book book = new Book();
        book.setName("Jpa 초격차 패키지");
        book.setAuthor("패스트캠퍼스");
        bookRepository.save(book);
        System.out.println(bookRepository.findAll());
    }
}

그럼 엔티티리스너를 통해 해당값을 주입 할 수 있다.

Hibernate: 
    insert 
    into
        book
        (author, created_at, name, updated_at, id) 
    values
        (?, ?, ?, ?, ?)
Hibernate: 
    select
        book0_.id as id1_0_,
        book0_.author as author2_0_,
        book0_.created_at as created_3_0_,
        book0_.name as name4_0_,
        book0_.updated_at as updated_5_0_ 
    from
        book book0_
[Book(id=6, name=Jpa 초격차 패키지, author=패스트캠퍼스, createdAt=2022-02-06T18:45:20.434, updatedAt=2022-02-06T18:45:20.434)]

위와 같이하면 로직 공통적인 부분에 대해서 리스너를 하나만 구현하고도 여러곳에서 @EntityListen를 통해 참조해서 사용하여 반복적인 코드를 줄일 수 있다.


흔하게 사용하는 Listener

User정보는 수정된 내용의 히스토리가 필요로 할 수 있다. UserHistory를 만들어보자.

✅ 유저엔티티가 동작할때 리스너가 동작해야함으로 User클래스 위에 @EntityListeners를 달아준다.

@EntityListeners(value = {UserEntityListener.class})

✅ UserHistory데이터를 담을 엔티티를 만들어준다.

@Entity
@NoArgsConstructor
@Data
public class UserHistory {
    @Id
    @GeneratedValue
    private Long id;
    private Long userId;
    private String name;
    private String email;
    private LocalDateTime createdAt;
    private LocalDateTime updatedAt;
}

✅ 유저 엔티티 리스너를 만들어준다.

public class UserEntityListener {
    /*
     * User의 정보가 생성,수정 될때, 그 User객체를 받아서 History정보를 저장한다.
     * */

//    @Autowired
//    private UserHistoryRepository userHistoryRepository;

    @PrePersist
    @PreUpdate
    public void prePersistAndPreUpdate(Object o) {
        /*
        * 여기서 주의해야할 점은 Listener는 @Autowired로 빈을 가져오지 못한다. 그래서 BeanUtils클래스를 이용해서 주입해 줘야 한다.
        * */
        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);
    }
}

✅ Bean을 쓰지못하는 리스너의 때문에 BeanUtils를 사용해 주입시켜줄수있다.

@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 해보자.

    @Test
    void userHistoryTest() {
        User user = new User();
        user.setEmail("martin-new@fastcampus.com");
        user.setName("martin-new");

        userRepository.save(user);

        user.setName("martin-new-new");

        userRepository.save(user);

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

✅ 결과 console

User 생성 및 수정 하기전에 History남기는 쿼리
Hibernate: 
    insert 
    into
        user_history
        (created_at, updated_at, email, name, user_id, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    insert 
    into
        user
        (created_at, updated_at, email, gender, name, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    select
        user0_.id as id1_2_0_,
        user0_.created_at as created_2_2_0_,
        user0_.updated_at as updated_3_2_0_,
        user0_.email as email4_2_0_,
        user0_.gender as gender5_2_0_,
        user0_.name as name6_2_0_ 
    from
        user user0_ 
    where
        user0_.id=?
User 생성 및 수정 하기전에 History남기는 쿼리
Hibernate: 
    call next value for hibernate_sequence
Hibernate: 
    insert 
    into
        user_history
        (created_at, updated_at, email, name, user_id, id) 
    values
        (?, ?, ?, ?, ?, ?)
Hibernate: 
    update
        user 
    set
        created_at=?,
        updated_at=?,
        email=?,
        gender=?,
        name=? 
    where
        id=?
Hibernate: 
    select
        userhistor0_.id as id1_3_,
        userhistor0_.created_at as created_2_3_,
        userhistor0_.updated_at as updated_3_3_,
        userhistor0_.email as email4_3_,
        userhistor0_.name as name5_3_,
        userhistor0_.user_id as user_id6_3_ 
    from
        user_history userhistor0_
UserHistory(id=6, userId=null, name=martin-new, email=martin-new@fastcampus.com, createdAt=null, updatedAt=null)
UserHistory(id=8, userId=7, name=martin-new-new, email=martin-new@fastcampus.com, createdAt=null, updatedAt=null)

위와 같이 User Insert update 할때 "User 생성 및 수정 하기전에 History남기는 쿼리"가 뜬다.
그리고 히스토리를 select했을때도 잘 들어가 있는걸 확인 할 수 있다.
🤔 하지만 생성일과 수정일 모두 null로 표시되어있으니 UserHistory 엔티티에도 MyEntityListener를 달아주자.

생성시간 수정시간같은 Auditing은 많이 사용되는 기능이기 때문에 기본리스너를 재공해준다. 기본리스너 사용해보자.

✅ @EnableJpaAuditing을 BookmanagerApplication에 달아주자.

@EnableJpaAuditing
public class BookmanagerApplication {
...
}

✅ User,Book,UserHistory 클래스에 MyEntityListener대신 AuditingEntityListener를 추가해준다.

✅ 그 후 아래와 같이 어노테이션을 달아준다.

@CreatedDate // 생성될때 자동으로 now들어간다.
private LocalDateTime createdAt;
@LastModifiedDate // 수정될때 자동으로 now들어간다.
private LocalDateTime updatedAt;

✅ @CreatedBy, @LastModifiedBy는 시간 뿐만아니라 누가 했는지 까지 기록한다.


리펙토링 해보자.

✅ 공통적으로 사용하는 createdAt,updatedAt을 묶어주고 @MappedSuperclass설정 해준다.

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

최종적으로 아래와 같이 된다.

profile
OK가자

0개의 댓글