2 분 소요

@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로의 로직이 동기로 동작했다! 이걸 비동기로 동작하게 하는 방법도 알아보자.

  • @Async를 이용해 Listener에서 비동기로 동작하는 로직 작성하기 (작성 예정)

Reference

카테고리:

업데이트:

댓글남기기