시스템 간 강결합 문제
쇼핑몰에서 구매를 취소하면 환불을 처리해야 한다.
이 때 환불 기능을 실행하는 주체는 주문 도메인 엔티티가 될 수 있다.
도메인 객체에서 환불 기능을 실행하려면 환불 기능을 제공하는 도메인 서비스를 인자로 받고 취소 도메인 기능에서 도메인 서비스를 실행해야 한다.
public class Order {
public void cancel(RefundService refundService){
// 취소 로직
// 환불 로직
// 외부 서비스에 의존하고 있음
refundService.refund(..)
}
}
보통 결제 시스템은 외부에 존재하므로 RefundService는 외부에 있는 결제 시스템이 제공하는 환불 서비스를 호출한다.
이 때 두 가지 문제가 발생할 수 있다.
첫 번째는 외부 서비스가 정상이 아닐 경우 트랜잭션 처리를 어떻게 해야 할지 애매하다는 것이다.
환불 기능을 실행하는 과정에서 익셉션이 발생하면 트랜잭션을 롤백해야 할지, 그냥 일단 커밋해야할지이다.
두 번째는 성능에 대한 것이다.
환불을 처리하는 외부 시스템의 응답 시간이 길어지면 그 만큼 대기 시간도 길어진다.
즉, 외부 서비스 성능에 직접적인 영향을 받게 된다.
우선 Order는 주문을 표현하는 도메인 객체인데 결제 도메인의 환불 관련 로직이 뒤섞이게 된다.
즉, 환불 관련 기능이 바뀌면 Order도 영향을 받게 된다.
이런 문제들이 발생하는 이유는 간단하다.
주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트가 강하게 결합되어 있기 때문이다.
이러한 강한 결합을 없앨 수 있는 방법이 있다.
바로 '이벤트' 라는 것을 사용하는 것이다.
특히 비동기 이벤트를 사용하면 두 시스템 간의 결합을 크게 낮출 수 있다.
이벤트 개요
'이벤트'라는 용어는 '과거에 벌어진 어떤 것' 을 의미한다.
이벤트가 발생했다는 것은 상태가 변경됐다는 것을 의미한다.
이벤트는 발생하는 것에서 끝나지 않고, 이벤트가 발생하면 그 이벤트에 반응하여 원하는 동작을 수행하는 기능을 구현한다.
'~할 때', '~가 발생하면', '만약 ~하면' 처럼의 요구사항은 도메인의 상태 변경과 관련된 경우가 많고 이런 요구사항을 이벤트를 이용해서 구현할 수 있다.
도메인 모델에서 이벤트 생성 주체는 엔티티, 밸류와 같은 도메인 객체이다.
이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킨다.
이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.
이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.
이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처이다.
이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.
이벤트는 크게 두 가지 용도로 쓰인다.
첫 번째 용도는 '트리거'이다.
도메인 상태가 바뀔 때마다 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
두 번째 용도는 '서로 다른 시스템 간의 데이터 동기화'이다.
예를 들어 배송지를 변경하면 외부 배송 서비스에 바뀐 배송지 정보를 전송해야 한다.
주문 도메인은 배송지 변경 이벤트를 발생시키고 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를 동기화할 수 있다.
이제 이벤트를 사용하면 어떻게 코드가 변경되는지 확인해보자.
// 이벤트를 활용하지 않은 코드
public class Order {
public void refund(RefundService refundService) {
this.state = OrderState.CANCELED;
this.refundStatus = State.REFUND_STATED;
try {
refundService.refund(getPaymentId());
this.refundStatus = State.REFUND_COMPLETED;
} catch (Exception e) {
...
}
}
}
public class Order {
public void cancel() {
this.state = OrderState.CANCELED;
Events.raise(new OrderCanceledEvent(number.getNumber()));
}
}
@RequiredArgsConstructor
public class OrderCanceledEventHandler {
private final RefundService refundService;
@EventListener(OrderCanceledEvent.class)
public void handle(OrderCanceledEvent event) {
refundService.refund(event.getOrderNumber());
}
}
이제 구매 취소 로직에 환불 로직이 사라졌다.
이벤트를 사용하여 주문 도메인에서 환불 도메인으로의 의존을 제거했다.
동기 이벤트 처리 문제
이벤트를 사용해서 강한 결합 문제는 해결했지만, 아직 남아있는 문제가 있다.
바로 외부 서비스에 영향을 받는 문제이다.
@Transactional
public void cancel(OrderNo orderNo) {
Order order = findOrder(orderNo);
order.cancel(); // OrderCanceledEvent 발생
}
@Service
public class OrderCanceledEventHandler {
@EventListener(OrderCanceledEvent.class)
public void handle(OrderCanceledEvent event) {
// refundService.refund()가 느려지거나 exception이 발생하면?
refundService.refund(event.getOrderNumber());
}
}
refundService.refund()가 느려지면 cancel()도 함께 느려진다.
이것은 외부 서비스의 성능 저하가 바로 내 시스템의 성능 저하로 연결된다는 것을 의미한다.
외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능 문제를 해소하는 방법은 이벤트르 비동기로 처리해보는 것이다.
비동기 이벤트 처리
회원 가입 신청을 하면 검증을 위해 이메일을 보내는 서비스가 많다.
회원 가입 신청을 하자마자 바로 내 메일함에 검증 이메일이 도착할 필요는 없다.
몇초 뒤에 도착해도 문제가 되지 않는다.
이렇게 우리가 구현해야할 것 중에서 'A 하면 이어서 B 하라'는 내용을 담고 있는 요구사항은 실제로 'A 하면 최대 언제까지 B 하라'인 경우가 많다. 즉, 일정 시간 안에만 후속 조치를 처리하면 되는 경우가 적지 않다.
해당 요구사항은 이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.
다시 말해 A 이벤트가 발생하면 별도 스레드로 B를 수행하는 핸들러를 실행하는 방식으로 요구사항을 구현할 수 있다.
'책 정리 > 도메인 주도 개발 시작하기' 카테고리의 다른 글
Chapter 11. CQRS (0) | 2023.12.22 |
---|---|
Chapter 9. 도메인 모델과 바운디드 컨텍스트 (1) | 2023.12.21 |
Chapter 8. 애그리거트 트랜잭션 관리 (0) | 2023.12.15 |
Chapter 7. 도메인 서비스 (0) | 2023.12.12 |
Chapter 6. 응용 서비스와 표현 영역 (1) | 2023.12.12 |