이벤트

706__·2023년 12월 22일

DDD

목록 보기
10/11

시스템 간 강결합 문제

쇼핑몰에서 구매를 취소하면 환불을 처리해야 한다.
이때 환불 기능을 실행하는 주체는 주문 도메인 엔티티가 될 수 있다.
도메인 객체에서 환불 기능을 실행하려면 환불 기능을 제공하는 도메인 서비스를 파라미터로 전달받고 취소 도메인 기능에서 도메인 서비스를 실행하게 된다.

public class Order {
	...
    // 외부 서비스를 실행하기 위해 도메인 서비스를 파라미터로 전달받음
    public void cancel(RefundService refundService) {
    	verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        
        this.refundStatus = State.REFUND_STARTED;
        try {
        	refundService.refund(getPaymentId());
            this.refundStatus = State.REFUND_COMPLETED;
        } catch (Exception ex) {
        	???
        }
    }
}

응용 서비스에서 환불 기능을 실행할 수도 있다.

public class CancelOrderService {
	private RefundService refundService;
    
    @Transactional
    public void cancel(OrderNo orderNo) {
    	Order order = findOrder(orderNo);
        order.cancel();
        
        order.refundStarted();
        try {
        	refundService.refund(order.getPaymentId());
            order.refundCompleted();
        } catch (Exception ex) {
        	???
        }
    }
}    

보통 결제 시스템은 외부에 존재하므로 RefundService 는 외부에 있는 결제 시스템이 제공하는 환불 서비스를 호출한다.
이때 두 가지 문제가 발생할 수 있다.

  1. 외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매하다. 트랜잭션 롤백? 일단 커밋? 트랜잭션 롤백이 맞아 보이지만, 반드시 그래야 하는 것은 아니다. 주문은 취소 상태로 변경하고 환불만 나중에 다시 시도하는 방식으로 처리할 수도 있다.

  2. 성능 문제다. 환불을 처리하는 외부 시스템의 응답 시간이 길어지면 그만큼 대기 시간도 길어진다. 즉, 외부 서비스 성능에 직접적인 영향을 받게 된다.

두 가지 문제 외에도 도메인 객체에 서비스를 전달하면 추가로 설계상 문제가 나타날 수 있다.
주문 로직과 결제 로직이 섞이는 문제가 있다.
이것은 환불 기능이 바뀌면 Order 도 영향을 받게 된다는 것을 의미한다.
주문 도메인 객체의 코드를 결제 도메인 때문에 변경할지도 모르는 상황은 좋아 보이지 않는다.

도메인 객체에 서비스를 전달할 시 또 다른 문제는 기능을 추가할 때 발생한다.
만약 주문을 취소한 뒤에 환불 뿐만 아니라 취소했다는 내용을 통지해야 한다면 어떻게 할까?
환불 도메인 서비스와 동일하게 파라미터로 통지 서비스를 받도록 구현하면 앞서 언급한 로직이 섞이는 문제가 더 커지고 트랜잭션 처리가 더 복잡해진다.
게다가 영향을 주는 외부 서비스가 두 개로 증가한다.

지금까지 언급한 문제가 발생하는 이유는 주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트 간의 강결합 때문이다.
주문이 결제와 강하게 결합되어 있어서 주문 바운디드 컨텍스트가 결제 바운디드 컨텍스트에 영향을 받게 되는 것이다.

이런 강한 결합을 없앨 수 있는 방법이 바로 이벤트를 사용하는 것이다.
특히 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.
한번 익숙해지면 모든 연동을 이벤트와 비동기로 처리하고 싶을 정도로 강력하고 매력적인 것이 이벤트다.


이벤트 개요

이 절에서 사용하는 '이벤트'는 '과거에 벌어진 어떤 것'을 의미한다.
예를 들어 사용자가 암호를 변경한 것을 '암호를 변경했음 이벤트'가 벌어졌다고 할 수 있다.
비슷하게 주문을 취소했다면 '주문을 취소했음 이벤트'가 발생했다고 할 수 있다.

이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.
이벤트는 발생하는 것에서 끝나지 않으며, 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.

도메인 모델에서도 UI 컴포넌트와 유사하게 도메인의 상태 변경을 이벤트로 표현할 수 있다.

이벤트 관련 구성요소

도메인 모델에 이벤트를 도입하려면 다음과 같은 네 개의 구성요소를 구현해야 한다.

  • 이벤트
  • 이벤트 생성 주체
  • 이벤트 디스패처 Publisher
  • 이벤트 핸들러 Subscriber

도메인 모델에서 이벤트 생성 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다.
이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킨다.

이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.

