Ch7. 시간 차원의 모델링
[이벤트 소싱]
프레드 브룩스의 추론을 사용해서 이벤트 소싱 패턴을 정의하고, 이것이 기존의 모델링과 데이터 저장 방식 측면에서 어떻게 다른지 이해해보자.
먼저 표 7-1의 데이터를 살펴보자.


이것은 텔레마케팅 시스템에서 잠재 고객 또는 리드를 관리하는데 사용하는 테이블임에 분명하다.
다양한 상태를 조사하여 각 잠재 고객의 처리 주기를 가정할 수도 있다.
- 판매 흐름은 NEW_LEAD 상태의 잠재 고객과 함께 시작한다.
- 판매 전화는 제안에 관심이 없는 사람(리드가 CLOSED), 후속 전화 예약(FOLLOWUP_SET), 또는 제안 수락(PENDING_PAYMENT)으로 종료될 수 있다.
- 결제가 성공하면 리드가 고객으로 전환(CONVERTED)된다. 결제가 실패할 수도 있다(PAYMENT_FALIED)
테이블의 데이터는 리드의 현재 상태를 문서화하지만 각 리드가 현재 상태에 도달한 이력에 대한 이야기가 누락되었다.
즉, 우리가 알 수 있는 건 리드의 현재 상태 뿐이다.
이러한 누락된 정보를 채우는 방법 중 하나가 이벤트 소싱을 이용하는 것이다.
이벤트 소싱 패턴은 데이터 모델에 시간 차원을 도입한다.
애그리게이트의 현재 상태를 반영하는 스키마 대신 수명주기의 모든 변경사항을 문서화하는 이벤트를 유지한다.
표 7-1의 12행에 있는 CONVERTED 고객을 생각해보자.
{
"lead-id": 12,
"evnet-id": 0,
"event-type": "lead-initinalized",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-2951",
"timestamp": "2020-05-20T09:52:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 1,
"event-type": "contacted",
"timestamp": "2020-05-20T12:32:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 2,
"event-type": "followup-set",
"followup-on": "2020-05-20T12:32:55.95Z",
"timestamp": "2020-05-20T12:32:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 3,
"event-type": "contacted-details-updated",
"first-name": "Casey",
"last-name": "David",
"phone-number": "555-8101"
"timestamp": "2020-05-20T12:32:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 4,
"event-type": "contacted",
"timestamp": "2020-05-27T12:32:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 5,
"event-type": "order-submitted",
"payment-deadline": "2020-05-30T12:32:55.95Z"
"timestamp": "2020-05-27T12:32:55.95Z"
},
{
"lead-id": 12,
"evnet-id": 6,
"event-type": "payment-confirmed",
"status": "conveted"
"timestamp": "2020-05-27T12:38:55.95Z"
},
위 목록의 이벤트는 고객의 이야기를 알려준다.
고객의 상태는 이러한 도메인 이벤트로부터 쉽게 프로젝션할 수 있다.
public class LeadSearchModelPrjoection
{
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 = @evnet.LeadId;
FirstNames = new HashSet<string>();
LastNames = new HashSet<string>();
PhoneNumbers = new HashSet<PhoneNumbers>();
FirstNames.Add(@evnet.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@evnet.PhoneNumber);
Verson = 0;
}
public void Apply(ContactDetailsChanged @evnet)
{
FirstNames.Add(@evnet.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@evnet.PhoneNumber);
Verson += 1;
}
public void Apply(Contacted @event)
{
Version += 1;
}
public void Apply(FollwupSet @event)
{
Version += 1;
}
public void Apply(OrderSubmitted @event)
{
Version += 1;
}
public void Apply(PaymentConfirmed @event)
{
Version += 1;
}
}
애그리게이트의 이벤트를 반복해서 순서대로 적절히 재정의한 Apply 메서드에 넣으면 정확한 상태 표현을 할 수 있다.
검색
검색 기능을 구현한다고 가정해보자.
이벤트 소싱을 사용하면 과거 정보를 쉽게 프로젝션 할 수 있다.
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 = @evnet.LeadId;
FirstNames = new HashSet<string>();
LastNames = new HashSet<string>();
PhoneNumbers = new HashSet<PhoneNumbers>();
FirstNames.Add(@evnet.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@evnet.PhoneNumber);
Verson = 0;
}
public void Apply(ContactDetailsChanged@evnet)
{
FirstNames.Add(@evnet.FirstName);
LastNames.Add(@event.LastName);
PhoneNumbers.Add(@evnet.PhoneNumber);
Verson += 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(PaymentConfired@event)
{
Version += 1;
}
}
이 프로젝션 로직은 LeadInitialized 와 ContactDetailsChanged 이벤트를 이용하여 각 리드의 개인 세부 정보를 채운다.
분석
어떤 부서에서 좀 더 분석하기 편한 리드 데이터를 요청한다고 가정해보자.
다양한 리드 중에서 후속 전화가 예약된 개수를 얻고자 한다.
public class AnalysisMdelProjection
{
public long LeadId {get; private set; }
public int Followups {get; private set; }
public LeadStatus Status {get; private set; }
public int Version {get; private set; }
public void Apply(LeadInitialized @evnet)
{
LeadId = @event.LeadId;
Followups = 0;
Status = LeadStatus.NEW_LEAD;
Version = 0;
}
public void Apply(Contacted@evnet)
{
Version += 1;
}
public void Apply(FollowupSet@evnet)
{
Status = LeadStatus.FOLLOWUP_SET;
Followups += 1;
Version += 1;
}
public void Apply(ContactDetailsChanged@evnet)
{
Version += 1;
}
public void Apply(OrderSubmitted@evnet)
{
Status = LeadStatus.PENDING_PAYMENT;
Version += 1;
}
public void Apply(PaymentConfirmed@evnet)
{
Status = LeadStatus.CONVERTED;
Version += 1;
}
}
원천 데이터
이벤트 소싱 패턴이 작동하려면 객체 상태에 대한 모든 변경사항이 이벤트로 표현되고 저장되어야 한다.
이러한 이벤트는 시스템의 원천 데이터가 된다.

