도메인 모델 패턴과 동일한 전제를 기반으로 한다.
* 복잡한 비즈니스 로직을 갖는 핵심 하위 도메인에 적용.
* 밸류 오브젝트, 애그리게이트, 도메인 이벤트 (도메인 모델과 동일한 전술적 패턴)를 사용.
이벤트 소싱 도메인 모델과 도메인 모델과의 차이점
- 애그리게이트의 상태를 저장하는 방식이 다름.
- 이벤트 소싱 패턴을 사용하여 애그리게이트 상태를 관리
- 애그리게이트 상태를 유지하는 대신 모델은 각 변경사항을 설명하는 도메인 이벤트를 생성하고 애그리게이트 데이터에 대한 원천 데이터로 사용.
- 이번 편에서는 이벤트 소싱을 도메인 모델 패턴과 결합하여 이벤트 소싱 도메인 모델로 만드는 방법을 알아본다.
이벤트 소싱
- 이벤트 소싱 패턴은 데이터 모델에 시간 차원을 도입한다.
- 애그리게이트의 현재 상태를 반영하는 스키마 대신 이벤트 소싱 기반 시스템은 애그리게이트의 수명주기의 모든 변경사항을 문서화하는 이벤트를 유지.
{
"lead-id": 12,
"event-id": 0,
"event-type": "lead-initialized",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-2951",
"timestamp": "2020-05-20T09:52:55.95Z"
},
{
"lead-id": 12,
"event-id": 1,
"event-type": "contacted",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 2,
"event-type": "followup-set",
"followup-on": "2020-05-27T12:00:00.00Z",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 3,
"event-type": "contact-details-updated",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-8101",
"timestamp": "2020-05-20T12:32:08.24Z"
},
{
"lead-id": 12,
"event-id": 4,
"event-type": "contacted",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 5,
"event-type": "order-submitted",
"payment-deadline": "2020-05-30T12:02:12.51Z",
"timestamp": "2020-05-27T12:02:12.51Z"
},
{
"lead-id": 12,
"event-id": 6,
"event-type": "payment-confirmed",
"status": "converted",
"timestamp": "2020-05-27T12:38:44.12Z"
}
- 위 이벤트는 고객의 이야기 흐름을 읽을 수 있다.
- 고객의 상태는 이러한 도메인 이벤트로부터 쉽게 프로젝션(이력 형태의 데이터를 원하는 시점의 데이터로 추출하는 기법)할 수 있음.
(간단한 변환 로직을 각 이벤트에 순차적으로 적용하면됨.)
public class LeadSearchModelProjection
{
public long LeadId {get; private set;}
public HashSet<string> FirstNames {get; private set;}
public HashSet<string> LastNames {get; private set;}
public HashSet<PhoneNumber> PhoneNumbers {get; private set;}
public int Version {get; private set;}
public void Apply(LeadInitialized @event)
{
LeadId = @event.LeadId;
FirstNames = new HashSet<string>();
LastNames = new HashSet<string>();
PhoneNumbers = new HashSet<PhoneNumber>();
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version = 0;
}
public void Apply(ContactDetailsChanged @event)
{
FirstNames.Add(@event.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@event.PhoneNumber);
Version += 1;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollowupSet @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Version += 1;
}
}
- 각 이벤트 적용한 후 버전 필드가 증가함을 알 수 있음. 이는 모든 변경의 횟수를 나타냄.
- 버전5 지점의 엔티티 상태가 필요한 경우, 처음 5개 이벤트만 적용하면 됨.
검색
- 검색 기능을 구현해야 한다고 가정.
- 이벤트 소싱을 사용하면 과거정보를 쉽게 프로젝션 할 수 있음.
- 위 프로젝션 로직을 사용하여 Casey Davis 이벤트 적용하면 아래와 같은 상태가 됨.
LeadId: 12
FirstNames: ['Casey']
LastNames: ['David', 'Davis']
PhoneNumbers: ['555-2951', '555-8101']
Version: 6
분석
- 분석이 필요한 데이터를 프로젝션 로직에 포함시킨다.
- 이때 분석 최적화 모델을 데이터베이스에 유지(materialize)해야하는데 이는 CQRS(명령과 조회의 책임 분리)패턴을 통해 구현할 수 있다.
원천 데이터
- 이벤트 소싱 패턴이 작동하려면 객체 상태에 대한 모든 변경사항이 이벤트로 표현되고 저장되어야함.
(이벤트는 시스템의 원천데이터가 됨) - 이벤트 스토어에 이벤트를 저장.
이벤트 스토어
- 이벤트 스토어는 추가만 가능한 저장소이므로 이벤트를 수정하거나 삭제할 수 없음.
- 이벤트 소싱 패턴을 구현하려면 이벤트 스토어가 엔티티에 속한 모든 이벤트를 가져오고, 이벤트를 추가하는 기능을 지원해야함.
interface IEventStore
{
IEnumerable<Event> Fetch(Guid instanceId);
void Append(Guid instanceId, Event[] newEvents, int expectedVersion);
}
/**
* - Append 메서드의 expectedVersion은 낙관적 동시성 제어를 구현하는데 필요.
**/
- 본질적으로 이벤트 소싱 패턴은 새로운 것이 아니고, 원장 관리에 사용되는 로직과 유사하다.
(비트코인에서 잔액 프로젝션을 생각해보자!)
이벤트 소싱 도메인 모델
- 이벤트 소싱 도메인 모델은 애그리게이트의 수명주기를 모델링하기 위해 독점적으로 도메인 이벤트를 사용함.
- 애그리게이트 상태에 대한 모든 변경사항은 도메인 이벤트로 표현되어야 한다.
- 이벤트 소싱 애그리게이트에 대한 각 작업은 다음 단계를 따름.(책에서 소스코드도 참고해보기.)
- 애그리게이트의 도메인 이벤트를 로드.
- 이벤트를 비즈니스 의사결정을 내리는데 사용할 수 있는 상태로 프로젝션해서 상태 표현을 재구성.
- 애그리게이트의 명령을 실행하여 비즈니스 로직을 실행하고 결과적으로 새로운 도메인 이벤트를 생성.
- 새 도메인 이벤트를 이벤트 스토어에 커밋.
왜 '이벤트 소싱 도메인 모델'일까?
- 이벤트를 사용하여 상태 전환(이벤트 소싱 패턴)을 나타내는 것은 도메인 모델의 구성요소가 있든 없든 가능.
- 도메인 모델 애그리게이트의 수명주기 변경을 나타내기 위해 이벤트 소싱을 사용하고 있음을 명시적으로 보여주는 방법이 이벤트 소싱 도메인 모델임.
장점
- 기존 모델은 애그리게이트의 현재상태만 DB에 유지하지만 이벤트 소싱 도메인 모델은 모델링에 더 많은 노력이 필요.
- 시간여행
- 도메인 이벤트를 사용하여 애그리게이트의 현재 상태를 재구성할 수 있는 것 처럼, 모든 과거 상태를 복원하는 데도 사용할 수 있음.(과거 상태를 필요할 때, 재구성가능.)
- 시간여행은 시스템의 동작을 분석하고, 시스템의 의사결정을 검사하고 비즈니스 로직을 최적화할 때 종종 필요.
- 소급 디버깅을 통해 애그리게이트를 버그가 관찰되었을 때의 상태로 되돌릴 수 있음.
- 심오한 통찰력
- 시스템의 상태와 동작에 대한 깊은 통찰력을 제공.
- 이벤트 소싱은 이벤트를 다른 상태 표현 방식으로 변환할 수 있는 유연한 모델을 제공.
기존 이벤트의 데이터를 활용하여 추가 통찰력을 제공할 새로운 프로젝션 방법을 언제든지 추가할 수 있음.
- 감사 로그
- 영속적인 도메인 이벤트는 애그리게이트 상태에 발생한 모든 것에 대한 강력하게 일관된 감사로그(audit log)를 나타낸다.
- 이 모델은 화폐 또는 금전 거래를 관리하는 시스템에 잘 이용됨. (의사결정과 계정간의 자금 흐름을 쉽게 추적가능)
- 고급 낙관적 동시성 제어
- 고급 낙관적 동시성 모델은 읽기 데이터가 기록되는 동안 다른 프로세스에 의해 덮어 쓰여지는 경우 예외를 발생시킴.
- 이벤트 스토어에 동시에 추가된 정확한 이벤트를 추출하고 새로운 이벤트가 시도된 작업과 충돌하는지, 또는 추가된 이벤트가 관련이 없고 계속 진행하는 것이 안전한지에 대해 비즈니스 도메인 주도 의사결정을 내릴 수 있음.
단점
- 학습 곡선
- 팀이 지금까지 이벤트 소싱 시스템을 구현한 경험이 없다면 팀 교육과 새로운 사고 방식에 익숙해지는 시간이 필요.
- 모델의 진화
- 이벤트 소싱의 정의를 엄밀하게 따지면 이벤트는 변경할 수 없음.
- 만약 이벤트의 스키마를 조정해야 하는 경우는 테이블의 스키마를 변경하는 것만큼 간단하지 않음.
(관련 서적 - Versioning in an Event Sourced System - Greg Young)
- 아키텍처 복잡성
- 이벤트 소싱 구현에는 수많은 아키텍처의 '유동적인 부분'이 도입되어 전체 설계가 더 복잡해짐. (CQRS 아키텍처 참고)
자주 묻는 질문?
성능
- 이벤트에서 애그리게이트 상태를 재구성하면 시스템 성능에 부정적인 영향을 준다. 이벤트가 추가되면서 성능이 저하된다. 어떻게 해결할 수 있을까?
- 실제 이벤트를 상태 표현 방식으로 프로젝션하는데에 컴퓨팅 성능이 필요하며, 대부분 시스템에서 10,000개 이상의 이벤트가 있을 경우 성능 저하가 눈에 띄게 나타난다.
- 하지만 대다수의 시스템에서 애그리게이트의 평균 수명은 100개 이벤트를 초과하지 않는다.
- 따라서 성능에 문제가 되는 경우는 드물지만, 스냅숏 패턴 같은 다른 패턴을 적용할 수도 있음.
- 프로세스는 이벤트 스토어에서 새 이벤트를 지속적으로 순회하고 해당 프로젝션을 생성하고 캐시에 저장.
- 애그리게이트에 대한 작업을 실행하려면 메모리 내 프로젝션이 필요. 이때
- 프로세스는 캐시에서 현재 상태의 프로젝션을 가져온다.
- 프로세스는 이벤트 스토어에서 스냅숏 버전 이후에 발생한 이벤트를 가져온다.
- 추가 이벤트는 메모리 내 스냅숏에 적용된다.
- 참고로 스냅숏 패턴은 적용에 대한 당위성 증명이 필요한 최적화 과정이라는 점.
- 10,000개 이상의 이벤트를 저장하지 않는 경우 스냅숏 패턴을 구현하는 것은 시스템을 복잡하게 만들 뿐.
데이터 삭제
- 이벤트 스토어는 추가 전용 데이터베이스지만 물리적으로 데이터를 삭제해야 하는 경우에는 어떻게 할까?
(예 - GDPR 준수를 위해 물리적 데이터 삭제가 필요한 경우)- 이와 같은 경우는 잊어버릴 수 있는 페이로드 패턴으로 해결가능 (Forgettable payload pattern).
- 모든 민감 정보는 암호화된 형식으로 이벤트에 포함하고, 암호화 키는 외부 키-값 저장소에 저장.
- 여기서 키는 특정 애그리게이트의 ID이고 값은 암호화 키다.
- 민감 데이터를 삭제해야 하는 경우 키 저장소에서 암호화 키를 삭제한다. (이벤트에 포함된 민감 정보 접근 불가)
그외...
- 텍스트 파일에 로그를 작성하여 감사로그로 사용할 수 없는 이유는?
- 실시간 데이터 처리 DB와 로그파일 모두에 데이터를 쓰는 경우, 결국 두 가지 저장 장치에 대한 트랜잭션이 됨.
- 첫 번째 작업이 실패하면, 두 번째 작업을 롤백해야하는데 만약 DB 트랜잭션이 실패하면 아무도 이전 로그메시지를 삭제하지 않는다. 결국 로그는 일관성 없어짐.
- 상태 기반 모델을 계속 사용할 수 없지만 동일하 데이터베이스 트랜잭션에서 로그를 로그 테이블에 추가할 수 없는 이유는?
- 인프라 관점에서 이 접근 방식은 상태와 로그 레코드 간의 일관된 동기화를 제공
- 그러나 미래에 코드베이스에서 작업할 엔지니어가 적절한 로그 레코드를 추가하는 것을 잊어버린다면?
- 또한 상태 기반 표현 방식을 원천 데이터로 사용할 때 추가 로그 테이블의 스키마는 일반적으로 빠르게 혼돈에 빠짐
(모든 필수 정보가 올바른 형식으로 작성되도록 강제할 방법은 없으므로)
- 상태 기반 모델을 계속 사용할 수 없지만 레코드의 스냅숏을 만들어 전용 '이력' 테이블에 복사하는 데이터베이스 트리거를 추가할 수 없는 이유는?
- 이 방식은 로그 테이블에 레코드를 추가하기 위해 명시적으로 수동 호출이 필요하지 않으므로 이전 방식의 단점을 극복함.
(어떤 필드가 변경되었는지에 대한 사실만 포함) - 하지만 어떤 필드가 왜 변경되었는지와 같은 비즈니스 컨텍스트를 잃게 됨.
(이 같은 변경에 대한 이력 정보가 없으면 부가적인 모델을 프로젝션하는 역량이 상당히 제한됨.)
- 이 방식은 로그 테이블에 레코드를 추가하기 위해 명시적으로 수동 호출이 필요하지 않으므로 이전 방식의 단점을 극복함.
'Design > DDD' 카테고리의 다른 글
[DDD 첫걸음] 2-5. 전술적 설계 - 커뮤니케이션 패턴 (1) | 2023.07.23 |
---|---|
[DDD 첫걸음] 2-4. 전술적 설계 - 아키텍처 패턴 (0) | 2023.07.14 |
[DDD 첫걸음] 2-2. 전술적 설계 - 복잡한 비즈니스 로직 다루기. (0) | 2023.06.11 |
[DDD 첫걸음] 2-1. 전술적 설계 - 간단한 비즈니스 로직 구현. (0) | 2023.04.25 |
[DDD 첫걸음] 1-4. 전략적 설계 - 바운디드 컨텍스트 연동. (0) | 2023.04.16 |