Programming/Spring Boot

Spring Event, @TransactionalEventListener 사용하기

Jan92 2023. 3. 16. 00:01

@TransactionalEventListener 사용하기 및 propagation.REQUIRES_NEW

 

@TransactionalEventListener

spring framework 4.2부터 스프링 이벤트의 사용이 간편해졌는데요.

지난 포스팅에서 spring event를 사용하는 이유와 @EventListener를 통한 기본적인 이벤트 처리 방법에 대해서 살펴본 것에 이어, 이번 포스팅에서는 더 향상된 기능인 @TransactionalEventListener에 대해서 살펴볼 예정입니다.

 

2022.12.23 - [Programming/Spring Boot] - spring 이벤트 사용하기(event publisher, event listener)

(이전 포스팅 내용으로 spring event에 대한 기본적인 처리 방법이 궁금하시다면 참고하시면 좋을 것 같습니다.)

 

 

@TransactionalEventListener는 동작하는 메서드를 트랜잭션으로 묶어서 처리하는 경우 Transaction의 상태에 따라 발생하는 이벤트를 처리해 주는 이벤트 리스너인데요.

때문에 이벤트 처리가 필요한 로직에서 트랜잭션을 적용해야 하는 경우 @EventListener가 아니라 @TransactionalEventListener를 사용해야 합니다.

추가로 트랜잭션이 적용되지 않는다면 이벤트 리스너가 작동하지 않는다는 특징이 있습니다.

(단, fallbackExecution 플래그를 재설정하는 경우에는 작동할 수 있습니다.)

 

 


@TransactionalEventListener를 사용해야 하는 경우

private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public void signup(MemberDto memberDto) {
    //1. 회원가입 회원 정보 저장
    memberRepository.save(new Member(memberDto.getId(), memberDto.getName()));
    //2. 회원가입 축하 메일 전송 이벤트 발생
    eventPublisher.publishEvent(new SavedMemberEvent(memberDto)); 

    //3. 어떠한 사유로 인해 exception 발생
    if (memberDto.getName().equals("master")) {
        throw new RuntimeException("can not use this name.");
    }
}

@EventListener의 경우 publishEvent() 메서드가 호출되는 시점에서 바로 이벤트를 publishing 하는데요.

 

만약 다음과 같이 트랜잭션으로 묶인 signup() 메서드에서 '1. 회원가입 회원 정보 저장' 부분과 '2. 회원가입 축하 메일 전송 이벤트 발생' 부분이 정상적으로 동작한 뒤에 '3. 어떠한 사유로 인해 exception 발생' 부분에서 exception이 발생된다면, '1. 회원 정보 저장' 부분은 트랜잭션에 의해 롤백이 실행되지만, '2. 축하 메일 전송' 부분은 롤백이 되지 않는 상황이 발생하게 됩니다.

 

이러한 경우가 발생하기 때문에 트랜잭션이 적용되는 로직에서 이벤트 처리가 필요할 때는 @TransactionalEventListener가 사용되는 것인데요. 

아래 내용을 통해 해당 어노테이션의 사용 방법과 옵션들에 대해서 살펴보겠습니다.

 

 


@TransactionalEventListener 옵션

사용법은 phase 옵션을 통해 트랜잭션 상태에 따른 이벤트 처리를 적용할 수 있는데요.

 

1. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)

  • default 값이며, 트랜잭션이 commit 되었을 때 이벤트를 실행합니다.

 

2. @TransactionalEventListener(phase = TransactionPhase.ROLLBACK)

  • 트랜잭션이 rollback 되었을 때 이벤트를 실행합니다.

 

3. @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)

  • 트랜잭션이 completion(commit 또는 rollback) 되었을 때 이벤트 실행합니다.

 

4. @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)

  • 트랜잭션이 commit 되기 전에 이벤트를 실행합니다.

 

 


실행 코드 및 결과

private final ApplicationEventPublisher eventPublisher;

@Transactional
public void signup(MemberDto memberDto) {
    log.info("before publishEvent() method.");
    eventPublisher.publishEvent(new SavedMemberEvent(memberDto));
    log.info("after publishEvent() method.");

    if (memberDto.getName().equals("master")) {
        throw new RuntimeException("can not use this name.");
    }
}