시스템의 이벤트를 저장하는 데이터베이스는 유일하고 강력하게 일관된 저장소인 시스템의 원천 데이터다.
이벤트를 저장하는데 사용되는 데이터베이스를 지칭하는 이름이 이벤트 스토어다.
이벤트 스토어
이벤트 스토어는 추가만 가능하고, 이벤트를 수정하거나 삭제할 수 없다.
즉, 특정 빈즈니스 엔티티에 속한 모든 이벤트를 가져오고 이벤트를 추가하는 것이다.
interface IEventStore
{
IEnumerable<Event> Fecth(Guid instanceId);
void Append(Guid instanceId, Event[] newEvents, int expectedVersion);
}
Append 메서드의 expectedVersion 인수는 낙관적 동시성 제어를 구현하는데 필요하다.
[이벤트 소싱 도메인 모델]
이벤트 소싱 도메인 모델은 애그리게이트의 수명주기를 모델링하기 위해 독점적으로 도메인 이벤트를 사용한다.
이벤트 소싱 애그리게이트에 대한 각 작업은 다음 단계를 따른다.
- 애그리게이트의 도메인 이벤트를 로드한다.
- 이벤트를 비즈니스 의사결정을 내리는데 사용할 수 있는 상태로 프로젝션해서 상태 표현을 재구성한다.
- 애그리게이트의 명령을 실행하여 비즈니스 로직을 실행하고, 결과적으로 새로운 도메인 이벤트를 생성.
- 새 도메인 이벤트를 이벤트 스토어에 커밋한다.
6장의 Ticket 애그리게이트 예제로 돌아가서 이벤트 소싱 애그리게이트로 구현해보자.
public class TicketAPI
{
private ITicketRepository _ticketRepository;
...
public void RequestEscalation(TicketId id, EscalationReason reason)
{
var events = _ticketRepository.LoadEvents(id);
var ticket = new Ticket(events);
var originalVersion = ticket.Version;
var cmd = new RequestEscalation(reason);
ticket.Execute(cmd);
_ticketRepository.CommitChanges(ticket, originalVersion);
}
...
}
public class Ticket
{
...
private List<DomainEvent> _domainEvents = new List<DomainEvent>();
private TicketState _state;
...
public Ticket(IEnumerable<IDomainEvents> events)
{
_state = new TicketState();
foreach(var e in events)
{
AppendEvent(e);
}
}
}
AppendEvent는 들어오는 이벤트를 TicketState 프로젝션 로직에 전달하여 티켓의 현재 상태에 대한 메모리 내 표현 방식을 만든다.
private void AppendEvent(IDomainEvent@event)
{
_domainEvnets.Append(@evnet);
// "Apply" 메서드의 올바른 오버로드를 동적으로 호출함.
((dynamic) state).Apply((dynamic) @event);
}
장점
애그리게이트의 현재 상태만을 데이트베이스에 유지하는 기존 모델에 비해 이벤트 소싱 도메인 모델은 애그리게이트를 모델링하는데 더 많은 노력이 필요하다.
그러나 이 패턴은 많은 시나리오에서 고려해볼만한 가치가 있다.
시간 여행
- 도메인 이벤트를 사용하여 애그리게이트의 현재 상태를 재구성할 수 있는것 처럼 도메인 이벤트는 애그리게이트의 모든 과거 상태를 복원하는데 사용할 수 있다.
- 시간 여행은 시스템의 동작을 분석하고, 시스템의 의사결정을 검사하고, 비즈니스 로직을 최적화할 때 종종 중요하다.
심오한 통찰력
- 이벤트 소싱은 시스템의 상태와 동작에 대한 깊은 통찰력을 제공한다.
감사 로그
- 영속적인 도메인 이벤트는 애그리게이트 상태에 발생한 모든 것에 대한 강력하게 일관된 감사 로그(audit log)를 나타낸다.
- 법률에 따라 일부 비즈니스 도메인은 반드시 이러한 감사 로그를 구현해야 하며, 이벤트 소싱은 이를 즉시 제공한다.
고급 낙관적 동시성 제어
- 고급 낙관적 동시성 모델은 읽기 데이터가 기록되는 동안 다른 프로세스에 의해 덮어 쓰여지는 경우 예외를 발생시킨다.
단점
이제 이벤트 소싱 도메인에 대한 몇가지 문제애 대해 살펴보자.
학습 곡선
- 패턴이 데이터를 관리하는 기존 기술과 엄청난 차이가 있다는 명백한 단점이다.
모델의 진화
- 이벤트 소싱 모델을 발전시키는 것은 어려울 수 있다.
아키텍처 복잡성
- 이벤트 소싱을 구현하면 수많은 아키텍처의 ‘유동적인 부분’이 도입되어 전체 설계가 복잡해진다.
[결론]
이벤트 소싱 도메인 모델에서 애그리게이트 상태에 대한 모든 변경사항은 일련의 도메인 이벤트로 표현된다.
결과 도메인 이벤트는 애그리게이트의 현재 상태를 프로젝션하는데 사용할 수 있다.
이 패턴은 분석과 최적화를 위해, 또는 법적으로 감사 로그를 요구하기 때문에 시스템 데이터에 대한 심오한 통찰력이 필요한 경우 적합하다.
Ch8. 아키택처 패턴
[비즈니스 로직과 아키텍처 패턴]
비즈니스 로직은 소프트웨어에서 가장 중요한 요소다.
코드베이스가 처리해야 할 다양한 관심사로 인해 비즈니스 로직이 다양한 구성요소로 흩어지기 쉽다.
관심사를 구현할 때 엄격하게 구성하지 않으면 코드베이스의 변경이 어려워 진다.
아키텍처 패턴은 코드베이스 사이의 구성 원칙을 도입하고 이들 사이의 명확한 경계를 제시한다.
지금부터 세가지 주요 아키텍처 패턴인 계층형 아키텍처, 포트와 어댑터, CQRS에 대해 알아보자.
[계층형 아키텍처]
계층형 아키텍처(layered architecture)는 가장 일반적인 아키텍처 패턴 중 하나다.
수평 계층으로 조직하여 각 계층은 사용자와 상호작용, 비즈니스 로직 구현, DB 저장과 같은 기술 구현으로 나뉜다.

