[Spring] @TransactionalEventListener, 트랜잭션 안에서 이벤트 처리까지
@TransactionalEventListener
왜 쓰나요?
@EventListener
가 적용된 함수를 B라 하고, B를 호출되도록 이벤트 발행 로직을 포함한 함수를 A라 해보자.
이전 글에서 살펴봤듯이 이벤트를 처리하는 구조로 분리하면서 느슨한 결합을 만들게 됐지만, A에서 문제가 생겼을 때 B에서 수행한
작업은 같이 롤백되지 않는다는 문제점이 존재한다. 이런 점을 해소하기 위해 트랜잭션을 걸어서 원자적으로 수행되도록 만들 수 있다.
트랜잭션 설정 - phase
@TransactionalEventListener
는 phase라는 값을 설정해서 어떤 상황이면 이벤트를 실행하는지에 대해 세세하게 설정할 수
있다. 아래 4가지 설정을 참조하자. (여기서 언급하는 호출부
는 위에서 설명한 A->B 구조에서 A 함수를 의미함)
- (기본값) AFTER_COMMIT - 호출부의 트랜잭션을 commit하면 이벤트 실행
- AFTER_ROLLBACK – 호출부의 트랜잭션이 rollback되면 이벤트 실행
- AFTER_COMPLETION – 호출부의 트랜잭션이 commit되거나 rollback하면 이벤트 실행
- BEFORE_COMMIT - 호출부의 트랜잭션 commit 전에 이벤트 실행
아래처럼 적용해주면 된다!
@TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
fun foo() {...}
테스트
Listener까지 트랜잭션이 잘 유지되는지 - 저장 테스트
TestService
에서 이벤트를 발행한다고 해보자. 그리고 잘 저장되는지 확인하기 위해 save()
도 호출하고, 트랜잭션도 건 상태다.
@Service
class TestService(
private val applicationEventPublisher: ApplicationEventPublisher,
private val testRepository: TestRepository,
) {
private val log = LoggerFactory.getLogger(this.javaClass)
@Transactional
fun txLogic() {
log.info("[before-publish] current thread id : ${Thread.currentThread().id}")
// 이벤트 발행
applicationEventPublisher.publishEvent(
TestData("testMessage")
)
log.info("[after-publish] current thread id : ${Thread.currentThread().id}")
testRepository.save(TestEntity("test"))
}
}
이렇게 발행한 이벤트는 TestListener
에서 받아 처리한다.
위에서 설명한 phase 4개를 각각 설정한 함수들을 구현했다. 그리고 저장이 잘 되는지 확인하기 위해 save()
도 호출해봤다.
@Component
class TestListener(
private val testRepository: TestRepository,
) {
private val log = LoggerFactory.getLogger(this.javaClass)
@TransactionalEventListener(phase = AFTER_COMMIT)
fun handleEventWithTx(testData: TestData) {
log.info("[TxEventListener - AFTER_COMMIT] thread id : ${Thread.currentThread().id} request: ${testData.message}")
testRepository.save(TestEntity("test in listener"))
}
@TransactionalEventListener(phase = AFTER_ROLLBACK)
fun handleEventWithTx2(testData: TestData) {
log.info("[TxEventListener - AFTER_ROLLBACK] thread id : ${Thread.currentThread().id} request: ${testData.message}")
testRepository.save(TestEntity("test in listener"))
}
@TransactionalEventListener(phase = AFTER_COMPLETION)
fun handleEventWithTx3(testData: TestData) {
log.info("[TxEventListener - AFTER_COMPLETION] thread id : ${Thread.currentThread().id} request: ${testData.message}")
testRepository.save(TestEntity("test in listener"))
}
@TransactionalEventListener(phase = BEFORE_COMMIT)
fun handleEventWithTx4(testData: TestData) {
log.info("[TxEventListener - BEFORE_COMMIT] thread id : ${Thread.currentThread().id} request: ${testData.message}")
testRepository.save(TestEntity("test in listener"))
}
}
그리고 호출해보면 다음 로그가 발생한다!
일단 호출부인 TestService에서 롤백해야 하는 상황이 아니므로 AFTER_ROLLBACK
는 호출되지 않았다.
TestService, TestListener - BEFORE_COMMIT에서는 저장이 잘 된걸 확인할 수 있지만, 나머지는 저장이 되지 않았다.
- AFTER_COMMIT, AFTER_COMPLETION, BEFORE_COMMIT만 호출
트랜잭션이 Listener까지 잘 유지될줄 알았는데 그렇지 않았다. 왜 그런걸까?
로그 시작하는 부분을 확인해보면, Listener 로그는 TestService
에 찍힌 로그가 다 나오고 나서야 시작된다.
AFTER로 시작하는 phase는 호출부(TestService)의 트랜잭션이 끝날 때(commit 또는 rollback)가 기준이다.
즉, 호출부 트랜잭션은 이미 commit을 했지만 Listener에 같은 트랜잭션을 사용하고 있을 뿐인 거라서 다시 commit할 수 없다.
그래서 save()
를 호출함에도 insert 쿼리가 추가로 나가지 않는다!
하지만 BEFORE_COMMIT은 유일하게 호출부 트랜잭션이 끝나기 전에 이벤트를 발행한다.
그래서 성공적으로 save()
까지 호출한 것이다.
AFTER_*를 사용하면서도 저장되도록 하고 싶다면 기존 트랜잭션을 유지하지 않고 새 트랜잭션을 열 수 있도록 다음 설정을 해줘야 한다.
@Transactional(propagation = Propagation.REQUIRES_NEW)
REQUIRES_NEW
로 설정하면 저장은 되지만 새 트랜잭션을 여는거라서 Listener에서 롤백이 발생해도TestService
까지 전파되지 않아서 롤백되지 않는다는 문제점이 있음- propagation 기본값은
REQUIRED
: 기존 트랜잭션을 그대로 활용 (commit, rollback도 같이 함)
호출부에서 예외가 발생했을 때
호출부인 TestService
에서 예외가 발생해 롤백해야 하는 상황이라고 해보자.
// TestService
@Transactional(rollbackFor = [Exception::class])
fun txLogic() {
log.info("[before-publish] current thread id : ${Thread.currentThread().id}")
// 이벤트 발행
applicationEventPublisher.publishEvent(
TestData("testMessage")
)
log.info("[after-publish] current thread id : ${Thread.currentThread().id}")
testRepository.save(TestEntity("test"))
throw Exception("exception test") // 예외 발생!!
}
그러면 AFTER_ROLLBACK, AFTER_COMPLETION만 호출되는걸 확인할 수 있다!
TestService
에서 save()
를 호출했기 때문에 insert 쿼리가 처음엔 나가지만 예외가 발생해 모두 롤백된다.
당연하게도 AFTER_ROLLBACK, AFTER_COMPLETION에서 호출한 save()
는 insert 쿼리가 나가지 않는다.
이전 글부터 지금까지는 호출부에서 Listener로의 로직이 동기로 동작했다! 이걸 비동기로 동작하게 하는 방법도 알아보자.
댓글남기기