도메인 이벤트

임태환·2025년 2월 15일

study

목록 보기
7/10

도메인 이벤트란?

도메인 이벤트는 애그리거트에 발생한 사건으로 비즈니스 도메인에서 상태 변경가 발생했음을
알리는 이벤트다.

과거 분사형 동사로 명명한 클래스로 이벤트에 의미를 부여하는 프로퍼티가 존재
프로퍼티는 원시 값 또는 밸류 객체이며 대부분 이벤트 ID, 타임스탬프 같은 메타데이터 존재

예시

Order 애그리거트라면 주문 생성, 주문 취소, 주문 배달 등 상태가 바뀌는 이벤트

도메인 이벤트를 왜 사용해야하는지?

느슨한 결합

  • 서비스가 다른 서비스에 의존하지않고 이벤트로 간접적으로 연결

확장성

  • 새로운 기능 추가 시 기존 코드를 변경하지 않고 이벤트 구독자로 추가 가능

비즈니스 로직 분리

  • 서비스 로직은 이벤트만 발행하고 부가 작업은 이벤트 핸들러가 담당

변경 이벤트를 발행하는 이유

중요한 데이터나 상태가 바뀌었을때 메시지를 시스템 내외부에 전달하는 것이다.
변경된 것을 알리는 것을 넘어서 다양한 목적을 가지고 있다

  1. 코레오그래피 사가
    • 여러 서비스 간 데이터 일관성 유지
      • 여러 개의 독립된 서비스에서 한 서비스의 상태가 변경되면 다른 서비스도 알아야함
  2. CQRS
  • 데이터를 읽기 전용과 쓰기 전용으로 나누는 방법
    • 데이터 업데이트와 빠른 조회
      • 데이터 변경 시 변경 이벤트 발행하여 읽기 전용 DB(레플리카)에 최신 상태로 반영
  1. 외부 애플리케이션 및 시스템에 알림 전달

    • 애플리케이션이 다른 외부 시스템과 연동되었을때 데이터 변경 사실을 외부에도 알려야함
      • 웹훅 : 미리 등록된 URL로 HTTP로 요청을 보내 외부 시스템에 알림 전달
      • 메시지 브로커(Kafka, RabbitMQ) : 변경 이벤트를 메시지 큐에 넣어 비동기적으로 처리
  2. 사용자에게 직접 알림 전달 (문자 메시지, 이메일)

    • 이벤트 발생시 sms나 이메일 발송 서비스로 사용자에게 알림 전달
  3. 실시간 사용자 알림

    • 주문 상태나 중요 알림을 실시간으로 받고 싶어할떄 새로고침 없이 최신 정보 확인 가능
      • 웹 소켓(WebSocket)
        변경 이벤트가 발생하면 브라우저에 실시간 메시지를 보내 주문 상태 업데이트를 바로 보여줍니다.
      • Elasticsearch 같은 텍스트 DB 업데이트
        변경 이벤트를 통해 검색 엔진의 데이터도 최신 상태로 유지할 수 있습니다.
  4. 시스템 모니터링 및 장애 감지

    • 변경 이벤트를 모니터링하여 시스템 정상 작동 여부 및 오류 발생 확인 가능
      • 이벤트 로그를 기록하고 모니터링 도구(예: Prometheus, Grafana, Elasticsearch 등)를 사용하여 실시간으로 시스템 상태를 확인한다.

도메인 이벤트 시스템의 핵심 역할을 하는 인터페이스 및 클래스 구조

/*
	예시코드
	OrderCreatedEvent와 DomainEventEnvelope interface
*/
interface DomainEvent {}
interface OrderDomainEvent extends DomainEvent {}
class OrderCreatedEvent implements OrderDomainEvent {}

interface DomainEventEnvelope<T extends DomainEvent> {
String getAggreagateID():
Message getMessage();
String getAggregateType();
String getEventId();

T getEvent();
}

DomainEvent 인터페이스

interface DomainEvent {}

도메인 이벤트를 나타내기 위한 마커 인터페이스
별도의 메서드를 정의하지 않고 이 객체는 도메인 이벤트임을 표시하는 역할이다

OrderDomainEvent 인터페이스

interface OrderDomainEvent extends DomainEvent {}

OrderCreatedEvent의 마커 인터페이스로 주문 도메인과 관련된 이벤트임을 명시한다.

OrderCreatedEvent 클래스

class OrderCreatedEvent implements OrderDomainEvent {}

주문이 생성되었음을 나타내는 구체적인 도메인 이벤트다.

DomainEventEnvelope 인터페이스

interface DomainEventEnvelope<T extends DomainEvent> {
String getAggreagateID(): // 발생한 이벤트의 애그러거트 식별자 반환
Message getMessage(); //이벤트와 함께 전파할 메타데이터를 담은 객체를 반환
String getAggregateType(); //이벤트가 속한 애그리거트의 타입을 반환
String getEventId(); //이벤트 자체를 고유하게 식별할 수 있는 이벤트 ID를 반환

T getEvent(); //실제로 발생한 도메인 이벤트 객체(T)를 반환
}

