본문 바로가기

Programming/Java & JSP & Spring

Spring Event에 대해서 알아보자

Spring에서 이벤트를 발생시켜 핸들링하는 방법으로는 여러가지가 있다. 그 중에서도 어노테이션을 활용하는 몇가지만 알아보자.

이벤트는 어떤 경우에 사용할까?

보통의 경우에는 그렇게 중요하지 않은 SIDE격 작업을 비즈니스 로직과 느슨하게 연결하기 위해 사용된다.

이벤트 어노테이션 종류

  • @EntityListeners
  • @EventListener
  • @TransactionEventListener

@EntityListeners

엔티티를 DB에 적용하기 이전 이후에 커스텀 콜백을 요청할 수 있는 어노테이션이다.

@Entity
@Table(name = "product_item")
@EntityListeners(ProductItemListener::class)
data class ProductItem

위와 같이 Entity에 Listner를 적용하고 Listner 클래스를 다음과 같이 작성한다.

class ProductItemListener { 
    @PreUpdate 
    fun onPreUpdate(item: ProductItem) {
    	... 
    } 
    
    @PostUpdate 
    fun onPostUpdate(item: ProductItem) {
    	... 
    } 
    
    @PostPersist fun onPostPersist(item: ProductItem) { 
    	... 
    } 
}
  • PreUpdate : 업데이트 이전에 실행
  • PostUpdate : 업데이트 이후에 실행
  • PreRemove : 삭제되기전 실행
  • PostPersist : 저장 이후에 실행

이렇게만 적용하면 Jpa Save 트랜잭션 종료 후 COMMIT이 되고 실제 DB SQL문이 실행 되기 전후에 위에 지정해둔 이벤트가 발생하고 Listener가 작동한다.

주의할 점

  • 이벤트는 repository 호출 시점이 아니다. 트랜잭션 종료 후 실제 COMMIT이 되고 난 후다.
  • Listener Class는 스프링 기반이 아니라 javax.persistence 를 이용하는 것이므로 빈이 아니다.
  • 빈이 아니라는 말은 Listener 안에서 @Autowired 같은 어노테이션으로 빈을 주입받을 수 없다는 말을 의미한다. 그래서 Listener안에서 빈을 사용하고 싶다면 애플리케이션 컨텍스트에서 직접 빈을 얻어와야한다.

EventListener

Entity 이벤트 말고 비즈니스 로직에서 Event를 발생시키고 싶을 수도 있다.

예를 들어, 아이템을 구매했는데 구매했다는 SMS을 보내고 싶을 때 등을 들 수 있다.

 

그럼 이벤트를 발생시키고 받아서 처리하는 Listener를 작성해보자.

 

우선 이벤트는 어떻게 발생시킬 수 있을까?

ApplicationEventPublisher로 이벤트를 발생시킬 수 있다.

class ProductItemEventPublisher { 
    @Autowired 
    private lateinit var publisher: ApplicationEventPublisher 
    
    fun publishEvent(item: ProductItem) { 
    	publisher.publishUpdateEvent(ProductItemEvent(item, EntityCrud.UPDATE)) 
    }

publisher를 생성했으니 위 코드에서 이벤트를 발생시켜보자

@Transaction 
class ProductItemService( 
    private val publisher: ProductItemEventPublisher 
) { 
    fun buy(item: ProductItem) { 
       ... // do something 
       publisher.publishEvent(item) // 이벤트 발생 
    } 
}

이벤트를 잡아서 리스너는 다음과 같이 작성한다.

@EventListener 
fun listenProductItemEvent(event: ProductItemEvent) { 
   ... // SMS 발송 등 
}

위와 같이 @EventListner 로 처리하면 이벤트가 발생하는 순간 바로 실행이 된다.

하지만 이렇게 되면 Event가 발생해서 바로 핸들링 처리를 하게되면 핸들링하는 작업도중 에러가 발생한다면 트랜잭션 비즈니스 로직 자체도 같이 묶여 있으므로 에러가 발생한다.

SMS발송이 실패했다고 아이템 구매도 안되버리면 난감할 것이다. 좀 더 느슨하게 만들어보자.

TransactionEventListener

트랜잭션 동기화를 할 수 있지만 비즈니스 트랜잭션과는 무관하게 좀 더 느슨하게 이벤트를 처리하고 싶다면

@TransactionEventListener를 사용하면된다.

해당 리스너는 이벤트가 발생한 트랜잭션이 종료되면 실행된다.

@Async 
@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT, fallbackExecution = true, condition = "...") 
fun listenUpdateEvent(event: ProductItem) { ... }
  • phase : COMMIT 이후에 실행할 것인지 이전에 실행할 것인지 실행 순서를 제어할 수 있다
  • fallbackExecution : 트랜잭션과는 무관하게 항상 실행할 것인지
  • condition : 이벤트 리스너가 작동하는 조건 설정

실행 순서를 한번 살펴보자.

  1. 아이템 구매 로직 실행 (트랜잭션 시작)
  2. 아이템 구매 로직 종료 (트랜잭션 종료)
  3. 이벤트 리스너 작동 (BEFORE_COMMIT)
  4. COMMIT
  5. 이벤트 리스너 작동 (AFTER_COMMIT)

EntityListener + EventListener + TransactionEventListener (???)

EntityListener가 기본적으로 작동하지만 더욱 복잡한 로직을 리스너에 녹여야해서 그러기엔 부담이라 EntityListener에서 한번 더 이벤트를 발생시켰다고 가정해보자.

  1. Entity 이벤트 발생
  2. EntityListener 이벤트 감지하고 작동
  3. EntityListener에서 또 다른 이벤트를 발생시킴
  4. @TransactionEventListener 가 해당 이벤트 감지

위와 같은 순서로 작동될 것이다. 하지만 여기서 주의해야 할 점이 있다.

1번이 발생했다는 뜻은 이미 COMMIT이 되었다는 뜻이다.

그렇다는 말은 4번에서 옵션으로 BEFORE_COMMIT 을 사용한다면 작동하지 않는다는 것이다.

 

이번엔 세가지를 동시에 사용한다 가정해보자.

 

이럴 경우엔 작동 순서에만 주의를 기울여 작업을 하면 된다.

아래와 같은 순서로 작동이 된다.

  1. 아이템 구매 로직 실행 (트랜잭션 시작)
  2. 이벤트 발생 (publisher 사용)
  3. @EventListener 작동
  4. 아이템 구매 로직 종료 (트랜잭션 종료)
  5. @TransactionEventListener BEFORE_COMMIT 작동
  6. COMMIT !
  7. @PreUpdate 작동 (EntityListener)
  8. DB SQL문 실행
  9. @PostUpdate 작동 (EntityListener)
  10. @TransactionEventListener AFTER_COMMIT 작동