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를 잘 활용하여 컴포넌트 간을 느슨하게 결합시켜 재사용 가능하며 확장 가능하도록 만들자