10) 이벤트

dbstmd·2024년 1월 14일
0

DDD

목록 보기
8/8

10.1 시스템 간 강결합 문제

쇼핑몰에서 구매 취소시 환불 처리를 해야한다. 도메인 객체에서 환불 기능을 실행하려면 환불 기능을 제공하는 도메인 외부 서비스를 호출하여 실행한다.

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. 추가적으로 도메인 객체에 서비스를 전달하면 설계상 문제가 나타날 수 있다.

주문 바운디드 컨텍스트와 결제 바운디드 컨텍스트간의 강결합.
주문이 강하게 결제와 결합되어 있어서 주문 바운디드 컨텍스트가 결제 바운디드 컨텍스트에 영향을 받게 된다.

이를 해결해주는게 이벤트를 사용하는 것이다.

10.2 이벤트 개요

이벤트 : 과거에 벌어진 어떤 것
주문을 취소했다면 '주문을 취소했음 이벤트'가 발생했다고 볼 수 있다.

이벤트 구성요소

  • 이벤트 생성 주체: 엔티티, 밸류, 도메인 서비스등 도메인 객체
  • 이벤트 디스패처(이벤트 퍼블리셔): 이벤트 생성 주체로부터 이벤트를 전달 받아 이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.
  • 이벤트 핸들러(이벤트 구독자): 이벤트를 전달 받아 이벤트에 담긴 데이터를 이용해 원하는 기능을 실행한다.

배송지 변경 발생 이벤트 예시

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));
  }
  ...
}
  • 배송지 정보를 변경한 뒤에 Events.raise()는 디스패처를 통해 이벤트를 전파하는 기능을 제공한다.
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());
    }
}
  • 이벤트는 데이터를 담아야 하지만 그렇다고 이벤트 자체와 관련 없는 데이터를 포함할 필요는 없다.

이벤트 용도

  • 트리거
    도메인의 상태가 바뀔 때 후처리가 필요한 경우 후처리 실행을 위한 트리거로 사용
  • 서로다른 시스템 간의 데이터 동기화
    배송지 변경시 외부 배송 서비스에 바뀐 배송지 정보를 전송 해야 한다.

이벤트의 장점

  • 도메인 로직이 섞이는 것을 방지할 수 있다.
    구매 취소 로직에 이벤트를 적용하여 환불 로직을 제거하였다.
  • 기능 확장시 핸들러를 추가하면 된다.

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

  • 이벤트 클래스: 이벤트 표현, 과거 시제 사용, 이벤트 처리를 위해 필요한 최소한의 데이터 포
  • 디스패처: 스프링이 제공하는 ApplicationEventPublisher를 이용한다.
  • Events: 이벤트를 발행한다. 이벤트 발행을 위해 ApplicationEventPublisher 를 사용한다.
  • 이벤트 핸들러: 이벤트를 수신해서 처리한다. 스프링 제공 기능 사용, 응용 서비스와 동일한 트랜잭션 범위에서 실행

이벤트 클래스의 이름을 결정할 땐 과거 시제를 사용하는 점을 유의하자.
이벤트 클래스는 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함해야 한다. 예를 들어 주문 취소됨 이벤트는 주문번호를 최소한 포함해야 한다.

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 메서드 찾기
// 이벤트 핸들러
// 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());
  }

}

흐름 정리

  • 도메인 상태 변경과 이벤트 핸들러는 같은 트랜잭션 범위에서 실행된다.
    응용 서비스와 동일한 트랜잭션 범위에서 이벤트 핸들러를 실행하고 있다.
profile
개인 학습용

0개의 댓글