Backend/Spring

[Spring] 트랜잭션 상태에 맞춘 이벤트 처리 @TransactionalEventListener

누구세연 2024. 10. 25. 20:05

@EventListener는 트랜잭션의 상태와 무관하게 이벤트를 수신하지만, @TransactionalEventListener는 이벤트 리스너가 트랜잭션 내에서 특정한 조건에 따라 동작하도록 합니다. 이를 통해, 예를 들어 트랜잭션이 성공적으로 커밋된 이후에만 이벤트를 처리하게 할 수 있습니다.

이 글에서 트랜잭션 상태에 따라 이벤트를 처리할 수 있는 @TransactionalEventListener 대해 알아보겠습니다. 🙂

 

 

@TransactionalEventListener

Spring이 제공하는 트랜잭션 이벤트 리스너입니다.

이 어노테이션을 사용하면 이벤트 리스너가 트랜잭션의 상태를 모니터링하며 이벤트를 처리할 수 있습니다.

@TransactionalEventListener 어노테이션은 다음과 같은 옵션들이 있습니다.

TransactionPhase

  • ATFER_COMMIT: 트랜잭션 커밋 후에만 이벤트를 수신(기본값)
  • AFTER_ROLLBACK: 트랜잭션 롤백 후에만 이벤트를 수신
  • AFTER_COMPLETION: 트랜잭션 완료 후(커밋/롤백 상관없이) 이벤트를 수신
  • BEFROE_COMMIT: 커밋 전에 이벤트를 수신

이 옵션들을 통해 트랜잭션의 다양한 상태에 따라 유연하게 이벤트를 처리할 수 있습니다.

 

 

사용 예시

트랜잭션 커밋 후 이벤트 처리(AFTER_COMMIT)

가장 일반적인 사용 예시로 트랜잭션이 성공적으로 완료된 후 이벤트를 처리하는 경우입니다.
예를 들어, 사용자가 회원가입 후 UserRegisterEvent라는 이벤트를 발행하고 해당 이벤트가 트랜잭션 커밋 이후에만 전송 메일을 발송하도록 설정할 수 있습니다!

 

1. 사용자 가입 이벤트 클래스 (UserRegisteredEvent)

  • UserRegisteredEvent는 이벤트 정보를 담고 있는 단순한 데이터 클래스입니다. 이 클래스는 사용자의 이메일 주소를 담고 있으며, 이벤트 수신자에게 가입한 사용자의 이메일을 제공합니다.
public class UserRegisteredEvent {
    private final String email;

    public UserRegisteredEvent(String email) {
        this.email = email;
    }

    public String getEmail() {
        return email;
    }
}

 

2. 이벤트 발행 클래스 (UserService)

  • UserService는 사용자 가입을 담당하는 서비스 클래스입니다.
  • registerUser 메서드에서 사용자 가입 로직을 처리하고, 이벤트를 발행하여 UserRegisteredEvent를 생성 및 전달합니다.
  • @Transactional 어노테이션을 통해 가입 로직을 트랜잭션 내에서 실행하고, 가입이 정상적으로 완료된 경우에만 UserRegisteredEvent가 발행됩니다.
@Service
public class UserService {
    private final ApplicationEventPublisher publisher;

    @Transactional
    public void registerUser(String email) {
        // 회원가입 로직 (데이터베이스에 사용자 정보 저장 등)
        publisher.publishEvent(new UserRegisteredEvent(email));
    }
}

 

  • publisher.publishEvent(new UserRegisteredEvent(email));를 통해 이벤트를 발행하며, 트랜잭션이 성공적으로 커밋되기 전까지는 UserEventListener가 이 이벤트를 처리하지 않습니다.

3. 이벤트 수신 클래스 (UserEventListener)

  • UserEventListener는 이벤트 리스너로, 발행된 UserRegisteredEvent를 수신하여 환영 이메일을 발송하는 역할을 합니다.
  • @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) 어노테이션을 사용하여 트랜잭션이 커밋된 후에만 이벤트가 처리되도록 합니다.
  • 트랜잭션이 정상적으로 완료된 경우에만 sendWelcomeEmail 메서드가 호출되며, 콘솔에 환영 이메일 전송 메시지를 출력합니다.
@Component
public class UserEventListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void sendWelcomeEmail(UserRegisteredEvent event) {
        System.out.println("Sending welcome email to " + event.getEmail());
    }
}

 

 

롤백 후 이벤트 처리(AFTER_ROLLBACK)

예를 들어, 결제 실패 시 사용자에게 알림을 보내야 하는 상황이 있다면 트랜잭션 롤백 후에 이벤트를 처리하도록 설정할 수 있습니다!

 

1. 이벤트 발행 (PaymentService 내 결제 처리 메서드):

  • 결제 처리가 이루어지고 트랜잭션이 생성됩니다. 예를 들어, 주문의 결제 금액을 차감하거나, 결제 정보를 저장하는 작업이 포함될 수 있습니다.
  • 트랜잭션 내에서 결제에 실패한 경우 PaymentFailedEvent 이벤트를 발행하고, 이후 트랜잭션이 롤백됩니다.