고전적인 형태로 계층형 아키텍처는
- 프레젠테이션 계층(PL)
- 비즈니스 로직 계층(BLL)
- 데이터 접근 계층(DAL)
로 구성된다.
프레젠테이션 계층
프르젠테이션 계층은 사용자와 상호작용을 위해 인터페이스를 구현한다.
즉, 프레젠테이션 계층은 프로그램의 퍼블릭 인터페이스다.

비즈니스 로직 계층
이 계층은 비즈니스 로직을 구현하고 묶는 것을 담당한다.
이곳에서 비즈니스 의사결정을 구현한다.

데이터 접근 계층
데이터 접근 계층은 영속성 메커니즘에 접근할 수 있게 해준다.
전통적으로는 데이터베이스만 가리켰으나, 현대 시스템에서는 더 넓은 범위의 책임을 가진다.

계층 간 커뮤니케이션
계층은 탑다운(top-down) 커뮤니케이션 모델에 따라 연동한다.
즉, 각 계층은 바로 아래 계층에만 의존한다.

변종(variation)
계층형 아키텍처 패턴을 확장해서 서비스 계층을 추가한 것을 흔히 볼 수 있다.
서비스 계층
“가용한 오퍼레이션을 구축하고, 각 오퍼레이션에서 애플리케이션의 응답을 조정하는 서비스 계층을 애플리케이션의 경계에 정의한다.”
서비스 계층은 프로그램의 프래젠테이션 계층과 비즈니스 로직 계층 사이의 중간 역할을 한다.

