쇼핑몰에서 구매 취소시 환불 처리를 해야한다. 도메인 객체에서 환불 기능을 실행하려면 환불 기능을 제공하는 도메인 외부 서비스를 호출하여 실행한다.
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 e) {
...
}
}
}
세 가지 문제가 발생할 수 있다.
1. 외부 서비스가 정상이 아닐 경우 트랙잭션 처리를 어떻게 할것인지.
2. 성능, 환불 외부 시스템의 응답 시간이 길어지면 그만큼 대기 시간도 길어진다.
3. 추가적으로 도메인 객체에 서비스를 전달하면 설계상 문제가 나타날 수 있다.
주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트간의 강결합.
주문이 강하게 결제와 결합되어 있어서 주문 바운디드 컨텍스트가 결제 바운디드 컨텍스트에 영향을 받게 된다.
이를 해결해주는게 이벤트를 사용하는 것이다.
이벤트 : 과거에 벌어진 어떤 것
주문을 취소했다면 '주문을 취소했음 이벤트'가 발생했다고 볼 수 있다.
이벤트 구성요소
배송지 변경 발생 이벤트 예시
public class ShippingInfoChangedEvent {
private String orderNumber;
private long timestamp;
private ShippingInfo newShippingInfo;
// 생성자, Getter
}
public class Order {
public void changeShippingInfo(ShippingInfo newShippingInfo) {
verifyNotYetShipped();
setShippingInfo(newShippingInfo);
Events.raise(new ShippingInfoChangedEvent(number, newShippingInfo));
}
...
}
public class ShippingInfoChangedHandler {
@EventListener(ShippingInfoChangedEvent.class)
public void handle(ShppingInfoChangedEvent evt) {
shippingInfoSynchronizer.sync(
evt.getOrderNumber(),
evt.getNewShippingInfo());
}
}
public class ShippingInfoChangedHandler {
@EventListener(ShippingInfoChangedEvent.class)
public void handle(ShippingInfoChangedEvent evt) {
// 이벤트가 필요한 데이터를 담고 있지 않으면,
// 이벤트 핸들러는 리포지터리, 조회 API, 직접 DB 접근 등의
// 방식을 통해 필요한 데이터를 조회해야함
Order order = orderRepository.findById(evt.getOrderNo());
shippingInfoSynchronizer.sync(
order.getNumber().getValuer(),
order.getShippingInfo());
}
}
이벤트 용도
이벤트의 장점
이벤트 클래스의 이름을 결정할 땐 과거 시제를 사용하는 점을 유의하자.
이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다. 예를 들어 주문 취소됨 이벤트는 주문번호를 최소한 포함해야 한다.
public class OrderCanceledEvent {
// 이벤트는 핸들러에서 이벤트를 처리하는 데 필요한 데이터를 포함한다.
private String orderNumber;
public OrderCanceledEvent(String number) {
this.orderNumber = number;
}
public String getOrderNumber() {
return orderNumber;
}
}
// 모든 이벤트 공통 프로퍼티가 있다면 상위 클래스를 만든다.
public abstract class Event {
private long timestamp;
public Event() {
this.timestamp = System.currentTimeMillis();
}
public long getTimestamp() {
return timestamp;
}
}
// Event 상속
public class OrderCanceledEvent extends Event {
private String orderNumber;
public OrderCanceledEvent(String number) {
super();
this.orderNumber = number;
}
}
Events 클래스와 ApplicationEventPublisher
이벤트 발생과 출판을 위해 스프링이 제공하는 ApplicationEventPublisher를 사용한다.
/**
* ApplicationEventPublisher 를 이용해 이벤트 발생
*/
public class Events {
private static ApplicationEventPublisher publisher;
static void setPublisher(ApplicationEventPublisher publisher) {
Events.publisher = publisher;
}
public static void raise(Object event) {
if (publisher != null) {
publisher.publishEvent(event); //이벤트 발생
}
}
}
// InitializingBean를 사용해 Events 클래스를 초기화
@Configuration
public class EventsConfiguration {
@Autowired
private ApplicationContext applicationContext;
@Bean
public InitializingBean eventsInitializer(ApplicationEventPublisher eventPublisher) {
return () -> Events.setPublisher(eventPublisher);
}
}
이벤트 발생과 이벤트 핸들러
// 이벤트 발생
public class Order {
public void cancel() {
verifyNotYetShipped();
this.state = OrderState.CANCELED;
Events.raise(new OrderCanceledEvent(number.getNumber())); //이벤트 발생
}
}
// 이벤트 핸들러
// OrderCanceledEvent 이벤트가 발생하면 handle 메소드가 실행된다.
public class OrderCanceledEventHandler {
private RefundService refundService;
public OrderCancelOrderService(RefundService refundService) {
this.refundService = refundService;
}
@EventListener(OrderCanceledEvent.class)
public void handle(OrderCanceledEvent orderCanceledEvent) {
refundService.refund(event.getOrderNumber());
}
}
흐름 정리