도메인 이벤트 자체외에 이벤트 객체 및 메타데이터를 조회하는 메서드가 존재하고
추가적인 메타데이터를 함께 전파하기위한 엔벨로프 역할을 한다.

이벤트 강화 기법

이벤트 발행 시점에 이벤트 컨슈머가 필요로 하는 추가 정보를 함께 포함시켜, 컨슈머가 별도의 서비스 호출 없이도 필요한 데이터를 즉시 활용할 수 있도록 하는 기법

class OrderCreatedEvent implements OrderEvent {
	private List<OrderLineItem> lineItems;
    private DeliveryInformation deliveryInformation;
    private PaymentInformation paymentInformation;
}

DeliveryInformation, PaymentInformation와 같은 필드들을 이벤트에 포함시킴으로써
이벤트 컨슈머는 별도의 서비스를 호출을 안하여도 정보를 활용 가능하다.

장점설명
컨슈머의 복잡성 감소이벤트에 필요한 정보를 모두 포함시킴으로써, 컨슈머는 별도의 서비스 호출 없이도 필요한 데이터를 즉시 활용할 수 있습니다.
시스템 효율성 향상불필요한 서비스 간 통신을 줄여 전체 시스템의 응답성과 효율성을 높일 수 있습니다.
서비스 간 의존성 감소컨슈머가 다른 서비스를 직접 호출할 필요가 없어, 서비스 간의 결합도를 낮출 수 있습니다.
단점설명
이벤트 크기 증가이벤트에 많은 정보를 포함하게 되면, 이벤트 메시지의 크기가 커져 전송 및 저장에 부담이 될 수 있습니다.
데이터 중복 가능성여러 이벤트에 동일한 정보가 포함될 수 있어 데이터 중복이 발생할 수 있습니다.
데이터 최신성 문제이벤트에 포함된 정보가 시간이 지남에 따라 변경될 수 있으며, 이벤트에 담긴 정보는 발행 시점의 상태를 반영하므로, 이후의 변경 사항을 반영하지 못할 수 있습니다.

이벤트 스토밍

도메인 이벤트는 여러 가지 방법으로 식별 가능하며
요즘은 이벤트 스토밍 방법을 사용하는 추세이다.

이벤트 스토밍의 3단계

  1. 이벤트 브레인 스토밍

    도메인 이벤트를 머릿속에서 쥐어짜 오렌지색 점착식 메모지로 구분된 도메인 이벤트를
    모델링 화면에 대략 그려 놓은 타임라인에 배치

  2. 이벤트 트리거 식별

    각각의 이벤트를 일으키는 트리거를 식별
    사용자 액션 : 파란색 점착식 메모지로 커맨드를 표시
    외부 시스템 : 자주색 점착식 메모지로 표시
    기타 도메인 이벤트
    시간 경과

  3. 애그리거트 식별

    각 커맨드 소비 후 적절한 이벤트를 발생시키는 식별해서 노란색 점착식 메모지로 표시

도메인 이벤트 생성

도메인 이벤트는 애그리거트가 발행한다.
애그리거트는 자신의 상태가 변경되는 시점과 그 결과 어떤 이벤트를 발행할지 알고 있다

인프라 관심사와 비즈니스 로직이 엉키지 않게 책임을 분리하는게 좋다
애그리거트는 상태 전이 시 이벤트를 생성하고 이렇게 생성한 이벤트를 2가지 방법으로
서비스에 반환한다.

public class Ticket {

    public List<TicketDomainEvent> accept(LocalDateTime readyBy) {
        this.acceptTime = LocalDateTime.now();
        this.readyBy = readyBy;
        return singletonList(new TicketAcceptedEvent(readyBy));
    }
}

accept 메서드는 티켓이 수락될때 호출되며
new TicketAcceptedEvent(readyBy): TicketAcceptedEvent는 티켓이 수락되었음을 나타내는 도메인 이벤트로 이 이벤트는 readyBy 시간을 포함하여 생성한다.

서비스는 애그리거트 루트 메서드를 호출한 후 이벤트를 발행한다.

public class kitchenservice{
	@Autowired
	private TicketRepository ticketRepository;

	@Autowired
	private TicketDomainEventPublisher domainEventPublisher;

	public void accept(long ticketId, LocalDateTime readyBy){
    	//DB에서 ticketID에 해당하는 티켓 조회 
		Ticket ticket = ticketRepository.findById(ticketId).orElseThrow(() ->new TicketNotFoundException(ticketId));
    // Ticket 애그리거트의 accept메서드로 티켓 수락
    // 이 메서드는 티켓의 상태를 변경하고, 해당 상태 변경을 나타내는 도메인 이벤트(TicketAcceptedEvent)를 생성하여 반환
	List<TicketDomainEvent> events = ticket.accept(readyBy);
    //도메인 이벤트 발행
	domainEvnetPublisher.publish(Ticket.class, orderId, events);
	}
}