여기서 서비스 계층은 논리적 경계가 된다는 것이 중요하다.
계층형 아키텍처에 사용되는 다른 용어를 봤을 수도 있다.
- 프레젠테이션 계층 = 사용자 인터페이스 계층
- 서비스 계층 = 애플리케이션 계층
- 비즈니스 로직 계층 = 도메인 계층 = 모델 계층
- 데이터 접근 계층 = 인프라스트럭처 계층
계층형 아키텍처를 사용하는 경우
비즈니스 로직과 데이터 접근 계층 간에는 의존성이 있다.
따라서 비즈니스 로직이 트랜잭션 스크립트 또는 액티브 레코드 패턴을 사용한다면 계층형 아키텍처 패턴이 적합하다.
[포트와 어댑터]
포트와 어댑터(port & adapter) 아키텍처는 계층형 아키텍처의 단점을 해결하고 좀 더 복잡한 비즈니스 로직을 구현하는데 적합하다.
의존성 역전 원칙
의존성 역전 원칙(DIP)에서 비즈니스 로직을 구현하는 상위 수준의 모듈은 하위 수준의 모듈에 의존해서는 안된다고 한다.
DIP를 전통적인 계층형 아키텍처에 적용하면 다음과 같이 된다.

이렇게 하면 비즈니스 로직 계층은 중심적인 역할을 하게 된다.
더이상 시스템의 인프라스트럭처 구성요소에 의지하지 않느다.

인프라 구성요소의 연동
포트와 어댑터 아키텍처의 핵심 목적은 인프라스트럭처 구성요소로부터 시스템의 비즈니스 로직을 분리하는 것이다.

