도메인 이벤트
도메인 이벤트
- 이벤트는 과거에 발생한 것[2]
- 도메인 전문가가 제공하는 유비쿼터스언어에 기반한 도메인 규칙을 명시적으로 표현할 수 있습니다.
- 데이터베이스 트랜잭션과 마찬가지로 도메인 이벤트와 관련된 모든 작업이 성공적으로 완료되거나 수행되지 않아야 합니다.
- 도메인 이벤트와 메시징 스타일 이벤트의 주요 차이점
- 메시지는 항상 비동기식으로 전송되고 프로세스와 머신에서 통신됩니다. 이것은 다수의 바인딩된 컨텍스트, 마이크로 서비스 또는 다른 애플리케이션을 통합하는데 유용합니다.
- 도메인 이벤트는 현재 실행 중인 도메인 작업에서 이벤트를 발생시키려고 하지만 파생 작업은 동일한 도메인 내에서 발생하기를 바랍니다. 도메인 이벤트와 그 파생 작업(이벤트 처리기가 관리하는 이후에 트리거되는 동작)은 거의 즉시, 일반적으로 프로세스 내에서 그리고 동일한 도메인 내에서 발생해야 합니다. 따라서 도메인 이벤트는 동기 또는 비동기일 수 있습니다. 단 통합 이벤트는 항상 비동기여야 합니다.
- 도메인 및 통합이벤트
- 구현 차이점
- 도메인 이벤트: IoC 컨테이너 또는 다른 메서드를 기반으로 메모리 내 중재자로 구현될 수 있는 도메인 이벤트 디스패치에 푸시되는 메세지입니다.
- 통합 이벤트
- 목표: 커밋된 트랜잭션 및 업데이트를 다른 마이크로 서비스, 바인딩된 컨텍스트 또는 외부 애플리케이션과 같은 추가적인 하위 시스템에 전파
- 엔티티가 성공적으로 지속되고 있는 경우에만 발생해야 하며, 그렇지 않으면 전체 작업이 발생하지 않은 것처첨 처리됩니다.
- 여러 마이크로 서비스(기타 바인딩된 컨텍스트) 또는 외부 시세틈/애플리케이션 간에도 비동기 통신을 기반으로 해야 합니다.
- 구현 차이점
- 도메인 이벤트 처리는 도메인 모델 계층이 아닌 애플리케이션 관련 문제입니다. 도메인 모델 계층은 도메인 논리(도메인 전문가가 이해하는 논리)에만 집중해야 하며 리포지토리를 이용한 파생 작업 지속 동작 및 처리기와 같은 애플리케이션 인프라는 신경 쓸 필요가 없습니다. 따라서 애플리케이션 계층 수준은 도메인 이벤트가 발생할 때 동작을 트리거하는 도메인 이벤트 처리기가 있어야 하는 곳입니다.
여기서 요점은 도메인 이벤트가 발생할 때 실행되는 동작의 수가 변화할 수 있다는 점입니다. 궁극적으로 도메인과 애플리케이션의 작업 및 규칙 수는 증가합니다. 어떤 일이 일어날 때 발생하는 파생 작업의 복잡성과 수는 증가하지만, 코드가 “접착제”(즉, new를 사용하여 특정 개체를 만드는 것)와 결합된 경우 새 작업을 추가해야 할 때마다 작업 및 테스트 코드도 변경해야 합니다. 이 변경으로 인해 새 버그가 발생할 수 있으며, 이 방식은 SOLID의 개방/폐쇄 원칙에도 위배됩니다. 뿐만 아니라, 작업을 오케스트레이션하는 원래 클래스가 계속 증가하게 되는데, 이것은 SRP(단일 책임 원칙)에 위배됩니다.
- 책임 분리 설계
- 명령 보내기(CreateOrder)
- 명령 처리기에서 명령 수신하기
- 단일 집계의 트랜잭션 실행
- (Optional) 파생 작업에 대해 도메인 이벤트 발생시키기(OrderStartedDomainEvent)
- 여러 집합체나 애플리케이션 동작에서 다수의 파생 작업을 실행하는 도메인 이벤트(현재 프로세스 내) 처리
- buyer(구매자) 및 payment(지불) 메서드를 검증 또는 생성
- 관련 통합 이벤트를 생성하고 이벤트 버스에 보내서 마이크로 서비스에 상태를 전파하거나 외부 동작(예: 구매자에게 이메일 보내기)을 트리거
- 다른 파생 작업을 처리
- 명령 처리기와 이벤트 처리기
- 두 가지 모두 애플리케이션 계층의 일부
- 명령처리기: 명령을 한 번만 처리 해야 함.
- 도메인 이벤트 처리기: 0번 또는 n번 처리될 수 있습니다. 각 처리기마다 다른 목적으로 여러 수신자 또는 이벤트 처리기에서 수신될 수 있기 때문입니다.
도메인 이벤트 구현
- 도메인 이벤트는 DTO와 같이 단지 데이터를 보유하는 구조체 또는 클래스 입니다.
- 방금 발생한 내용과 관련된 모든 정보를 포함
- 도메인 유비쿼터스 언어 측면에서 볼 때 이벤트는 과거에 발생한 것이기 때문에 이벤트의 클래스 이름은 과거 시제 동사로 나타내야 합니다.
- 과거에 발생한 것이므로 변경되지 않는다 –> 불변객체여야 합니다. (readonly 활용)
- 직렬화 혹은 역직렬화 큐를 이용하는 경우, 속성은 readonly 대신 private 이어야 하므로 역직렬 변환기는 큐에서 제거할 때 값을 할당할 수 있습니다.
도메인 이벤트 발생
- 이벤트를 관리하고 발생시키기 위해 정적 클래스를 사용하도록 제안하였음
DomainEvents.Raise(Event myEvent) - Udi Dahan-정적 클래스 사용
- Jimmy Bogard-유사방식
- 정적 클래스를 사용하는 경우 처리기에 즉시 디스패치됩니다. 이로인해 테스트와 디버깅이 더 어려워지며, 이것은 파생 작업논리가 있는 이벤트 처리기가 이벤트가 발생한 직후에 실행되기 때문입니다. 테스트 및 디버깅을 수행하는 경우에는 현재 Agreegate 클래스 및 여기서 벌어지는 일에만 집중하는 것이 좋습니다. 다른 Agreegate나 애플리케이션 논리와 관련된 파생 작업에 대한 다른 이벤트 처리기로 갑자기 리디렉션되는 것은 좋지 않습니다. 이런 이유 때문에 다른 방식이 진화하였습니다.(발생 및 디스패치 이벤트에 대한 지연된 접근 방법)
발생 및 디스패치 이벤트에 대한 지연된 접근 방법
- 도메인 이벤트 처리기에 즉시 디스패치하는 것보다 좋은 방법은 도메인 이벤트를 컬렉션에 추가한 다음, 트랜잭션을 커밋하기 직전이나 직후에 도메인 이벤트를 디스패치 하는 것입니다. - Jimmy Bogard-A better domain events pattern
- 도메인 이벤트를 트랜잭션을 커밋하기 직전에 보낼지 또는 직후에 보낼지를 결정하는 것은 중요합니다. 그에 따라 파생 작업을 동일한 이벤트의 일부로 포함시킬 지 또는 다른 트랜잭션에 포함시킬 지가 결정되기 때문입니다.
- 다른 트랜잭션에 포함시킬때는 여러 집합체 전반에서 최종 일관성을 처리해야 합니다.
abstract class Entity {
private List<INotifiaction> domainEvents;
public void addDomainEVent(INotification eventItem) {
domainEvents.add(eventItem);
}
}
- 엔티티 개체의 일부이거나, 더 좋게는 기준 엔티티 클래스의 일부여야 합니다.
집합체 전반의 단일 트랜잭션 및 집합체 전반의 최종 일관성
- 하나의 트랜잭션이 하나의 Agreegate라는 규칙을 옹호 (Eric Evans, Vaughn Vernon)
- 집합체에 걸쳐있는 규칙은 항상 최신 상태가 유지될 것으로 예상되지 않습니다. 이벤트 처리, 일괄 처리 또는 기타 업데이트 메커니즘을 통해 다른 종속성이 특정 시간 내에 확인될 수 있습니다.
- Vaughn Vernon-Effective Aggregate Design
- 하나의 Aggregate 인스턴스에서 명령을 실행하는 경우 하나 이상의 Aggregate에서 추가적인 비즈니스 규칙을 실행해야 하며, 최종 일관성을 사용하여 DDD 모델에서 최종 일관성을 지원하는 실용적인 방법이 있습니다. Aggregate 메서드는 하나 이상의 비동기 구독자에게 제시간에 배달되는 도메인 이벤트를 게시합니다.
- 높은 확장성이 필요한 대규모 애플리케이션에서는 데이터베이스 잠금 수가 상당할 것이라는 생각 때문, 이런 상황에서는 즉각적인 트랜잭션 일관성이 필요하지 않다는 사실을 수용하면 최종 일관석 개념을 받아들이는 데 도움이 됩니다. 비즈니스에서 원자성 변경은 필요하지 않은 경우가 많으며 ==특정 작업에 원자성 트랜잭션이 필요한지 여부를 결정하는 것은 어떤 경우든 도메인 전문가의 책임입니다. 작업에 여러 Aggregate 사이의 원자성 트랜잭션이 항상 필요한 경우에는 Aggregate가 저 커야 하는지 또는 제대로 설계되지 않은 것인지에 의문을 가져하 합니다.==
- 최종 일관성을 위해서는 Aggregate 전반에서 잠재적인 불일치를 탐지하고 보정 작업을 구현하기 위해 더 복잡한 코드가 필요합니다. Aggregate 변경 내용을 커밋한 후 이벤트가 디스패치 될 때 문제가 발생하고 이벤트 처리기가 파생 작업을 커밋할 수 없는 경우 집계 사이에 불일치가 발생합니다.
- 하나의 Aggregate 인스턴스에서 명령을 실행하는 경우 하나 이상의 Aggregate에서 추가적인 비즈니스 규칙을 실행해야 하며, 최종 일관성을 사용하여 DDD 모델에서 최종 일관성을 지원하는 실용적인 방법이 있습니다. Aggregate 메서드는 하나 이상의 비동기 구독자에게 제시간에 배달되는 도메인 이벤트를 게시합니다.
- Jimmy Bogard와 같은 설계자는 단일 트랜잭션이 여러 집합체에 걸쳐 있어도 괜찮다고 합니다. 단, 추가적인 집합체가 동일한 원래 명령의 파생 작업과 관련이 있는 경우에 한합니다.
- 일반적으로 도메인 이벤트의 파생 작업이 논리 트랜잭션 내에서 발생하기를 바라지만 도메인 이벤트를 발생시키는 범위와 동일해야 하는 것은 아닙니다. 트랜잭션을 커밋하기 직전에 이벤트를 해당 처리기에 디스패치합니다.
- 트랜잭션을 커밋하기 직전에 도메인 이벤트를 디스패치 하는 것은 이러한 이벤트의 파생 작업을 동일한 트랜잭션에 포함시키기를 바라기 때문입니다. EF DbContext SaveChanges 메서드가 실패하면 트랜잭션은 과년 도메인 이벤트 처리기가 수행한 파생 작업의 결과를 비롯한 모든 변경 내용을 롤백합니다.(HttpRequest) 범위와 일치
- 이게 더 쉬움
도메인 이벤트 디스패치: 이벤트에서 이벤트 처리기로 매핑
- 방법1(오버스펙): 실제 메세지 시스템 또는 메모리 내 이벤트와 대조적으로 서비스 버스를 기반으로 할 수 있는 이벤트 버스입니다. 동일한 프로세스 내(동일한 도메인 및 애플리케이션 계층 내)에 있는 이벤트만 처리하면 되기 때문에 실제 메세지는 도메인 이벤트를 처리하기에 과도합니다.
- IoC 컨테이너 형식 등록을 사용하여 이벤트를 디스패치할 곳을 동적으로 추론하는 것입니다. 특정 이벤트를 얻기 위해 이벤트 처리기에 무엇이 필요한지 알아야 합니다.
- (데이터 흐름기준): Domain -> Transaction Infrastructure -> DomainEVent Dispatcher -> Event Handler via IoC container
이벤트 처리 방법
- 인프라 리포지토리를 사용하여 필요한 추가 Aggregate를 확보하고 파생 작업 도메인 논리를 실행하는 애플리케이션 계층 코드를 구현합니다.
class OrderStartedDomainEventHandler { private readonly BuyerRepository buyerRepository; private readonly IdentityService identityService; public handleAsync(OrderStartedDomainEvent orderStartedEvent) { Long userId = this.identityService.getUserIdentity(); Buyer buyer = buyerRepository.findByUserId(userId); // buyer 없는 경우 if(buyer == null) { buyer = new Buyer(userId); } buyer.addPayment(orderStartedEvent.cardNumber, orderStartedEvent.Order.Id); // buyer 없는 경우 if(buyer == null) { this.buyerRepository.add(buyer); } this.buyerRepository.update(buyer); this.buyerRepository.UnitOfWork.SaveEntitiesAsync(); // UnitOfWork 이용 하는 것을 가정 } } - 도메인 이벤트는 마이크로 서비스 경계 밖에 게시할 통합 이벤트를 생성할 수 있습니다.
Example
// 주문이 생성되었을 경우
public void ValidateOrAddBuyerAgreegateWhenOrderStartedDomainEventHandler {
// OrderStartedDomainEvent 이벤트 발행
this.publisher.emit(OrderStartedDomainEvent);
}
- 원칙적으로 “시작 후 망각형(Fire and Forget)” 명령은 절대 사용하지 말아야 합니다. 모든 비즈니스 애플리케이션은 명령이 성공적으로 처리되었는지 아니면 최소한 유효성이 검사되고 수락되었는지를 알아야 합니다.
- 비동기 명령은 실패를 나타낼 간단한 방법이 없기 때문에 시스템의 복잡성을 크게 증가시킵니다. 따라서 크기 조정 요구 사항이 필요하거나 메시지를 통해 내부 마이크로 서비스를 통신하는 특수한 경우가 아니라면 비동기 명령은 사용하지 않는 것이 좋습니다. 이런 경우 장애에 대한 별도의 보고 및 복구 시스템을 설계해야 합니다.