[DDD] 10장. 이벤트

매빈·2023년 5월 8일
0
post-thumbnail

10.1 시스템 간 강결합 문제

환불 기능을 실행하는 과정에서 외부 환불 서비스를 호출할 때 생기는 문제

  • 외부 시스템이 정상이 아닐 경우 트랜잭션 처리를 어떻게 할지 애매함
  • 외부 시스템의 응답 시간이 길어질 경우 내부시스템의 성능 또한 직접적으로 영향을 받음

위와 같은 문제가 발생하는 이유는 내부 BOUNDED CONTEXT와 결제 BOUNDED CONTEXT간의 높은 결합도 때문. (주문이 결제와 강하게 결합되어 있어서 주문 BOUNDED CONTEXT가 결제 BOUNDED CONTEXT에 영향을 받게 되는 것)
➡️ 이것을 해결하기 위해 이벤트를 사용함

10.2 이벤트 개요

  • 이벤트: ‘과거에 벌어진 어떤 것'
  • 이벤트가 발생한다 == 상태가 변경됐다

10.2.1 이벤트 관련 구성요소

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

    • 이벤트
    • 이벤트 생성 주체
    • 이벤트 디스패처(퍼블리셔)
    • 이벤트 핸들러(구독자)
  • 이벤트 생성 주체

    • 도메인 객체(엔티티, 밸류, 도메인 서비스)
    • 도메인 로직을 실행해서 상태가 바뀌면 관련 이벤트를 발생시킴
  • 이벤트 핸들러

    • 이벤트 생성 주체가 발생한 이벤트에 반응
    • 생성 주체가 발생한 이벤트를 전달받아 이벤트에 담긴 데이터를 이용해서 원하는 기능을 실행
  • 이벤트 디스페처

    • 이벤트 생성 주체와 이벤트 핸들러를 연결
    • 과정: 이벤트 생성 주체가 이벤트를 생성해서 디스패처에 이벤트 전달 ➡️ 디스패처가 전달받은 이벤트를 처리할 수 있는 핸들러에 이벤트 전파
  • 이벤트 디스패처의 구현 방식에 따라 이벤트 생성과 처리를 동기나 비동기로 실행하게 됨

10.2.2 이벤트의 구성

  • 이벤트

    • 발생한 이벤트에 대한 정보를 담음
    • 표현하는 정보: 이벤트 종류, 이벤트 발생 시간, 추가 데이터(이벤트와 관련된 정보, ex. 주문 번호, 신규 배송지 정보 등)
  • 이벤트 생성 주체

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

10.2.3 이벤트 용도

  1. 트리거: 도메인의 상태가 바뀔 때 후처리가 필요한 경우 후처리를 실행하기 위한 트리거로 이벤트를 사용
    (ex. 주문 취소 이벤트 ➡️ 환불 처리)
  2. 타 시스템 간의 데이터 동기화: 서로 다른 도메인 로직이 섞이는 것을 방지
    (ex. 주문 도메인 ➡️ 배송지 변경 이벤트 ➡️ 외부 배송 서비스와 배송지 정보 동기화)

10.2.4 이벤트 장점

  • 서로 다른 도메인 로직이 섞이는 것을 방지할 수 있음
  • 기능 확장에 용이

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

10.3.1 이벤트 클래스

  • 이벤트는 과거에 벌어진 상태 변화나 사건을 의미하므로 이벤트 클래스의 이름을 결정할 때에는 과거 시제를 사용
  • 이벤트를 처리하는 데 필요한 최소한의 데이터를 포함

10.3.2 Events 클래스와 ApplicationEventPublisher

  • 이벤트 발생과 출판을 위해 Spring이 제공하는 ApplicationEventPublisher 사용

10.3.3 이벤트 발생과 이벤트 핸들러

  • 이벤트 발생: Events.raise() 메서드 사용
public class Order {
	public void cancel() {
    	verifyNotYetShipped();
        this.state = OrderState.CANCELED;
        // 주문 상태를 취소로 변경한 후 이벤트 발생
        Events.raise(new OrderCanceledEvent(number.getNumber()));
    }
    ...
}
  • 핸들러: @EventListener 애너테이션 사용

10.3.4 흐름 정리

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

10.4 동기 이벤트 처리 문제

환불기능에서 만약 외부의 환불기능을 사용한다고 가정했을 때, 외부의 환불기능이 갑자기 느려지면 취소에 해당하는 cancel() 메서드도 함께 느려짐
➡️ 이벤트를 비동기로 처리함으로써 해결

10.5 비동기 이벤트 처리