인프라스트럭처 구성요소를 직접 참조하는 대신, 비즈니스 로직 계층은 인프라스트럭처 계층이 구현해야 할 ‘포트’를 정의한다.
인프라스트럭처 계층은 ‘어댑터’를 구현한다.
변형
포트와 어댑터 아키텍처는 핵사고날 아키텍처, 어니언 아키텍처, 클린 아키텍처로 알려져 있다.
이 모든 패턴이 비록 동일한 설계 원칙에 기반하지만 사용하는 용어가 조금씩 상이할 수 있다.
- 애플리케이션 계층 = 서비스 계층 = 유스케이스 계층
- 비즈니스 로직 계층 = 도메인 계층 = 핵심 계층
[CQRS]
CQRS 패턴은 여러 영속 모델에 시스템의 데이터를 표현할 수 있다.
구현
이름에서 알 수 있듯이 이 패턴은 시스템 모델의 책임을 분리시킨다.
여기에는 커맨드 실행 모델과 읽기 모델의 두 유형이 있다.
커맨드 실행 모델
CQRS에는 시스템의 상태를 수정하는 오퍼레이션(시스템 커맨드)을 전담으로 수행하는 단일 모델이 있다.
비즈니스 엔티티의 일관적 사애를 읽을 수 있어야 하고, 갱신할 때 낙관적 동시성을 지원해야 한다.
읽기 모델(프로젝션)
시스템은 사용자에게 데이터를 보여주거나 다른 시스템에 정보를 제공하기 위해 필요한 만큼 모델을 정의해야 한다.
읽기 모델(read model)은 캐시에서 언제든 다시 추출할 수 있는 프로젝션이다.
읽기 모델은 읽기 전용이다. 시스템의 어떠한 오퍼레이션도 읽기 모델의 데이터를 직접 수정할 수 없다.
읽기 모델의 프로젝션
읽기 모델이 작동하려면 시스템은 커맨드 실행 모델에서 변경을 모든 읽기 모델로 프로젝션해야 한다.

다음으로 프로젝션을 생성하는 두 가지 방식인 동기식과 비동기 방식을 살펴보자.
동기식 프로젝션
동기식 프로젝션은 격차 해소 구독 모델을 통해 OLTP 데이터의 변경사항을 가져온다.
- 프로젝션 엔진이 OLTP 데이터베이스로부터 마지막에 처리했던 체크포인트 이후에 추가되거나 갱신된 레코드를 조회한다.
- 프로젝션 엔진이 조회된 데이터를 이용하여 시스템의 읽기 모델을 재생성 또는 갱신한다.
- 프로젝션 엔진은 마지막으로 처리 레코드의 체크포인트를 저장한다. 이 값은 다음 처리 때 추가되거나 갱신된 레코드를 조회할 때 사용된다.


격차 해소 구독이 작동하려면 커맨드 실행 모델이 추가되거나 갱신되는 모든 데이터베이스 레코드를 체크포인트로 관리해야 한다.
비동기식 프로젝션
비동기식 프로젝션 시나리오에서 커맨드 실행 모델은 모든 커밋된 변경사항을 메시지 버스에 발행한다.

위 그림처럼 시스템의 프로젝션 엔진은 발행된 메시지를 구독하고 읽기 모델을 갱신하는데 사용한다.
도전과제
비동기식 프로젝션 방식의 확실한 확장성과 성능의 장점에도 불구하고, 분산 컴퓨팅에서 문제가 발생하기 쉽다.
메시지의 순서가 잘못되거나 중복 처리되면 읽기 모델의 일관성 없는 데이터가 프로젝션 된다.
또한 새로운 프로젝션을 추가하거나 이미 존재하는 것을 재생성하는 것이 어렵다.
그러므로 가능하면 동기식 프로젝션 방식을 구현하고, 선택적으로 비동기식 프로젝션 방식을 추가하는 것을 권장한다.
모델 분리
CQRS 기반 시스템에 대한 일반적인 오해는 커맨드는 데이터를 수정만 할 수 있고, 데이터를 오직 표현 용도로 읽기 모델을 통해서만 조회할 수 있다는 것이다.
하지만 이것은 잘못된 것이다.
커맨드는 실행이 성공했는지 실패했는지 항상 호출자에게 알려야 한다.
그러므로 대부분의 경우 커맨드는 데이터를 반환해야 한다.
CQRS를 사용해야 하는 경우
CQRS 패턴은 궁극적으로 다양한 종류의 데이터베이스에 저장된 동일한 데이터와 작동할 필요가 있는 애플리케이션에 유용하다.
또한 CQRS는 이벤트 소싱 도메인 모델에 적합하다.
[범위]
우리의 목적은 실제 필요성과 비즈니스 전략에 따라 설계 의사결정을 내리는 것이다.
그림 8-18 처럼 비즈니스 하위 도메인을 묶는 모듈의 논리적 경계를 분명하게 하고 각각에 맞는 적합한 도구를 사용하는 것이 중요하다.