이벤트 생성 주체와 이벤트 핸들러를 연결해주는 것이 Event Dispatcher이다.
이벤트 생성 주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다.
이벤트를 전달받은 디스패처는 해당 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.
이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.

이벤트의 구성

이벤트는 발생한 이벤트에 대한 정보를 담는다. 이 정보는 다음을 포함한다.

  • 이벤트 종류: 클래스 이름으로 이벤트 종류를 표현
  • 이벤트 발생 시간
  • 추가 데이터: 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보

이벤트는 이벤트 핸들러가 작업을 수행하는 데 필요한 데이터를 담아야 한다.
이 데이터가 부족하면 핸들러는 필요한 데이터를 읽기 위해 관련 API를 호출하거나 DB에서 데이터를 직접 읽어와야 한다.
이벤트는 데이터를 담아야 하지만 그렇다고 이벤트 자체와 관련 없는 데이터를 포함할 필요는 없다.

이벤트 용도

이벤트는 크게 두 가지 용도로 쓰인다.

첫 번째 용도는 Trigger다.
도메인의 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
주문에서는 주문 취소 이벤트를 트리거로 사용할 수 있다.

예매 결과를 SMS로 통지할 때도 이벤트를 트리거로 사용할 수 있다.
예매 도메인은 예매 완료 이벤트를 발생시키고 이 이벤트 핸들러에서 SMS를 발송하는 식으로 구현할 수 있다.

두 번째 용도는 서로 다른 시스템 간의 데이터 동기화이다.
배송지를 변경하면 외부 배송 서비스에 바뀐 배송지 정보를 전송해야 한다.
주문 도메인은 배송지 변경 이벤트를 발생시키고 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를 동기화할 수 있다.

이벤트 장점

이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.

구매 취소 로직에 이벤트를 적용함으로써 환불 로직이 없어진 것을 알 수 있다.
환불 실행 로직은 주문 취소 이벤트를 받는 이벤트 핸들러로 이동하게 된다.
이벤트를 사용하여 주문 도메인에서 결제 도메인으로의 의존을 제거했다.

이벤트 핸들러를 사용하면 기능 확장도 용이하다.
구매 취소 시 환불과 함께 이메일로 취소 내용을 보내고 싶다면 이메일 발송을 처리하는 핸들러를 구현하면 된다.
기능을 확장해도 구매 취소 로직은 수정할 필요가 없다.


이벤트, 핸들러, 디스패처 구현

실제 이벤트와 관련된 코드를 구현해보자. 이벤트와 관련된 코드는 다음과 같다.

구성요소설명
Event이벤트를 표현한다.
Dispatcher스프링이 제공하는 ApplicationEventPublisher 를 이용한다.
Events이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher 를 사용한다.
Handler이벤트를 수신해서 처리한다. 스프링이 제공하는 기능을 사용한다.

이벤트 클래스

이벤트 자체를 위한 상위 타입은 존재하지 않는다.
원하는 클래스를 이벤트로 사용하면 된다!
이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로 과거 시제를 사용해야 한다는 점만 유의하자.

이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다.
모든 이벤트가 공통으로 갖는 property 가 존재한다면 관련 상위 클래스로 만들 수도 있다.

Events 클래스와 ApplicationEventPublisher

이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 사용한다.
스프링 컨테이너는 ApplicationEventPublisher도 된다.
Events 클래스는 이를 사용해서 이벤트를 발생시키도록 구현한다.

public class Events {
	private static ApplicationEventPublisher publisher;
    
    // Events 클래스가 사용할 ApplicationEventPublisher 객체는 setPublisher()를 통해 전달받는다.
    static void setPublisher(ApplicationEventPublisher publisher) {
    	Events.publisher = publisher;
    }
    
    // ApplicationEventPublisher가 제공하는 publishEvent()를 이용해 이벤트를 발생시킨다.
    public static void raise(Object event) {
    	if (publisher != null) {
        	publisher.publishEvent(event);
        }
    }
}   
@Configuration
public class EventsConfiguration {
	@Autowired
    private ApplicationContext applicationContext;
    
    // 스프링 빈 객체를 초기화할 때 사용하는 인터페이스, InitializingBean 타입 객체를 빈으로 설정한다.
    // Events 클래스를 초기화
    @Bean
    public InitializingBean eventsInitializer() {
    	// ApplicationContext는 ApplicatioinEventPublisher를 상속하므로 초기화할 때 전달한다.
    	return () -> Events.setPublisher(applicationContext);
    }
}   

이벤트 발생과 이벤트 핸들러

이벤트를 발생시킬 코드는 Events.raise() 를 사용한다.