(service)

 

@Slf4j
@Component
public class MemberEventListener {

    @EventListener
    public void defaultEventListener(SavedMemberEvent event) {
        log.info("defaultEventListener ---> {}", event);
    }

    @TransactionalEventListener(phase = TransactionPhase.BEFORE_COMMIT)
    public void transactionalEventListenerBeforeCommit(SavedMemberEvent event) {
        log.info("TransactionPhase.BEFORE_COMMIT ---> {}", event);
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void transactionalEventListenerAfterCommit(SavedMemberEvent event) {
        log.info("TransactionPhase.AFTER_COMMIT ---> {}", event);
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void transactionalEventListenerAfterRollback(SavedMemberEvent event) {
        log.info("TransactionPhase.AFTER_ROLLBACK ---> {}", event);
    }

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMPLETION)
    public void transactionalEventListenerAfterCompletion(SavedMemberEvent event) {
        log.info("TransactionPhase.AFTER_COMPLETION ---> {}", event);
    }
}

(eventListener)

 

 

성공 케이스 결과

성공 케이스의 결과를 살펴보면 'defaultEventListener' -> 'BEFORE_COMMIT' -> '트랜잭션 commit' -> 'AFTER_COMMIT' -> 'AFTER_COMPLETION'의 순서로 실행되는 것을 확인할 수 있는데요.

 

 

실패 케이스 결과

실패 케이스의 결과를 살펴보면 'defaultEventListener' -> '트랜잭션 rollback' -> 'AFTER_ROLLBACK' -> 'AFTER_COMPLETION'의 순서로 실행되는 것을 확인할 수 있습니다.

 

 


Propagation.REQUIRES_NEW

private final MemberRepository memberRepository;
private final ApplicationEventPublisher eventPublisher;

@Transactional
public void signup(MemberDto memberDto) {
    //1. 회원가입 회원 정보 저장
    memberRepository.save(new Member(memberDto.getId(), memberDto.getName()));
    //2. 회원가입 축하 메일 전송 이벤트 발생
    eventPublisher.publishEvent(new SavedMemberEvent(memberDto));
}

(service)

 

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
public void transactionalEventListenerAfterCommit(SavedMemberEvent event) {
    log.info("TransactionPhase.AFTER_COMMIT ---> {}", event);
    eventLogRepository.save(new EventLog(1L, "event log1"));
}

(eventListener)

 

만약 위 코드와 같이 이벤트 리스너에서 추가로 데이터베이스에 insert, update, delete 작업을 진행해야 하는 경우가 있을 수 있는데요.

실제로 해당 코드가 동작하였을 때, 오류가 발생하지 않고 동작하였음에도 불구하고 eventLog에 대한 데이터는 insert 되지 않는 상황이 생기게 됩니다.

 

이유는 @TransactionalEventListener의 경우 event publisher의 트랜잭션 안에서 동작하며, 커밋이 된 이후 추가 커밋을 허용하지 않기 때문인데요.

때문에 insert, update, delete 같은 작업이 필요한 경우 아래 코드와 같이 이벤트 리스너에서 @Transactional(propagation = Propagation.REQUIRES_NEW)를 추가 설정하는 과정이 필요합니다.

 

REQUIRES_NEW 설정은 해당 메서드가 이전 트랜잭션을 이어받지 않고 새로운 트랜잭션을 시작하겠다는 설정인데요.

event publisher의 commit을 보장하고, 이벤트 리스너에서는 새로운 트랜잭션에서의 작업 수행을 가능하게 합니다.

 

@TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void transactionalEventListenerAfterCommit(SavedMemberEvent event) {
    log.info("TransactionPhase.AFTER_COMMIT ---> {}", event);
    eventLogRepository.save(new EventLog(1L, "event log1"));
}

 

 


내용에서 잘못된 부분이나 궁금한 점은 댓글 남겨주시면 답변드리도록 하겠습니다.

부족한 부분은 아래 참고 자료 및 github 코드 참고하시면 좋을 것 같습니다. 감사합니다.

 

 

< 참고 자료 >

https://sabarada.tistory.com/188

https://browngoo.tistory.com/19

 

< github >

https://github.com/JianChoi-Kor/spring-event