[ ddd start! ] 10. 이벤트

박병찬·2022년 2월 27일
0

ddd start

목록 보기
10/11

CHAPTER 10. 이벤트

1. 시스템 간 강결합의 문제

환불 기능을 실행하는 과정에서 Exception이 발생하면 트랜잭션을 롤백해야할까? 아니면 일단 커밋을 해야할까?

  • 외부 시스템이 정상이 아닐 경우, 트랜잭션 처리를 어떻게 할지 애매하다는 것이다. 환불 실패시 반드시 트랜잭션을 롤백하는 것은 아니다. 주문은 취소 상태로 변경하고 환불만 나중에 다시 시도하는 방식으로 처리할 수도 있다.
  • 외부 시스템의 응답 시간이 길어질 경우 내부시스템의 성능 또한 영향을 받는 문제가 있다.

위와 같은 문제가 발생하는 이유는 내부 BOUNDED CONTEXT와 결제 BOUNDED CONTEXT간의 높은 결합도 때문이다.

  • 외부 시스템과의 결합도를 없엘 수 있는 방법은 이벤트를 사용하는 것이다.

2. 이벤트 개요

  • 이 절에서 사용하는 이벤트라는 용어는 ‘과거에 벌어진 어떤 것'을 뜻한다. 이벤트가 발생한다는 것은 상태가 변경됐다는 것을 의미한다.

  • 도메인 모델에 이벤트를 도입하려면 아래와 같은 네 개의 구성요소를 구현해야 한다.

  • 도메인 모델에서 이벤트 주체는 엔티티, 밸류, 도메인 서비스와 같은 도메인 객체이다. 이들 도메인 객체는 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생한다.

  • 이벤트 핸들러는 이벤트 생성 주체가 발생한 이벤트에 반응한다.

  • 이벤트 핸들러는 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행한다.

  • 이벤트 생성 주체와 이벤트 핸들러를 연결해 주는 것이 이벤트 디스패처이다.

  • 이벤트 생성 주체는 이벤트를 생성해서 디스패처에 이벤트를 전달한다.

  • 이벤트를 전달받은 디스패처는 해당이벤트를 처리할 수 있는 핸들러에 이벤트를 전파한다.

  • 이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 된다.

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());
  }

이벤트 용도

트리거
도메인의 상태가 바뀔때 후처리가 필요할 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.

타 시스템간의 데이터 동기화
이벤트를 사용하면 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있다.

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

  • 이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로 이벤트 클래스의 이름을 결정할 때에는 과거 시제를 사용해야 한다는 점만 유의하면 된다.

이벤트 핸들러 인터페이스는 이벤트 핸들러를 위한 상위 인터페이스이다.

  • 도메인을 사용하는 응용 서비스는 이벤트를 받아 처리할 핸들러를 Events.handle()로 등록하고, 도메인 기능을 실행한다.
  • Events는 내부적으로 핸들러 목록을 유지하기 위해 ThreadLocal을 사용한다.
  • Events.handle() 메서드는 인자로 전달받은 EventHandler를 List에 보관한다. 이벤트가 발생하면 이벤트를 처리할 Eventhandler를 List에서 찾아 EventHandler의 handle() 메서드를 호출해서 이벤트를 처리한다.

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

4. 동기 이벤트 처리 문제

  • 환불기능에서 만약 외부의 환불기능을 사용한다고 가정했을 때, 외부의 환불기능이 갑자기 느려지면 취소에 해당하는 cancel() 메서드도 함께 느려진다. 시스템의 성능 저하 뿐만 아니라 트랜잭션도 문제가 된다.
  • 외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법 중 하나가 이벤트를 비동기로 처리하는 것이다.

5. 비동기 이벤트 처리

로컬 핸들러를 비동기로 실행하기
동기를 비동기로 실행할 이벤트 핸들러를 처리하는 방식은
ExecutorService를 선언하고 submit을 이용해서 스레드 풀에 핸들러 실행 작업을 등록하는 반면에 동기로 실행할 이벤트 핸들러는 바로 실행하게 한다.

  • 이벤트를 발생시키는 도메인 기능과 메시지 큐에 이벤트를 저장하는 절차를 한 트랜잭션으로 묶어야 한다.
  • 이런 행위 자체를 같은 트랜잭션 범위에서 실행하려면 글로벌 트랜잭션이 필요한데, 글로벌 트랙잭션을 사용하면 이벤트를 안전하게 메시지 큐에 전달할 수 있지만 전체적으로 성능이 떨어지는 단점도 있다.
profile
안녕하세요

0개의 댓글