public class Order {
	public void cancel() {
    	verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        Events.raise(new OrderCanceledEvent(number.getNumber()));
    }
}   

이벤트를 처리할 핸들러는 스프링이 제공하는 @EventListener 애노테이션을 사용해서 구현한다.

@Service
public class OrderCanceledEventHandler {
	private RefundService refundService;
    
    public OrderCanceledEventHandler(RefundService refundService) {
    	this.refundService = refundService;
    }
    
    // ApplicationEventPublisher.publisherEvent()를 호출할 때 OrderCanceledEvent 타입 객체를 전달하면
    // OrderCanceledEvent.class 값을 갖는 @EventListener 애노테이션을 붙인 메서드를 찾아 실행한다.
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
    	refundService.refund(event.getOrderNumber());
    }
}   

흐름 정리

  1. 도메인 기능을 실행한다.
  2. 도메인 기능은 Events.raise()를 이용해서 이벤트를 발생시킨다.
  3. Events.raise()는 스프링이 제공하는 ApplicationEventPublisher를 이용해서 이벤트를 출판한다.
  4. ApplicationEventPublisher는 @EventListener 애노테이션이 붙은 메서드를 찾아 실행한다.

코드 흐름을 보면 응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행하고 있다.
즉, 도메인 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다.


동기 이벤트 처리 문제

이벤트를 사용해서 강결합 문제는 해결했지만 아직 남아 있는 문제가 하나 있다.
바로 외부 서비스에 영향을 받는 문제이다.

@Transactional
public void cancel(OrderNo orderNo) {
	Order order = findOrder(orderNo);
    order.cancel();
}

@Service
public class OrderCanceledEventHandler {
	...
    
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
    	// refund()가 느려지거나 예외가 발생하면?
    	refundService.refund(event.getOrderNumber());
    }
}    

refundService.refund() 가 외부 환불 서비스와 연동된다고 가정하자.
만약 외부 환불 기능이 갑자기 느려지면 cancel() 도 함께 느려진다.
이것은 외부 서비스의 성능 저하가 바로 내 시스템의 성능 저하로 연결된다는 것을 의미한다.

성능 저하뿐만 아니라 트랜잭션도 문제가 된다.
refundService.refund() 에서 Exception이 발생하면 cancel() 의 트랜잭션을 롤백해야 할까?
트랜잭션을 롤백하면 구매 취소 기능을 롤백하는 것이므로 구매 취소가 실패하는 것과 같다.

생각해 볼 만한 것은 외부 환불 서비스 실행에 실패했다고 해서 반드시 트랜잭션을 롤백해야 하는지에 대한 문제다.
일단 구매 취소 자체는 처리하고 환불만 재처리하거나 수동으로 처리할 수도 있다.

외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.


비동기 이벤트 처리

회원 가입 신청을 하면 검증을 위해 이메일을 보내는 서비스가 많다.
회원 가입 신청을 하자마자 바로 내 메일함에 검증 이메일이 도착할 필요는 없다.
심지어 이메일을 받지 못하면 다시 받을 수 있는 기능을 이용하면 된다.

비슷하게 주문을 취소하자마자 바로 결제를 취소하지 않아도 된다.

이렇게 우리가 구현해야 할 것 중에서 'A 하면 이어서 B 하라'는 내용을 담고 있는 요구사항은 실제로 'A 하면 최대 언제까지 B 하라'인 경우가 많다.
즉, 일정 시간 안에만 후속 조치를 처리하면 되는 경우가 적지 않다.
게다가 B를 하는 데 실패하면 일정 간격으로 재시도를 하거나 수동 처리를 해도 상관없는 경우가 있다.

'A 하면'은 이벤트로 볼 수도 있다.
즉, '회원 가입 신청함 이벤트'를 처리하는 핸들러에서 '인증 이메일을 보내라' 기능을 보낼 수 있다.

앞서 말했듯 'A 하면 최대 언제까지 B 하라'로 바꿀 수 있는 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.
다시 말해서 A 이벤트가 발생하면 별도 스레드로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다.

이벤트를 비동기로 구현할 수 있는 방법은 다양한데, 다음 네 가지 방식으로 비동기 이벤트 처리를 구현하는 방법이 존재한다.

  • 로컬 핸들러를 비동기로 실행하기
  • 메시지 큐를 사용하기
  • 이벤트 저장소와 이벤트 포워더 사용하기
  • 이벤트 저장소와 이벤트 제공 API 사용하기

각 방식은 각자 구현하는 방식도 다르고 그에 따른 장단점도 존재한다.

로컬 핸들러 비동기 실행