즉 Ticket 클래스는 객체 자신의 상태 변화와 관련된 이벤트를 생성하고
KitchenService는 Ticket 객체의 수락과 관련된 비즈니스 로직을 처리하며
이벤트를 외부에 발행한다 이러한 분리로 애그리거트는 비즈니스 로직에 집중하고
서비스는 infrastructure나 외부 시스템의 통신을 처리하게 된다.

애그리거트 루트의 특정 필드에 이벤트를 쌓아두고 서비스가 이벤트를 가져다 발행하는 방법

//도메인 이벤트를 기록하는 상위 클래스 AbstractAggregateRoot 상속
public class Ticket extends AbstractAggreagateRoot{
	public void accept(LocalDateTime readBy){
		this.acceptTime = LocalDateTime.now();
        this.readyBy = readyBy;
        registerEvent(new TicketAcceptedEvent(readyBy))
	}
}

AbstractAggregateRoot를 상속받음으로써 도메인 이벤트의 등록과 관리를 위한 편의 기능을 제공받을 수 있다.
registerEvent 메서드 사용: 엔티티 내에서 발생한 도메인 이벤트를 손쉽게 등록하고, JPA의 엔티티 저장 또는 업데이트 시 자동으로 이벤트를 발행

도메인 이벤트를 확실하게 발행하는 방법

서비스는 DB에서 애그리거트를 업데이트하는 트랜잭션의 일부로 이벤트를 발행하기 위해
트랜잭션널 메시징을 사용해야한다.

이벤추에이트 트램 프레임워크에서 이런 메커니즘이 구현되어 있다.
이벤추에이트 트램 프레임워크는 DomainEventPublisher라는 인터페이스를 지원하며
오버로드된 publish() 메서드가 여러 개 정의 되어있다.

public interface DomainEventPublisher{
	// 애그리거트 타입, 애그리거트 ID, 도메인 이벤트 목록을 매개 변수로 받는다.
	void publish(String aggregateType, Object aggregateId, List<DomainEvent> domainEvents)
}

도메인 이벤트 발행의 중요성

서비스가 직접 DomainEventPublisher를 호출하여 이벤트를 발행할 수 있지만, 이 접근 방식은 서비스가 항상 올바른 이벤트를 발행한다는 보장을 제공하지 않습니다. 예를 들어, KitchenService는 Ticket 애그리거트의 이벤트 마커 인터페이스인 TicketDomainEvent를 구현한 이벤트만 발행해야 합니다. 그러나 직접 호출 방식에서는 이러한 제약을 강제하기 어렵다.

AbstractAggregateDomainEventPublisher는 타입-안전한 도메인 이벤트 발행을 지원하는 추상 클래스입니다. 이 클래스는 제네릭 타입을 사용하여 특정 애그리거트와 해당 도메인 이벤트 마커 인터페이스를 지정합니다. 이를 통해 컴파일 시점에 잘못된 이벤트 발행을 방지하고, 서비스와 애그리거트 간의 책임을 명확하게 분리할 수 있습니다.

사용 예시

// Ticket 애그리거트의 도메인 이벤트를 발행하는 클래스 정의
public class TicketDomainEventPublisher
        extends AbstractAggregateDomainEventPublisher<Ticket, TicketDomainEvent> {

    // 생성자에서 DomainEventPublisher를 주입받아 상위 클래스에 전달
    public TicketDomainEventPublisher(DomainEventPublisher eventPublisher) {
        // 상위 클래스의 생성자를 호출하여 애그리거트 타입과 ID 추출 메서드 지정
        super(eventPublisher, Ticket.class, Ticket::getId);
    }

도메인 이벤트 소비

도메인 이벤트는 결국 메시지로 바뀌어 아파치 카프카와 같은 메시지 브로커에 발행된다.

방법 1
브로커가 제공하는 클라이언트 API를 직접 사용하여 이벤트를 발행하고 처리할 수 있다.

  • 복잡성: 메시지 브로커의 설정과 이벤트 발행/수신 로직을 직접 구현해야 하므로 복잡도가 높습니다.
  • 유연성: 메시지 브로커의 세부 기능을 직접 활용할 수 있어 유연성이 높습니다.

방법 2
이벤추에이트 트램 프레임워크에 있는 DomainEventDispatcher와 같은 고수준 API를 써서
도메인 이벤트를 적절한 메서드로 디스패치 하는 방법

  • 간편성: 고수준 API를 통해 이벤트 발행과 처리를 간소화하여 개발 생산성을 높입니다.
  • 추상화: 메시지 브로커와의 직접적인 상호작용을 추상화하여 코드의 가독성과
    유지보수성을 향상시킵니다.
profile
웹 개발자

0개의 댓글