JPA

Jpa Entity Listener

Bong Gu 2021. 11. 25. 02:53
728x90

JPA

JpaEntityListener

Event

  • 생성방법

    • ApplicationEvent 상속

      @Getter
      public class MyEvent extends ApplicationEvent {
        private int data;
      
        public MyEvent(object source) {
          super(source);
        }
      
        public MyEvent(object source, int data) {
          super(source);
          this.data = data;
        }
      }
    • Spring 4.2 부터는 ApplicationEvent를 상속받지 않아도 이벤트로 사용할 수 있다.

      @Getter
      public class MyEvent {
        private int data;
      
        public MyEvent(int data) {
          this.data = data;
        }
      }
  • 발행방법

    • ApplicationEventPublisher 사용

      @RequiredArgsConstructor
      public class MyService {
      
        private final ApplicationEventPublisher eventPublisher;
      
        public void publishEvent() {
               eventPublisher.publishEvent(new MyEvent(100));
        }
      }
    • ApplicationContext 사용

      @RequiredArgsConstructor
      public class MyService {
      
        private final ApplicationContext ctx;
      
        public void publishEvent() {
              ctx.publishEvent(new MyEvent(100));
        }
      }
  • Spring이 제공하는 Event

    • ContextRefreshEvent : ApplicationContext를 초기화하거나, 리프레쉬하는 경우 발생
    • ContextStartedEvent : ApplicationContext를 start하여 라이프사이클 Bean들이 시작신호를 받은 시점에 발생
    • ContextStoppedEvent : ApplicationContext를 stop하여 라이프사이클 Bean들이 정지 신호를 받은 시점에 발생
    • ContextCloseEvent : ApplicationContext를 close하여 싱글톤 빈들이 소멸되는 시점에 발생
    • RequestHandlerEvent : HTTP 요청을 처리했을 때, 발생

EventHandling

  • ApplicationListener 구현

    @Service
    public class MyListener implements ApplicationListener<MyEvent> {
    
      @Override
      public void execute(MyEvent event) {
        System.out.println("Event : " + event.getData());
      }
    }
  • Spring 4.2 부터는 ApplicationEvent를 상속하지 않아도되기 때문에, ApplicationListener를 구현하지 않고 어노테이션을 사용하여 Listener를 등록

    @Service
    public class MyListener {
    
      @Async
      @Order(Ordered.HIGHEST_PRECEDENCE)
      @EventListener
      public void execute(MyEvent event) {
        System.out.println("Event : " + event.getData());
      }
    }
    • @Order 를 사용하여 Listener 순서를 지정해 줄 수 있다.

@TransactionalEventListener

  • Spring 4.2 부터 사용 가능
  • Spring Transactio 상태에 따라 발생하는 이벤트를 처리해주는 이벤트 리스너
  • @EventListener 는 트랜잭션 범위내에서 동기적으로 실행되고, @TransactionalEventListener 는 커밋 전/후 등을 자유롭게 지정할 수 있다.
    • 트랜잭션이 없다면 장독하지 않는다. 단, fallbackExecution 이 설정되어있으면 작동한다.
  • @Async 를 사용하여 비동기로 이벤트를 처리할 수 있다.
    • @Async 를 걸지 않으면 이벤트처리를 마칠때 까지, DB커넥션을 놓지 않는다.
  • 설정
    • AFTER_COMMIT (default setting) : 커밋후에 동작
      • Entity를 수정해도 처리되지 않는다. 이미 커밋했기 때문에
      • 데이터 변경작업이 필요하다면 @Transactional(Propagtion.REQUIRES_NEW)를 설정해주자.
        • 이벤트 작업의 성공 /실패 여부가 영향끼지 않도록 REQUIRES_NEW 전략 사용
    • AFTER_ROLLBACK : 롤백후에 동작
    • AFTER_COMPLETION : 트랜잭션이 끝난후에 동작 (커밋 / 롤백 여부에 상관없이)
    • BEFORE_COMMIT : 커밋전에 동작
      • @Async 를 사용하지 말자.