이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 별도 스레드로 실행하는 것이다.
스프링이 제공하는 @Async 애노테이션을 사용하면 손쉽게 비동기로 이벤트 핸들러를 실행할 수 있다.
이를 위해 다음 두 가지가 수행되어야 한다.

  • @EnableAsync 애노테시녀을 사용해서 비동기 기능을 활성화한다.
  • 이벤트 핸들러 메서드에 @Async 애노테이션을 붙이면 된다.
@SpringBootApplication
@EnableAsync
public class ShopApplication {
	public static void main(String[] args) {
    	SpringApplication.run(shopApplication.class, args);
    
    }
}    
@Service
public class OrderCanceledEventHandler {
	
    @Async
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event) {
    	refundService.refund(event.getOrderNumber());
    }
}  

스프링은 OrderCanceledEvent 가 발생하면 handle() 을 별도 스레드를 이용해서 비동기로 실행한다.

메시징 시스템을 이용한 비동기 구현

비동기로 이벤트를 처리해야 할 때 사용하는 또 다른 방법은 Kafka나, RabbitMQ와 같은 메시징 시스템을 사용하는 것이다.
이벤트가 발생하면 디스패처는 이벤트를 메시지 큐에 보낸다.
메시지 큐는 이벤트를 메시지 리스너에 전달하고, 메시지 리스너는 알맞은 핸들러를 이용해서 이벤트를 처리한다.
이 때 이벤트를 메시지 큐에 저장하는 과정에서 메시지 큐에서 이벤트를 읽어와 처리하는 과정은 별도 스레드나 프로세스로 처리된다.

필요하다면 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다.
도메인 기능을 실행한 결과를 DB에 반영하고 이 과정에서 발생한 이벤트를 메시지 큐에 저장하는 것을 같은 트랜잭션 범위에서 실행하려면 글로벌 트랜잭션이 필요하다.

글로벌 트랜잭션을 사용하면 안전하게 이벤트를 메시지 큐에 전달할 수 있는 장점이 있지만 반대로 글로벌 트랜잭션으로 인해 전체 성능이 떨어지는 단점도 있다.
글로벌 트랜잭션을 지원하지 않는 메시징 시스템도 있다.

메시지 큐를 사용하면 보통 이벤트를 발생시키는 주체와 이벤트 핸들러가 별도 프로세스에서 동작한다.
이것은 이벤트 발생 JVM과 이벤트 처리 JVM이 다른다는 것을 의미한다.
물론 한 JVM에서 이벤트 발생 주체와 핸들러가 메시지 큐를 이용해 이벤트를 주고받을 수 있지만, 동일 JVM에서 비동기 처리를 위해 메시지 큐를 사용하는 것은 시스템을 복잡하게 만들 뿐이다.

메시징 시스템글로벌 트랜잭션 지원장점
RabbitMQ지원O클러스터와 고가용성을 지원하기 때문에 안정적으로 메시지를 전달할 수 있다. 다양한 개발 언어와 통신 프로토콜을 지원한다.
Kafka지원X다른 메시징 시스템에 비해 높은 성능을 보여준다.

이벤트 저장소를 이용한 비동기 처리

이벤트를 비동기로 처리하는 또 다른 방법은 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 것이다.
이 방식의 실행 흐름은 다음과 같다.

이벤트가 발생하면 핸들러는 스토리지에 이벤트를 저장한다.
포워더는 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러를 실행한다.
포워더는 별도 스레드를 이용하기 때문에 이벤트 발행과 처리가 비동기로 처리된다.

이 방식은 도메인의 상태와 이벤트 저장소로 동일한 DB를 사용한다.
즉, 도메인의 상태 변화와 이벤트 저장이 로컬 트랜잭션으로 처리된다.
이벤트를 물리적 저장소에 보관하기 때문에 핸들러가 이벤트 처리에 실패할 경우 포워더는 다시 이벤트 저장소에서 이벤트를 읽어와 핸들러를 실행하면 된다.

이벤트 저장소를 이용한 두 번째 방법은 이벤트를 외부에 제공하는 API를 사용하는 것이다.

API 방식와 포워더 방식의 차이점은 이벤트를 전달하는 방식에 있다.
포워더 방식이 포워더를 이용해서 이벤트를 외부에 전달한다면, API 방식은 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져간다.
포워더 방식은 이벤트를 어디까지 처리했는지 추적하는 역할이 포워더에 있다면 API 방식에서는 이벤트 목록을 요구하는 외부 핸들러가 자신이 어디까지 이벤트를 처리했는지 기억해야 한다.


이벤트 적용 시 추가 고려 사항

이벤트를 구현할 때 추가로 고려할 점이 있다.

