관리 메뉴

개발그래머

Spring Event[2] @TransactionalEventListener 사용법 본문

Spring

Spring Event[2] @TransactionalEventListener 사용법

임요환 2024. 2. 4. 19:30

Spring의 @TransactionalEventListener를 사용하는 이유?(@EventListener와의 차이점)

  • Spring 4.2부터 이벤트리스너를 트랜잭션 단계에서 바인딩할 수 있는 @EventListener의 확장 애노테이션
  • 트랜잭션 내에서 이벤트를 수신하고 처리함
  • 기존 이벤트리스너는 데이터의 불일치가 생길 가능성이 있음
@Transactional 
fun save() { 
    aRespository.save(); 
    eventPublisher.publishEvent(aEvent); 

    bRepository.save(); 
    eventPublisher.publishEvent(bEvent); 
}
  • bRepository를 저장하는 과정에서 에러가 발생한다면 aEvent가 만약 비동기로 발행된 상황이면 예상하지 못한 데이터가 저장될 가능성이 있음
  • @TransactionEventListener를 사용하면 해당 트랜잭션에 대하여 이벤트가 발행되므로 위와 같은 상황을 피할 수 있음

@TransactionalEventListener의 옵션

  • phase에 아래의 옵션을 사용하여 어떤 타이밍에 동작하는지 정의할 수 있음
  • AFTER_COMMIT(기본) = 트랜잭션이 성공적으로 커밋되면 동작함
  • AFTER_ROLLBACK = 트랜잭션이 롤백되면 동작함
  • AFTER_COMPLETION = 트랜잭션이 완료되면 동작함(AFTER_COMMIT, AFTER_ROLLBACK)
  • BEFORE_COMMIT = 트랜잭션이 커밋되기 전에 동작함

주의사항

트랜잭션이 없으면 event가 발행되지 않음

@Service
class PostService(
    private val postRepository: PostRepository,
    private val publisher: ApplicationEventPublisher // 이벤트 발행
) {
    private val logger: Logger = LoggerFactory.getLogger(PostService::class.java)

    fun savePost(): Post {
        val savedPost = postRepository.save(Post("첫글", "첫글입니다", "잡담"))
        publisher.publishEvent(PostSaveEvent(savedPost)) // 이벤트가 발행되지 않는다.
        return savedPost
    }
}
  • @TransactionEventListener는 트랜잭션이 존재하는 상태에서 이벤트를 발행할 수 있음
  • 위와 같이 @Transactional이 없는 상태에서 이벤트를 발행하게 되면 이벤트는 발행되지 않음

@TransactionalEventListener의 AFTER_COMMIT 상태에서 DB에 쓰는 행위를 할 때 새로운 트랜잭션을 얻어야 함

@Component
class PostSaveEventListener(
    private val postRepository: PostRepository
) {
    private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)

    @TransactionalEventListener
    fun listen(event: PostSaveEvent) {
        // send mail
        event.post.apply {
            this.content = "수정된 본문"
        }
        postRepository.save(event.post) // 동작하지 않음
    }
}
  • 동일한 쓰레드의 트랜잭션이 완료되면 다시 사용할 수가 없음
  • 이럴 때 @Transactional(propagation = Propagation.REQUIRES_NEW)를 사용하여 새로운 트랜잭션을 얻어 위의 동작을 정상적으로 마무리할 수 있음
  • 또는 비동기로도 처리가능함

BEFORE_COMMIT은 언제 사용하는지?

  • BEFORE_COMMIT이 과연 사용될 일이 있을까라는 생각을 하면서 고민해 봄
  • 동기적으로 사용되는 위와 같은 상황에서 BEFORE_COMMIT을 사용하면 새로운 트랜잭션을 얻지 않아도 동작이 정상적으로 마무리됨
@Component
class PostSaveEventListener(
    private val postRepository: PostRepository
) {
    private val logger: Logger = LoggerFactory.getLogger(PostSaveEventListener::class.java)

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    fun listen(event: PostSaveEvent) {
        // send mail
        event.post.apply {
            this.content = "수정된 본문"
        }
        postRepository.save(event.post) // 동작함
    }
}
  • phase를 BEFORE_COMMIT으로 하게 되면 이전에 사용한 트랜잭션을 같이 사용하게 됨
  • 최종적으로는 비즈니스 로직의 트랜잭션과 이벤트 로직의 트랜잭션이 동일하게 됨 -> 이벤트를 사용하는 이유가 없어지는 느낌

요약

  • Event를 잘 활용하여 컴포넌트 간을 느슨하게 결합시켜 재사용 가능하며 확장 가능하도록 만들자