@Service
public class PaymentService {
    private final ApplicationEventPublisher publisher;

    @Transactional
    public void processPayment(Order order) {
        try {
            // 결제 처리 로직 수행
            // 예: 결제 금액 차감, 결제 정보 저장
            if (/*결제 실패 조건*/) {
                throw new PaymentException("결제 실패");
            }
        } catch (Exception e) {
            // 결제 실패 시 이벤트 발행
            publisher.publishEvent(new PaymentFailedEvent(order.getOrderId()));
            throw e; // 트랜잭션 롤백을 위한 예외 재발생
        }
    }
}

 

2. 이벤트 리스너 (PaymentEventListener):

  • @TransactionalEventListener의 TransactionPhase.AFTER_ROLLBACK 설정에 따라, 트랜잭션이 롤백된 후에만 PaymentFailedEvent를 수신하게 됩니다.
  • 결제 실패 시에는 System.out.println("Payment failed for " + event.getOrderId());를 통해 결제 실패 메시지를 출력하거나, 실패한 결제에 대해 로그를 남기거나 알림을 발송할 수 있습니다.
@Component
public class PaymentEventListener {

    @TransactionalEventListener(phase = TransactionPhase.AFTER_ROLLBACK)
    public void handleFailedPayment(PaymentFailedEvent event) {
        System.out.println("Payment failed for " + event.getOrderId());
    }
}

 

3. 트랜잭션 상태에 따른 이벤트 처리:

  • TransactionPhase.AFTER_ROLLBACK 설정 덕분에 결제가 성공적으로 완료된 경우에는 PaymentFailedEvent를 무시하고, 결제 실패 시에만 이벤트가 처리됩니다. 이 방식으로, 실패한 트랜잭션에 대해서만 특정 작업을 수행할 수 있게 되며, 코드의 의도와 명확히 맞아떨어지게 됩니다.

 

장점

  1. 트랜잭션 안정성: 트랜잭션이 성공적으로 완료된 후에만 이벤트가 처리되므로, 실패한 트랜잭션에 대한 불필요한 작업을 방지할 수 있습니다. 예를 들어, 데이터 저장이 실패했을 때는 후속 작업인 알림 전송이나 로그 기록을 건너뛸 수 있습니다.
  2. 코드 가독성 향상: 비즈니스 로직과 이벤트 처리 로직을 분리함으로써, 코드가 더 읽기 쉬워지고 유지보수가 용이합니다.
  3. 구체적인 트랜잭션 상태 제어: TransactionPhase 옵션을 사용해 AFTER_COMMIT, AFTER_ROLLBACK 등의 상태에 따라 이벤트를 다르게 처리할 수 있어 유연성이 높습니다.
  4. 비동기 처리가 가능: 필요에 따라 @Async와 함께 사용해 트랜잭션 이벤트를 비동기로 처리할 수 있습니다. 이로 인해 중요한 작업은 트랜잭션 내에서 마무리하고, 부가적인 작업(이메일 전송 등)은 비동기로 진행할 수 있습니다.

단점

  1. 트랜잭션 의존성: 트랜잭션이 없는 경우 @TransactionalEventListener는 이벤트를 처리하지 않습니다. 즉, 트랜잭션 범위 외에서 발생하는 이벤트가 있다면 @EventListener와 같이 별도로 관리해야 합니다.
  2. 복잡성 증가: TransactionPhase 설정을 잘못하면 예기치 않은 결과가 발생할 수 있습니다. 예를 들어, AFTER_COMMIT으로 설정했지만 트랜잭션이 롤백되는 경우 해당 이벤트가 무시되는 등의 상황이 발생할 수 있습니다.
  3. 비동기 이벤트 예외 처리 주의 필요: 비동기로 이벤트를 처리할 때 예외가 발생하면 해당 예외가 상위 트랜잭션으로 전파되지 않으므로 예외 처리를 위한 별도의 로직이 필요할 수 있습니다.
  4. 성능 저하 가능성: 많은 트랜잭션 이벤트를 비동기로 발행하는 경우, 리스너에서 발생하는 이벤트가 많아지면 성능에 영향을 줄 수 있습니다. 이 경우 스레드 풀 설정 등을 통해 적절한 제어가 필요합니다.

 

🚨 주의 사항

  • 비동기 처리: @TransactionalEventListener는 기본적으로 동기로 작동하므로, 비동기 처리가 필요하다면 @Async를 함께 사용하여 비동기 이벤트 처리를 설정해야 합니다.
  • 트랜잭션 전파: 이벤트가 트랜잭션 내에서 처리되므로, 트랜잭션 전파 방식에 주의해야 합니다. 특히 이벤트 리스너에서 데이터베이스 접근 시 새로운 트랜잭션을 생성하는 경우가 아니면, 같은 트랜잭션 내에서 동작합니다.