EntityListeners

  • JPA Entity에서 이벤트가 발생할 때마다 콜백 메서드를 실행 시킬 수 있다.

    @Entity
    public class Entity {
      @Id
      @GeneratedValue
      private Long id;
    
      private int data;
    
      @PrePersist
      public void prePersist() {
        System.out.println("Pre Persist")
      }
    }
  • 콜백 메서드

    >>> 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
    • @PrePersist : insert
    • @PreRemove : delete
    • @PostPersist
    • @PostRemove
    • @PreUpdate : merge
    • @PostUpdate
    • @PostLoad : select 조회가 된 직후
    • PreUpdate는 실질적으로 UpdateSQL문이 실행되었을때, 실행이된다.
      즉, flush 또는 트랜잭션 종료시점
      이미변경된 값으로 조회가 도ㅚ어 결과정적으로 PostUpdate와 동일한 결과를 보일 수 있다.
      • @Transient 필드를을 사용해서, 이전값들을 알 수 있다.
  • 특정 EntityListener를 사용하는 방법

    @EntityListeners({AuditingEntityListener.class, MyListener.class})
    @Entity
    public class Entity {
      @Id
      @GeneratedValue
      private Long id;
    
      private int data;
    
    }
    • 여러 Entity에서 공통 Listener를 사용할 수 도 있다.
    • 여러 Listener도 등록 가능하다.

EntityListeners DI 방법

  • Spring Bean 생성시점이 달라서 DI가 잘 안된다.

  • EntityManagerFactory를 Bean으로 등록할 때, EntityListener에 대해서 Bean으로 등록하는 작업이 존재한다.

  • 따라서 EntityListener에서 EntityManagerFactory를 사용하는 Repository류의 Bean을 주입하면 문제가 발생한다.

  • 우회 방법

    • ApplicationContext

      public class MyListener {
      
        @Autowired
        private ApplicationCOntext ctx;
      
        @PrePersist
        public void prePersist(Entity entity) {
          EntityRepository repo = ctx.getBean(EntityRepository.class);
        }
      }
      • EntityListeners에 등록 시, 기본생성자가 필요하기 때문에 필드주입장식을 사용한다.
    • @Lazy

      public class MyListener {
      
        @Lazy
        @Autowired
        private EntityRepository repository
      
        @PrePersist
        public void prePersist(Entity entity) {
        }
      }
      • context refresh 시점에는 proxy 였다가, 사용 시 초기화
    • BootstrapMode Deferred or Lazy

      @EnableJpaRepositories(bootstrapMode = BootstrapMode.DEFERRED)
      spring:
        data:
          jpa:
            repositories:
              bootstrap-mode: deferred
      • BootstrapMode를 Deffrred로 설정하게 되면, JpaRepositories를 proxy로 생성 해준다.
      • 또한, Spring context가 load하는 thread와 다른 thread를 이용해서 작업이 진행되고, ContextRefreshedEvent에 trigger에 의해서 repository가 초기화가 진행된다.
      • 결론은 @Lazy와 비슷하게 동작 하지만 application이 시작 전에 Repository들이 초기화가 보장되어 있고, load 속도도 빨라진다.
      • BootstrapMode를 변경하는 방법은 @EnableJpaRepositories과, properties를 이용해서 설정 가능하다.
      • Lazy의 경우는 앞에 설명한 방식을 전체 Repository에 일괄 적용 해주게 된다.
      • Lazy로 Application을 시작하는 경우 런타임시에 문제가 발생할 수 있으니 주의해야 한다.
  • 우회 방법들이 좋아 보이지는 않아 다른방법을 찾아보던중, 다른 event를 발행하여 처리하는 방법을 찾았다.

    • EntityListeners 등록

      @EntityListeners(MyListener.class)
      @Entity
      public class Entity {
        @Id
        @GeneratedValue
        private Long id;
      
        private int data;
      
      }
    • 다른 이벤트를 발행

      @Getter
      public class TheOtherEvent {
      
        private Entity entity;
      
        public TheOtherEvent(Entity entity) {
          this.entity = entity;
        }
      }
      public class MyListener {
      
        @Autowired
        private ApplicationEventPublisher eventPublisher;
      
        @PrePersist
        public void prePersist(Entity entity) {
              eventPublisher.publishEvent(new TheOtherEvent(entity));
          eventpublisher.publishEvent(new SomethingEvent(entity));
        }
      }
      @RequiredArgsConstructor
      @Service
      public class TheOtherEventListener {
      
          private final EntityRepository repository
      
          @EntityListener
        public void prePersist(TheOtherEvent event) {
              Entity entity = event.getEntity();
          repository.save(entity);
        }
      }
      • ApplicationEventPublisher가 DI 되는 이유는 사용자가 생성한 빈이 아닌, 스프링에서 제공하는 이미 등록된 빈으로 MyListener 생성보다 순서가 빠르다.

참고

728x90