[결론]
계층형 아키텍처는 기술적 관심사에 따라 코드베이스를 분해한다.
이 패턴은 비즈니스 로직과 데이터 접근 구현을 결합시키므로 액티브 레코드 기반 시스템에 적합하다.
포트와 어댑터 아키텍처는 관계를 역전시킨다.
비즈니스 로직을 중심으로 모든 인프라스트럭처의 의존성을 역전시킨다.
이 패턴은 도메인 모델 패턴을 구현하는 비즈니스 로직에 적합하다.
CQRS 패턴은 여러 모델에서 동일한 데이터를 표현한다.
이 패턴은 이벤트 소싱 도메인 모델에 기반한 시스템에 적합하지만, 다양한 영속 모델을 사용할 필요가 있는 어떤 시스템에도 사용할 수 있다.
Ch9. 커뮤니케이션 패턴
[모델 변환]
모델의 변환 로직은 스테이트리스 또는 스테이트풀이 될 수 있다.
상태를 보존하지 않는 스테이트리스 전환은 수신(OHS) 또는 발신(ACL) 요청이 발행할 때 즉석에서 발생하는 반면, 스테이트풀 변환은 상태 보존을 위해 데이터베이스를 사용하여 좀 더 복잡한 로직을 다룰 수 있다.
스테이트리스 모델 변환
스테이트리스 모델 변환은 프락시 패턴을 구현하여 수신과 발신 요청을 삽입하고 소스 모델을 바운디드 컨텍스트의 목표 모델에 매핑한다.

프락시 구현은 바운디드 컨텍스트가 동기식을 통신할지 또는 비동기식으로 통신할지에 따라 들다.
동기
동기식 통신에 사용되는 모델을 변환하는 일반적인 방법은 그림 9-2와 같이 바운디드 컨텍스트의 코드베이스에 변환 로직을 포함하는 것이다.

경우에 따라 변환 로직을 API 게이트웨이 패턴과 같은 외부 컴포넌트로 넘기는 것이 더 비용 효과적이고 편할 수 있다.
오픈 호스트 패턴을 구현하는 바운디드 컨텍스트의 경우 API 게이트웨이는 내부 모델을 통합에 최적화된 공표된 언어로 변환하는 역할을 한다.
또한 명시적 API 게이트웨이를 사용하면 API의 여러 버전을 관리하고 제공하는 프로세스를 도울 수 있다.

API 게이트웨이를 사용하여 구현된 충돌 방지 계층은 여러 다운스트림 바운디드 컨텍스트에서 사용할 수 있다.

이러한 바운디드 컨텍스트는 종종 교환 컨텍스트라고도 불린다.
비동기
비동기 통신에 사용하는 모델을 변환하기 위해서 메시지 프락시(message proxy)를 구현할 수 있다.
메시지 프락시는 소스 바운디드 컨텍스트에서 오는 메시지를 구독하는 중개 컴포넌트다.

메시지 모델을 변환하는 것 외에도 중개 컴포넌트는 관련 없는 메시지를 필터링할 수도 있다.
오픈 호스트 서비스를 구현할 때는 비동기식 모델 변환이 반드시 필요하다.
비동기 변환을 사용하면 도메인 이벤트를 가로채서 공표된 언어로 변환할 수 있으므로 바운디드 컨텍스트의 구현 상세를 더 잘 캡슐화할 수 있다.
또한 메시지를 공표된 언어로 변환하면 바운디드 컨텍스트의 내부 요구사항을 위한 프라이빗 이벤트와 퍼블릭 이벤트를 구분할 수 있다.