이벤트 소스를 EventEntry에 추가할지 여부
앞서 다루었던 EventEntry는 이벤트 발생 주체에 대한 정보를 갖지 않는다.
따라서 ' Order 가 발생시킨 이벤트만 조회하기'처럼 특정 주체가 발생시킨 이벤트만을 조회하는 기능을 구현할 수 없다.
이 기능을 구현하려면 이벤트에 발생 주체 정보를 추가해야 한다.

포워더에서 전송 실패를 얼마나 허용할 것인가
포워더는 이벤트 전송에 실패하면 실패한 이벤트부터 다시 읽어와 전송을 시도한다.
그런데 특정 이벤트에서 계속 전송에 실패하면 어떻게 될까?
그 이벤트 때문에 나머지 이벤트를 전송할 수 없게 된다.
따라서 포워더를 구현할 때는 실패한 이벤트의 재전송 횟수 제한을 두어야 한다.
예를 들어, 동일 이벤트를 전송하는 데 3회 실패했다면 해당 이벤트는 생략하고 다음 이벤트로 넘어가는 등의 정책이 필요하다.

처리에 실패한 이벤트를 생략하지 않고 별도 실패용 DB나 메시지 큐에 저장하기도 한다.
처리에 실패한 이벤트를 물리적인 저장소에 남겨두면 이후 실패 이유 분석이나 후처리에 도움이 된다.

이벤트 손실
이벤트 저장소를 사용하는 방식은 이벤트 발생과 짱을 한 트랜잭션으로 처리하기 때문에 트랜잭션에 성공하면 이벤트가 저장소에 보관된다는 것을 보장할 수 있다.
반면에 로컬 핸들러를 이용해서 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 된다.

이벤트 순서
이벤트 발생 순서대로 외부 시스템에 전달해야 할 경우, 이벤트 저장소를 사용하는 것이 좋다.
이벤트 저장소는 저장소에 이벤트를 발생 순서대로 저장하고 그 순서대로 이벤트 목록을 제공하기 때문이다.
반면에 메시징 시스템은 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수도 있다.

이벤트 재처리
동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지 결정해야 한다.
가장 쉬운 방법은 마지막으로 처리한 이벤트의 순번을 기억해 두었다가 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트를 처리하지 않고 무시하는 것이다.
에를 들어, 회원 가입 신청 이벤트가 처음 도착하면 이메일을 발생하는데, 동일한 순번의 이벤트가 다시 들어오면 이메일을 발송하지 않는 방식으로 구현한다.
이외에 이벤트를 멱등으로 처리하는 방법도 있다.

멱등성
연산을 여러 번 적용해도 결과가 달라지지 않는 성질을 말한다.

이벤트 처리와 DB 트랜잭션 고려

DB 트랜잭션 관점에서 고려할 점을 살펴보자.
이벤트를 처리할 때는 DB 트랜잭션을 함께 고려해야 한다.

이벤트를 비동기로 처리할 때도 데이터베이스 트랜잭션을 고려해야 한다.
이벤트 처리는 동기든 비동기든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다.

트랜잭션 실패와 이벤트 처리 실패를 모두 고려하면 복잡해지므로 경우의 수를 줄이면 도움이 된다.
경우의 수를 줄이는 방법은 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 것이다.

스프링은 @TransactionalEventListener 애노테이션을 지원한다.
이 애노테이션은 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다.

@TransactionalEventListener(
	classes = OrderCanceledEvent.class,
    // 트랜잭션 커밋에 성공해야지 핸들러 메서드를 실행한다.
    // 중간에 에러가 발생해서 트랜잭션이 롤백되면 핸들러 메서드를 실행하지 않는다.
    // 이 기능을 사용하면 이벤트 핸들러를 실행했는데 트랜잭션이 롤백되는 상황은 발생하지 않는다.
    phase = TransactionPhase.AFTER_COMMIT
)
public void handle(OrderCanceledEvent event) {
	refundService.refund(event.getOrderNumber());
}    

이벤트 저장소로 DB를 사용해도 동일한 효과를 볼 수 있다.
이벤트 발생 코드와 이벤트 저장 처리를 한 트랜잭션으로 처리하면 된다.
이렇게 하면 트랜잭션이 성공할 때만 이벤트가 DB에 저장되므로, 트랜잭션은 실패했는데 이벤트 핸들러가 실행되는 상황은 발생하지 않는다.

트랜잭션이 성공할 때만 이벤트 핸들러를 실행하게 되면 트랜잭션 실패에 대한 경우의 수가 줄어, 이벤트 처리 실패만 고민하면 된다.
이벤트 특성에 따라 재처리 방식을 결정하면 된다.

0개의 댓글