[DDD START] 10장 이벤트

시스템 간 강결합의 문제

쇼핑몰에서 구매를 취소하려면 환불을 처리해야 한다. 이 때 환불 기능을 실행하는 주체는 주문 엔티티가 될 수 있다.
다음 두가지 방법으로 환불 기능을 구현할 수 있다.

주문 도메인에서 환불 처리

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) {
            ...
        }
    }
}

응용 서비스에서 환불 처리

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 e) {
            ...
        }
    }
}

보통 결제 시스템은 외부에 존재하므로 RefundService는 외부의 환불 시스템을 호출하는데 여기서 세 가지 문제가 발생한다.

  1. 외부의 환불 서비스에서 예외가 발생하면 주문 트랜잭션을 롤백해야 하는가?
    • 주문만 취소 상태로 변경하고 환불은 나중에 처리할수도 있다.
  2. 외부 시스템의 성능에 직접적인 영향을 받는다.
  3. 주문 도메인에서 환불을 처리하면 주문 도메인이 결제 도메인의 영향을 받게 된다.

이벤트

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

이벤트의 구성요소

구현

이벤트

public class OrderCanceledEvent extends Event {

    private String orderNumber;
    
    public OrderCanceledEvent(String number) {
        super();
        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;
    }   
}

EventHandler 인터페이스

public interface EventHandler<T> {
    
    // handle 메서드를 이용해서 필요한 기능을 구현한다. 
    void handle(T event);

    // 핸들러가 이벤트를 처리할 수 있는지 여부를 검사한다.
    default boolean canHandle(Object event) {
        Class<?>[] typeArgs = TypeResolver.resolveRawArguments(
            EventHandler.class, this.getClass()
        );
        
        return typeArgs[0].isAssignableForm(event.getClass());
    }
}

Event Dispatcher인 Events 구현

public class Events {
    
    // EventHandler 목록을 보관하는 ThreadLocal 변수를 생성한다.
    private static ThreadLocal<List<EventHandler<?>>> handlers = 
        new ThreadLocal<>();

    // 이벤트를 처리 중인지 여부를 판단하는 ThreadLocal 변수를 생성한다.
    private static ThreadLocal<Boolean> publishing = 
        new ThreadLocal<Boolean>() {
            @Override
            protected Boolean initialValue() {
                return Boolean.FALSE;
            }   
        };

    // 파라미터로 전달받은 이벤트를 처리한다.
    public static void raise(Object event) {
        // 이벤트를 처리 중이면 진행하지 않는다.
        if (publishing.get()) return;
    
        try {
            // 이벤트 처리 중 상태를 true로 변경한다.
            publishing.set(Boolean.TRUE);
            
            // handlers에 담긴 EventHandler가 파라미터로 전달받은 이벤트를 처리할 수 있는지 확인하고 
            List<EventHandler<?>> eventHandlers = handlers.get();
            if (eventHandlers == null) return;
            for (EventHandler handler: eventHandlers) {
                // handlers에 담긴 EventHandler가 파라미터로 전달받은 이벤트를 처리할 수 있는지 확인한다.
                if (handler.canHandle(event)) {
                    // 처리 가능하면 핸들러의 handle() 메서드에 이벤트 객체를 전달한다.
                    handler.handle(event);
                }
            }
        } finally {
            // 핸들러의 이벤트 처리가 끝나면 처리 중 상태를 False로 변경한다.
            publishing.get(Boolean.FALSE);
        }
    }

    // 이벤트 핸들러를 등록하는 메서드
    public static void handle(EventHandler<?> handler) {
        // 이벤트를 처리 중이면 등록하지 않는다.
        if (publishing.get()) return;
        
        List<EventHandler<?>> eventHandlers = handlers.get();
        if (eventHandlers == null) {
            eventHandlers = new ArrayList<>();
            handlers.set(eventHandlers);
        }
        eventHandlers.add(handler);
    }

    // handlers에 보관된 List 객체를 삭제한다.
    public static void reset() {
        if (!publishing.get()) {
            handlers.remove();
        }
    }
}

이벤트 적용

public class CancelOrderService {
    
    private OrderRepository orderRepository;
    private RefundService refundService;

    @Transactional
    public void cancel(OrderNo orderNo) {
        // handle 메서드에 전달한 EventHandler를 이용해서 이벤트를 처리하게 된다.
        Events.handle(
            (OrderCanceledEvent evt) -> refundService.refund(evt.getOrderNumber())
        );

        Order order = findOrder(orderNo);
        order.cancel();
    
        // ThreadLocal 변수를 초기화해서 OOME가 발생하지 않도록 한다.
        Events.reset();
    }
}
public class Order {
    
    public void cancel() {
        verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        // Events.raise를 이용해서 이벤트를 발생시키면 Events.raise() 메서드는 이벤트를 처리할 핸들러를 찾아 handle() 메서드를 실행한다.
        Events.raise(new OrderCanceledEvent(number.getNumber()));
    }
}

이벤트 처리 흐름

  1. 이벤트 처리에 필요한 이벤트 핸들러를 생성한다.
  2. 이벤트 발행 전에 이벤트 핸들러를 Events.handle() 메서드를 이용해서 등록한다.
  3. 이벤트를 발행하는 도메인 기능을 실행한다.
  4. 도메인은 Events.raise()를 이용해서 이벤트를 발행한다.
  5. Events.raise()는 등록된 핸들러의 canHandle()을 이용해서 이벤트를 처리할 수 있는지 확인한다.
  6. 핸들러가 이벤트를 처리할 수 있다면 handle() 메서드를 이용해서 이벤트를 처리한다.
  7. Events.raise() 실행을 끝내고 리턴한다.
  8. 도메인 기능 실행을 끝내고 리턴한다.
  9. Events.reset()을 이용해서 ThreadLocal을 초기화한다.

동기 이벤트 처리 문제

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

A하면 이어서 B하라는 요구사항 중에서 A하면 최대 언제까지 B하라로 바꿀 수 있는 요구사항은
이벤트를 비동기로 처리하는 방식으로 구현할 수 있다.

비동기 이벤트 구현 방법

Reference