스테이트풀 모델 변환
더 중요한 모델 변환의 경우 스테이트풀 변환이 필요할 수 있다.
예를 들어, 원천 데이터를 집계하거나 여러 개의 요청에서 들어오는 데이터를 단일 모델로 통합해야 하는 변환 메커니즘의 경우다.
들어오는 데이터 집계하기
바운디드 컨텍스트가 들어오는 요청을 집계하고 성능 최적화를 위한 일괄 처리에 관심 있다고 가정해 보자.
이 경우 동기와 비동기 요청 모두에 대해 집계가 필요할 수 있다.(그림 9-7)

소스 데이터를 집계하는 또 다른 유스케이스는 그림 9-8처럼 여러 개의 세분화된 메시지를 단일 메시지로 결합하는 것이다.

유입되는 데이터를 집계하는 모델 변환은 좀 더 정교한 스테이트풀 처리가 필요하다.
그림 9-9처럼 변환 로직에 자체 영구 저장소가 필요하다.

일부 유스케이스에서는 상용 제품을 사용함으로써 스테이트풀 변환을 위한 맞춤 제작 솔루션을 구현하지 않는 경우도 있다.
예를 들어 스트림 처리 플랫폼(kafka, AWS Kinesis 등) 또는 일괄 처리 솔루션(NiFi, AWS Glue, Spark)을 사용할 수 있다.
여러 요청 통합
다른 바운디드 컨텍스트를 포함하여 여러 요청에서 집계된 데이터를 처리해야 할 수도 있다.
예를 들어 여러 다른 컨텍스트의 데이터를 처리하고, 이를 위한 복잡한 비즈니스 로직을 구현해야 하는 바운디드 컨텍스트다.

[애그리게이트 연동]
애그리게이트가 시스템의 나머지 부분과 통신하는 방법 중 하나는 도메인 이벤트를 발행하는 것이다.
그렇다면 어떻게 도메인 이벤트가 메시지 버스에 발행될까?
아웃박스
아웃박스 패턴(그림 9-11)은 다음 알고리즘을 사용하여 도메인 이벤트의 안정적인 발행을 보장한다.
- 업데이트된 애그리게이트의 상태와 새 도메인 이벤트는 모두 동일한 원자성 트랜잭션으로 커밋한다.
- 메시지 릴레이는 데이터베이스에서 새로 커밋된 도메인 이벤트를 가져온다.
- 릴레이는 도메인 이벤트를 메시지 버스에 발행한다.
- 성공적으로 발행되면 릴레이는 이벤트를 데이터베이스에서 발행한 것으로 표시하거나 완전히 삭제한다.

관계형 데이터베이스를 사용할 때 그림 9-12와 같이 두 개의 테이블에 원자적으로 커밋하고, 메시지를 저장하기 위한 전용 테이블을 사용하는 데이터베이스 기능을 활용하는 것이 좋다.

