본문 바로가기

programming/DDD

[DDD] chapter 10

반응형

Chapter 10

10.1 시스템 간 강결합 문제

외부 서비스를 사용할 때 발생할 수 있는 문제점

  • 트랜잭션 처리가 애매해진다.
  • 성능이 비교적 감소한다. (외부 서비스 성능에 직접적인 영향을 받게 된다.)

위의 문제를 해결하는 방법은 이벤트를 사용하는 것이다.

10.2 이벤트  개요

이벤트란?

  • 과거에 벌어진 어떤 것.
    • 사용자가 암호를 변경했을 때 > "암호를 변경했음 이벤트"가 벌어졌다고 할 수 있다.
    • 사용자가 주문을 취소했을 때 > "주문을 취소했음 이벤트"가 벌어졌다고 할 수 있다.

도메인의 상태 변경과 관련된 요구사항들을 이벤트를 통해 구현할 수있다.

  • "주문을 취소할 때 이메일을 보낸다."라는 요구사항에서 "주문을 취소할 때"는 주문이 취소 상태로 바뀌는 것을 의미하므로 "주문 취소됨 이벤트"를 이용해서 구현할 수 있다.

10.2.1 이벤트 관련 구성요소

이벤트 구성요소

  • 이벤트 생성 주체
  • 이벤트 디스패처
  • 이벤트 핸들러

도메인 모델에서의 이벤트 생성 주체

  • 엔티티
  • 벨류
  • 도메인 서비스
  • 모든 도메인 객체

위의 이벤트 생성 주체는 도메인 로직을 실행해서 상태가 바뀌면, 관련 이벤트를 발생시킨다.

 

이벤트 핸들러

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

이벤트 디스패처

  • 이벤트 생성 주체와 이벤트 핸들러를 연결해주는 역할을 한다.
  • 디스패처는 이벤트 생성 주체가 생성한 이벤트를 받아 관련 핸들러에 뿌려주는 역할을 한다.

(Kafka의 Consumer와 Producer가 생각난다...)

10.2.2 이벤트의 구성

이벤트가 포함하는 정보

  • 이벤트 종류: 클래스 이름으로 이벤트 종류를 표현
  • 이벤트 발생 시간 
  • 추가 데이터: 주문번호, 신규 배송지 정보 등 이벤트와 관련된 정보 

다음은 배송지를 변경할 때 발생하는 이벤트 클래스이다. (MSA 공부할 때 많이 보던 것이다...)

 

public class ShippingInfoChangedEvent {
    private String orderNumber;
    private  long timestamp;
    private ShippingInfo  newShippingInfo;
}

 

이벤트는 현재 기준으로 과거에 벌어진 것을 표현하기 때문에 이벤트 이름에는 과거 시제를 사용한다.

 

다음은 Events.raise()를 사용하여 구현한 이벤트 주체의 이벤트 전달 기능 구현 방법이다.

 

public class Order {

    public void changeShippingInfo(ShippingInfo newShippingInfo){
        verifyNotYetShipped();
        setShippingInfo(newShippingInfo);
        Events.raise(new ShippingInfoChangedEvent(number, newShippingInfo))
    }
}

 

이벤트 핸들러는 이 이벤트(객체)를 받아 작업을 수행한다. 혹시 이벤트에 이러한 정보가 없다면 직접 정보를 읽어와야할 수 있다. 단, 이벤트에 필요한 정보만을 읽어와야한다.

 

10.2.3 이벤트 용도

이벤트의 용도는 크게 두 가지가 있다.

  • 트리거
    • 도메인의 상태가 바뀔 때 다른 후처리가 필요하면 후처리를 실행하기 위한 트리거로 이벤트를 사용할 수 있다.
    •  주문에서는 주문 취소 이벤트를 트리거로 사용할 수 있다.
      • 주문을 취소하면 환불을 처리해야 하는데 이때 환불 처리를 위한 트리거로 주문 취소 이벤트를 사용할 수 있다.
  • 시스템 간의 동기화
    • 배송지를 변경하면 외부 배송 서비스에 바뀐 배송지 정보를 전송해야한다. (DB도 다른걸 쓰는건가 지금...?)
      • 주문 도메인은 배송지 변경 이벤트를 발생 시키고 이벤트 핸들러는 외부 배송 서비스와 배송지 정보를 동기화 할 수 있다.

10.2.4 이벤트 장점

이벤트의 장점

  • 도메인 로직이 섞이는 것을 방지할 수 있다.
    • Order로직에서 환불로직을 사용할 필요 없이 이벤트만 발생시킬 수 있다. (이건 진짜 미쳤다...)
  • 기능 확장이 매우 용이하다. (천재다...)
    • 구매 취소 시 환불과 함께 이메일로 취소 내용을 보내고 싶다면 이메일 발송을 처리하는 핸들러를 구현하여 추가만 해주면 된다.

너무 감동 받아서 외부자료를 찾아보았는데... 무려 마이크로소프트에서 제공하는 예시가 있다...



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

10.3.1 이벤트 클래스

이벤트 자체를 위한 상위 타입은 없다. 따라서 원하는 클래스를 작성하면 된다.

 

public class OrderCanceledEvent {
    private String orderNumber;
    
    public OrderCanceledEvent(String number){
        this.orderNumber = number;
    }
    
    public String setOrderNumber(){return orderNumber;}
}

 

모든 이벤트가 공통으로 갖는 프로퍼티가 존재한다면 관련 상위 클래스를 만들 수도 있다.

 

public class Event {
    private long timestamp;
    
