환불 기능을 실행하는 과정에서 외부 환불 서비스를 호출할 때 생기는 문제
- 외부 시스템이 정상이 아닐 경우 트랜잭션 처리를 어떻게 할지 애매함
- 외부 시스템의 응답 시간이 길어질 경우 내부시스템의 성능 또한 직접적으로 영향을 받음
위와 같은 문제가 발생하는 이유는 내부 BOUNDED CONTEXT와 결제 BOUNDED CONTEXT간의 높은 결합도 때문. (주문이 결제와 강하게 결합되어 있어서 주문 BOUNDED CONTEXT가 결제 BOUNDED CONTEXT에 영향을 받게 되는 것)
➡️ 이것을 해결하기 위해 이벤트를 사용함
도메인 모델에 이벤트를 도입하려면 아래와 같은 네 개의 구성요소를 구현해야 함.
이벤트 생성 주체
이벤트 핸들러
이벤트 디스페처
이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 됨
이벤트
이벤트 생성 주체
public class Order {
public void changeShippingInfo(ShippingInfo newShippingInfo) {
// 배송지 정보를 변경한 뒤에 이벤트 발생
Events.raise(new ShippingInfoChangedEvent(number, new ShippingInfo);
public class ShippingInfoChangedHandler implement EventHandler<ShippingInfoChangedEvent> {
@Override
public void handle(ShippingInfoChangedEvent evt) {
// 이벤트는 이벤트 핸들러가 작업을 수행하는데 필요한 최소한의 데이터를 담아야 함
// 이벤트가 필요한 데이터를 담고 있지 않으면 직접 조회
Order order = orderRepository.findById(evt.getOrderNo());
shippingInfoSynchronizer.sync(
order.getNumber().getValue(),
order.getNewShippingInfo());
}
ApplicationEventPublisher
사용Events.raise()
메서드 사용public class Order {
public void cancel() {
verifyNotYetShipped();
this.state = OrderState.CANCELED;
// 주문 상태를 취소로 변경한 후 이벤트 발생
Events.raise(new OrderCanceledEvent(number.getNumber()));
}
...
}
@EventListener
애너테이션 사용❗️이벤트 처리 흐름
1. 이벤트 처리에 필요한 이벤트 핸들러를 생성
2. 이벤트 발생 전에 이벤트 핸들러를Events.handle()
메서드를 이용해 등록
3. 이벤트를 발생하는 도메인 기능을 실행
4. 도메인은Events.raise()
를 이용해서 이벤트를 발생
5.Events.raise()
는 등록된 핸들러의canHandle()
을 이용해서 이벤트를 처리할 수 있는지 확인
6. 핸들러가 이벤트를 처리할 수 있다면handle()
메서드를 이용해서 이벤트를 처리
7.Events.raise()
실행을 끝내고 리턴한다.
8. 도메인 기능 실행을 끝내고 리턴한다.
9.Events.reset()
을 이용해서 ThreadLocal을 초기화
환불기능에서 만약 외부의 환불기능을 사용한다고 가정했을 때, 외부의 환불기능이 갑자기 느려지면 취소에 해당하는 cancel() 메서드도 함께 느려짐
➡️ 이벤트를 비동기로 처리함으로써 해결
이벤트를 비동기로 구현할 수 있는 방법
@Async
애너테이션 사용@EnableAsync
애너테이션을 사용해서 비동기 기능 활성화@Async
애너테이션 붙이기카프카
나 래빗MQ
와 같은 메시징 시스템 사용함으로써 비동기 구현[그림 10.9] 도메인의 상태와 이벤트 저장소로 동일한 DB 사용
[그림 10.10] 이벤트를 외부에 제공하는 API 사용
➡️ 차이점: 이벤트가 외부에 전달되는 방식
EventEntry
에 추가할지 여부EventEntry
public class EventEntry {
private Long id;
private String type;
private String contentType;
private String payload;
private long timestamp;
public EventEntry(String type, String contentType, String payload) {
this.type = type;
this.contentType = contentType;
this.payload = payload;
this.timestamp = timestamp;
}
public Long getId() {
return id;
}
public String getType() {
return type;
}
public String getContentType() {
return contentType;
}
public String getPayLoad() {
return payload;
}
public long getTimestamp() {
return timestamp;
}
}
EventEntry
는 이벤트 발생 주체에 대한 정보를 갖지 않으므로 특정 주체가 발생시킨 이벤트만 조회하는 기능을 구현할 수 없음✚ 이벤트 처리와 DB 트랜잭션 고려
이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 함
@TransactionalEventListener
를 사용하여 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행하도록 설정- 이벤트 저장소로 DB 사용: 이벤트 발생 코드와 이벤트 저장 처리를 한 트랜잭션으로 처리하면 트랜잭션이 성공할 때만 이벤트가 DB에 저장됨
➡️ 트랜잭션 실패에 대한 경우의 수 감소, 이벤트 처리 실패만 고민하면 됨. 이벤트 특성에 따라 재처리 방식 결정