이벤트를 비동기로 구현할 수 있는 방법

  • 로컬 핸들러를 비동기로 실행하기
  • 메시지 큐를 사용하기
  • 이벤트 저장소와 이벤트 포워더 사용하기
  • 이벤트 저장소와 이벤트 제공 API 사용하기

10.5.1 로컬 핸들러 비동기 실행

  • 이벤트 핸들러를 별도 스레드로 실행함으로써 이벤트 핸들러를 비동기로 실행
  • @Async 애너테이션 사용
    • @EnableAsync 애너테이션을 사용해서 비동기 기능 활성화
    • 이벤트 핸들러 메서드에 @Async 애너테이션 붙이기

10.5.2 메시징 시스템을 이용한 비동기 구현

  • 카프카래빗MQ와 같은 메시징 시스템 사용함으로써 비동기 구현
  • 이벤트 발생 ➡️ 이벤트 디스패처가 이벤트를 메시지 큐에 전송 ➡️ 메시지 큐가 이벤트를 메시지 리스너에 전달 ➡️ 메시지 리스너가 알맞은 이벤트 핸들러를 이용하여 이벤트 처리

10.5.3 이벤트 저장소를 이용한 비동기 처리

  • 이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용하여 이벤트 핸들러에 전달

[그림 10.9] 도메인의 상태와 이벤트 저장소로 동일한 DB 사용

  • 이벤트 발생 ➡️ 핸들러가 스토리지에 이벤트 저장 ➡️ 포워더가 주기적으로 이벤트 저장소에서 이벤트를 가져와 이벤트 핸들러 실행
  • 포워더는 별도 스레드를 이용하므로 이벤트 발행과 처리가 비동기로 처리됨

[그림 10.10] 이벤트를 외부에 제공하는 API 사용

  • 이벤트 발생 ➡️ 로컬 핸들러가 스토리지에 이벤트 저장 ➡️ 외부 핸들러가 API 서버를 통해 이벤트 목록을 가져감

➡️ 차이점: 이벤트가 외부에 전달되는 방식

10.6 이벤트 적용 시 추가 고려 사항

  1. 이벤트 소스를 EventEntry에 추가할지 여부
  • EventEntry
public class EventEntry {
	private Long id;
    private String type;
    private String contentType;
    private String payload;
    private long timestamp;
    
    public EventEntry(String type, String contentType, String payload) {
    	this.type = type;
        this.contentType = contentType;
        this.payload = payload;
        this.timestamp = timestamp;
    }
    
    public Long getId() {
    	return id;
    }
    
    public String getType() {
    	return type;
    }
    
    public String getContentType() {
		return contentType;
    }
    
    public String getPayLoad() {
    	return payload;
    }
    
    public long getTimestamp() {
    	return timestamp;
    }
}
  • EventEntry는 이벤트 발생 주체에 대한 정보를 갖지 않으므로 특정 주체가 발생시킨 이벤트만 조회하는 기능을 구현할 수 없음
  • 방안: 기능을 구현하기 위해 이벤트에 발생 주체 정보 추가
  1. 포워더에서 전송 실패를 얼마나 허용할 것인지
  • 포워더가 이벤트 전송에 실패하면 실패한 이벤트부터 다시 읽어와 전송 시도 ➡️ 특정 이벤트에서 계속 전송 실패하게 되면 그 이후의 이벤트를 전송하지 못하게 됨
  • 방안: 실패한 이벤트의 재전송 횟수 제한 두기
  1. 이벤트 손실
  • 로컬 핸들러를 이용하여 이벤트를 비동기로 처리할 경우 이벤트 처리에 실패하면 이벤트를 유실하게 됨
  1. 이벤트 순서
  • 메시징 시스템의 경우, 사용 기술에 따라 이벤트 발생 순서와 메시지 전달 순서가 다를 수도 있음
  1. 이벤트 재처리
  • 동일한 이벤트를 다시 처리해야 할 때 이벤트를 어떻게 할지
  • 방안: 마지막으로 처리한 이벤트의 순번을 기억, 이미 처리한 순번의 이벤트가 도착하면 해당 이벤트 처리하지 않고 무시

이벤트 처리와 DB 트랜잭션 고려
이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 함

  • @TransactionalEventListener를 사용하여 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행하도록 설정
  • 이벤트 저장소로 DB 사용: 이벤트 발생 코드와 이벤트 저장 처리를 한 트랜잭션으로 처리하면 트랜잭션이 성공할 때만 이벤트가 DB에 저장됨
    ➡️ 트랜잭션 실패에 대한 경우의 수 감소, 이벤트 처리 실패만 고민하면 됨. 이벤트 특성에 따라 재처리 방식 결정

0개의 댓글