    public Event(){
        this.timestamp = System.currentTimeMillis();
    }
    
    public long getTimestamp(){
        return timestamp;
    }
}

 

10.3.2 Events 클래스와 ApplicationEventPublisher

이벤트 발생과 출판을 위해 Spring은 ApplicationEventPublisher를 사용한다.

 

Events 클래스의 구현

 

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

 

Events.publisher = publisher; > 이 문법이 잘 이해가 안 된다...

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

 이벤트를 발생시킬 코드는   Events.raise() 메서들르 사용한다.

 

다음은 Spring에서 제공하는 @EventListener 어노테이션을 사용하여 구현한 예이다.

 

@Service
public class OrderCanceledEventHandler {
    private RefundService refundService;
    
    public OrderCanceledEventHandler(RefundService refundService){
        this.refundService = refundService;
    }
    
    @EventListener(OrderCanceledEvent.class)
    public void handle(OrderCanceledEvent event){
        refundService.refund(event.getOrderNumber());
    }
    
}

 

만약 OrderCanceledEvent.class값을 가지는 이벤트가 발생하면 설정된 @EventListener() 어노테이션 안에  Order CanceledEvent.class를 가지는 모든 메소드가 실행된다. (이정도면 카프카가 아닌가...?)

 

좀 더 찾아보니 Kafka와 Spring의 EventListener는 관련이 없는 것 같다.


 

 

SpringBoot에서 Kafka Event Message 보내고 받기

빅 데이터의 세계에서 안정적인 스트리밍 플랫폼은 필수이다. 현재 가장 주목받고있는 kafka 스트리핑 플랫폼과 SpringBoot를 활용해서 간단한 Event 메시지를 주고받는 작업을 해보자

www.wool-dev.com


10.4 동기 이벤트 처리 문제

외부 시스템과의 연동을 동기로 처리할 때 발생하는 성능과 트랜잭션 범위 문제를 해소하는 방법은 이벤트를 비동기로 처리하거나 이벤트와 트랜잭션을 연계하는 것이다.

10.5 비동기 이벤트 처리

"A하면 B를 한다." 라는 방식의 요구사항은 대부분 "A하면 일정기간 안에 B를 하라." 라는 경우가 많다. A를 이벤트의 관점으로 보면서 비동기 이벤트 처리를 구현할 수 있다.

 

비동기 이벤트 처리의 구현방식

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

10.5.1 로컬 핸들러 비동기 실행

이벤트 핸들러를 비동기로 실행하는 방법은 이벤트 핸들러를 별도 스레드로 실행하는 것이다.

 

@EnableAsync 어노테이션을 사용하여 비동기를 활성화하고 이벤트 핸들러 메서드에 @Async 어노테이션을 붙이면 된다.

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

Kafka나 RabbitMQ를 사용하는 메시징 시스템을 사용하여 구현할 수 있다.

 

메시징 시스템은 Dispatcher와 Handler사이에서 작동한다. 



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

이벤트를 일단 DB에 저장한 뒤에 별도 프로그램을 이용해서 이벤트 핸들러에 전달하는 방식으로 구현할 수 있다.



하나의 DB를 도메인과 포워더가 같이 사용하게 된다.

 

구현 방식을 보니, 외부 API를 사용하는 경우 장점을 가질 것 같지만, 지금 나의 상황에서는 Kafka가 더 끌린다.

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

이벤트 적용 시 고려애햐 할 사항

  • 이벤트 소스를 EventEntry에 추가할지에 대한 여부
  • 포워더의 전송 실패 허용 범위
  • 이벤트 손실
  • 이벤트 순서
  • 이벤트 재처리

10.6.1 이벤트 처리와 DB 트랜잭션 고려

주문 취소 기능의 이벤트와 흐름

  • 주문 취소 기능은 주문 취소 이벤트를 발생시킨다.
  • 주문 취소 이벤트 핸들러는 환불 서비스에 환불 처리를 요청한다.
  • 환불 서비스는 외부 API를 호출해서 결제를 취소한다.

이벤트 처리를 동기로 하든 비동기로 하든 이벤트 처리 실패와 트랜잭션 실패를 함께 고려해야 한다. 그러나 경우의 수가 많아지므로, 트랜잭션이 성공할 때만 이벤트 핸들러를 실행하는 것이다.

 

스프링은 @TransactionalEventListener 어노테이션을 지원한다. 해당 어노테이션은 스프링 트랜잭션 상태에 따라 이벤트 핸들러를 실행할 수 있게 한다.

 

@Service
public class OrderCanceledEventHandler {
    private RefundService refundService;

    public OrderCanceledEventHandler(RefundService refundService){
        this.refundService = refundService;
    }
    
    @TransactionalEventListener(
            classes = OrderCanceledEvent.class;
            phase = TransactionPhase.AFTER_COMMIT;
    )
    public void handle(OrderCanceledEvent event){
        refundService.refund(event.getOrderNumber());
    }
}

 

해당 설정을 사용하면 트랜잭션 커밋에 성공한 뒤에 핸들러 메서드를 실행한다. 만약 중간에 에러가 발생되어 트랜잭션이 롤백 되면 핸들러 메소드를 실행하지 않는다.

반응형

'programming > DDD' 카테고리의 다른 글

[DDD] chapter 11  (4) 2024.03.09
[DDD] chapter 9  (1) 2024.02.27
[DDD] chapter 8  (0) 2024.02.21
[DDD] chapter 7  (0) 2024.02.16
[DDD] chapter 6  (0) 2024.02.10