만약 다중 문서 트랜잭션을 지원하지 않는 NoSQL 를 사용한다면 (메시지 버스로) 전달될 도메인 이벤트는 애그리게이트 레코드에 포함되어야 한다.
{
"campaign-id": "3414e3r4-asdf134-aedr",
"state": {
"name": "Autumn 2017",
"publishing-state": "DEACTIVATED",
"ad-locations": [
...
]
...
},
"outbox": [
{
"campaign-id": "3414e3r4-asdf134-aedr",
"type": "campaign-deactivated",
"reason": "Goals met",
"published": false
}
]
}
발행되지 않은 이벤트 가져오기
발행 릴레이는 풀(pull) 기반 또는 푸시(push) 기반으로 새 도메인 이벤트를 가져올 수 있다.
풀:발행자 폴링
- 릴레이는 발행되지 않은 이벤트에 대해 데이터베이스를 지속해서 질의할 수 있다.
- 지속적인 풀링으로 인한 부하를 최소화하려면 인덱스가 필요하다.
푸시:트랜잭션 로그 추적
- 데이터베이스의 기능을 활용하여 새 이벤트가 추가될 때마다 발행 릴레이를 호출할 수 있다.
- 일부 관계형 데이터베이스는 트랜잭션 로그를 추적하여 업데이트/삽입에 대한 알림을 받을 수 있다.
- 일부 NoSQL 데이터베이스는 커밋된 변경사항을 이벤트 스트림으로 노출하기도 한다.
아웃박스 패턴은 적어도 한번은 메시지 배달을 보장한다는 점에 유의하자.
사가(Saga)
핵심 애그리게이트 설계 원칙 중 하나는 각 트랜잭션을 애그리게이트의 단일 인스턴스로 제한하는 것이다.
그러나 여러 애그리게이트에 걸쳐 있는 비즈니스 프로세스를 구현해야 하는 경우가 있다.
예를 들어 광고 캠페인이 활성화되면 캠페인의 광고 자료를 퍼블리셔에게 자동으로 제출해야 한다고 해보자.
이 흐름은 광고 캠페인과 퍼블리셔라는 두 가지 비즈니스 엔티티에 걸쳐 있다.
이들은 책임도 다르고 다른 바운디드 컨텍스트에 속한다.
이럴 경우 사가로 구현할 수 있다.
사가는 오래 지속되는 비즈니스 프로세스다.
여기서 중요한 건 시간 측면이 아니라 트랜잭션 측면에서 보는 것이다.
즉, 여러 트랜잭션에 걸쳐 있는 비즈니스 프로세스를 말한다.
사가는 실행 단계 중 하나가 실패하면 시스템 상태를 일관되게 유지되도록 적절할 보상 조치를 한다.
그림 9-13를 통해 사가의 흐름을 살펴보자.

발행 프로세스를 구현하기 위해서 사가는 Campaign 애그리게이트로부터 CampaignActivated 이벤트를, AdPublishing 바운디드 컨텍스트로부터 PublishingConfirmed와 PublishingRejected 이벤트를 기다린다.
일관성
사가 패턴이 다중 컴포넌트의 트랜잭션을 조율하기는 하지만 관련된 컴포넌트의 상태는 궁극적으로 일관성을 갖는다.
[결론]
이번 장에서는 시스템 컴포넌트를 연동하기 위한 다양한 패턴을 배웠다.
충돌 방지 계층 또는 오픈 호스트 서비스를 구현하는데 사용할 수 있는 모델 변환 패턴부터 알아보았다.
- 스테이트리스 모델 변환
- 스테이트풀 모델 변환
아웃박스 패턴은 애그리게이트의 도메인 이벤트를 발행하는 안정적인 방법이다.
다른 프로세스 실패에 직면해도 도메인 이벤트를 항상 발행한다.
사가 패턴은 간단한 교차 컴포넌트 비즈니스 프로세스를 구현하는데 사용할 수 있다.
도메인 이벤트에 대한 비동기식 반응과 커맨드 실행에 의존한다.
'Book Notes' 카테고리의 다른 글
| [개발서적] 도메인 주도 설계 첫걸음 3부(ch12~13) 요약 (2) (0) | 2026.01.11 |
|---|---|
| [개발서적] 도메인 주도 설계 첫걸음 3부(ch10~11) 요약 (1) (0) | 2026.01.11 |
| [개발서적] 도메인 주도 설계 첫걸음 2부(ch5~6) 요약 (1) (0) | 2026.01.11 |
| [개발서적] 도메인 주도 설계 첫걸음 1부(ch3~4) 요약 (2) (0) | 2026.01.10 |
| [개발서적] 도메인 주도 설계 첫걸음 1부(ch1~2) 요약 (1) (0) | 2026.01.10 |