- 이번 장에서는 단일 컴포넌트의 경계를 넘어 시스템 요소 전반의 커뮤니케이션 흐름을 구성하는 패턴을 알아본다.
- 바운디드 컨텍스트 간 커뮤니케이션을 용이하게 하고,
- 애그리게이트 설계 원칙에 의해 부과된 제한 사항을 해결하며,
- 여러 시스템 컴포넌트에 걸쳐 비즈니스 프로세스를 조율하는 방법을 알아본다.
모델 변환
- 바운디드 컨텍스트는 유비쿼터스 언어 모델의 경계이다.
- 서로 다른 바운디드 컨텍스트 사이에 커뮤니케이션 하기 위한 다양한 설계 패턴이 있음.
- 각기 다른 바운디드 컨텍스트를 구현하는 두 팀이 효과적으로 의사소통하고 협력할 의향이 있을 경우
- 파트너십을 통해 통합할 수 있음. 프로토콜은 임시방편식으로 조정될 수 있고 모든 통합 문제는 사실상 팀 간의 커뮤니케이션을 통해 해결할 수 있다.
- 공유 커널은 다른 협력 기반 통합 방식. 팀은 모델의 제한된 부분을 분리해서 공동으로 함께 발전시킴.
- 예) 바운디드 컨텍스트의 연동 컨트랙트를 공동 소유의 저장소로 분리할 수 있음.
- 다운스트림 바운디드 컨택스트가 업스트림 바운디드 컨텍스트 모델을 따를 수 없는 경우. (모델 변환 로직 적용)
- 다운스트림이 맞춰줄 때 : ACL(충돌 방지 계층)을 사용하여 업스트림 바운디드 컨텍스트의 모델을 필요에 맞게 조정할 수 있음.
- 업스트림이 맞춰줄 때 : OHS(오픈 호스트 서비스) 역할을 하고 연동 관련 공표된 언어(public)를 사용하여 구현 모델에 대한 변경으로부터 사용자를 보호할 수 있음.
- 모델 변환 로직은 스테이트리스 변환(Stateless translation) 혹은 스테이트풀 변환(Stateful translation)이 있음.
- 스테이트 리스 변환 : 수신(OHS) 혹은 발신(ACL) 요청이 발행할 때 즉석에서 발생.
- 스테이트 풀 변환 : 상태 보존을 위해 데이터베이스를 사용하여 좀 더 복잡한 변환 로직을 다룰 수 있음.
- 각기 다른 바운디드 컨텍스트를 구현하는 두 팀이 효과적으로 의사소통하고 협력할 의향이 있을 경우
스테이트리스 모델 변환
- 스테이트리스 모델 변환을 소유하는 바운디드 컨텍스트(ACL, OHS)는 프락시 디자인 패턴을 구현하여 수신과 발신 요청을 삽입하고 소스 모델을 바운디드 컨텍스트의 목표 모델에 매핑함.
- 프락시는 동기식으로 통신하는지 또는 비동기식으로 통신하는지에 따라 다름.
동기
- 동기식 모델 변환의 일반적인 방법은 바운디드 컨텍스트의 코드베이스에 변환 로직을 포함하는 것.
- OHS에서는 유입되는 요청을 처리할 때, ACL에서는 업스트림 바운디드 컨텍스트를 호출할 때 발생.
- 경우에 따라 변환 로직을 API 게이트웨이 패턴과 같은 외부 컴포넌트로 넘기는 것이 더 비용 효과적이고 편할 수 있음.
- OHS 패턴을 구현하는 바운디드 컨텍스트의 경우 API 게이트웨이는 내부 모델을 통합에 최적화된 공표된 언어로 변환하는 역할을 한다.
- 명시적 API 게이트웨이를 사용하면 아래 그림과 같이 바운디드 컨텍스트에 API의 여러 버전을 관리하고 제공하는 프로세스를 도울 수 있음.
- API 게이트웨이를 사용하여 구현된 ACL은 여러 다운스트림 바운디드 컨텍스트에서 사용할 수 있음.
- 이 경우 ACL은 아래와 같이 통합 관련 바운디드 컨텍스트 역할을 한다.
- 이러한 바운디드 컨텍스트는 주로 다른 컴포넌트에서 좀 더 편리하게 사용할 수 있게 모델을 변환하는 역할을 하며, 종종 교환 컨텍스트(interchange context)라고도 부름.
비동기
- 비동기 통신에 사용하는 모델을 변환하기 위해 메시지 프락시(message proxy)를 구현할 수 있음.
- 메시지 프락시는 소스 바운디드 컨텍스트에서 오는 메시지를 구독하는 중개 컴포넌트이다.
- 프락시는 필요한 모델 변환을 적용하고 결과 메시지를 대상 구독자에게 전달함.
- 메시지 모델을 변환하는 것 외에도 중개 컴포넌트는 관련 없는 메시지를 필터링하여 목표 바운디드 컨텍스트의 노이즈를 줄일 수 있다.
- OHS를 구현할 때 비동기식 모델 변환은 반드시 필요.
- 도메인 이벤트를 가로채서 공표된 언어로 변환할 수 있으므로 바운디드 컨텍스트의 구현상세를 더 잘 캡슐화 할 수 있음.
- 메시지를 공표된 언어로 변환하면 바운디드 컨텍스트의 내부 요구사항을 위한 프라이빗 이벤트와 다른 바운디드 컨텍스트와 연동하기 위해 설계된 퍼블릭 이벤트를 구분할 수 있음.
스테이트풀 모델 변환
- 원천 데이터를 집계하거나 여러 개의 요청에서 들어오는 데이터를 단일 모델로 통합해야 하는 변환 메커니즘의 경우 사용.
들어오는 데이터 집계하기
- 바운디드 컨텍스트가 들어오는 요청을 집계하고 성능 최적화를 위한 일괄처리가 필요할 경우.
- 이 경우, 아래와 같이 비동기 요청 모두에 대해 집계가 필요할 수 있음.
- 아래는 여러 개의 세분화된 메시지를 단일 메시지로 결합하는 케이스.
- 유입되는 데이터를 집계하는 모델 변환의 경우, 들어오는 데이터를 추적하고 그에 따라 처리하려면 변환 로직에 자체 영구 저장소가 필요.
- 일부는 스테이트풀 변환을 위한 상용제품을 사용함, 예로 스트림 처리 플랫폼(Kafka, AWS Kinesis), 또는 일괄 처리 솔루션 (Apache NiFi, AWS Glue, Spark 등)
여러 요청 통합
- 다른 바운디드 컨텍스트를 포함하여, 여러 요청에서 집계된 데이터를 처리해야할 경우가 있음.
- 예) 사용자 인터페이스가 여러 서비스에서 발생하는 데이터를 결합해야 하는 프런트 엔드를 위한 백엔드 패턴(backend-for-frontend pattern)
애그리게이트 연동
- 이벤트 발행 프로세스에서 일어날 법한 몇 가지 일반적인 실수와 각 접근 방식의 결과를 먼저 살펴보자.
public class Campaign
{
...
List<DomainEvent> _events;
IMessageBus _messageBus;
...
public void Deactivate(string reason)
{
for (l in _locations.Values())
{
l.Deactivate();
}
IsActive = false;
//새 이벤트가 인스턴스화 됨.
var newEvent = new CampaignDeactivated(_id, reason);
//내부 목록에 새 이벤트가 추가됨.
_events.Append(newEvent);
//메시지 버스로 이벤트 발행.
_messageBus.Publish(newEvent);
}
}
- 애그리게이트에서 바로 도메인 이벤트를 발행하는 위의 구현은 잘못되었다.
- 애그리게이트의 새 상태가 DB에 커밋되기 전에 이벤트가 전달된다.
- 만약 DB의 기술적인 문제로 트랜잭션이 커밋되지 않으면 DB트랜잭션은 롤백되었지만 이벤트는 발행되어버리고, 이벤트를 철회할 수 있는 방법이 없을 것.
public class ManagementAPI
{
...
private readonly IMessageBus _messageBus;
private readonly ICampaignRepository _repository;
...
public ExecutionResult DeactivateCampaign(CampaignId id, string reason)
{
try
{
//1.campaign 애그리게이트를 로드
//2.비활성화 command실행.
//3.갱신상태 커밋.
var campaign = repository.Load(id);
campaign.Deactivate(reason);
_repository.CommitChanges(campaign);
//4.새 도메인 이벤트를 발행하기 시작
var events = campaign.GetUnpublishedEvents();
for(IDomainEvent e in events)
{
_messageBus.publish(e);
}
campaign.ClearUnpublishedEvents();
}
catch(Exception ex)
{
...
}
}
}
- 위 예제에서는 새 도메인 이벤트를 발행할 책임을 애플리케이션 계층으로 이전함.
- 위코드는 애그리게이트 상태를 업데이트 한 후, 도메인 이벤트를 발행.
- 이 예제 또한 신뢰할 수 없다.
- 서버가 DB 트랜잭션을 커밋한 직후 이벤트 발행에 실패하면 시스템은 일관성 없는 상태로 종료
- 즉 DB 트랜잭션은 커밋되지만 도메인 이벤트는 발행되지 않는다.
- 이러한 문제들은 아웃박스 패턴을 사용하여 해결할 수 있음.
아웃박스
아웃박스 패턴은 다음 알고리즘을 사용하여 도메인 이벤트의 안정적인 발행을 보장한다.
- 업데이트된 애그리게이트의 상태와 새 도메인 이벤트는 모두 동일한 원자성 트랜잭션으로 커밋된다.
- 메시지 릴레이는 데이터베이스에서 새로 커밋된 도메인 이벤트를 가져온다.
- 릴레이는 도메인 이벤트를 메시지 버스에 발행.
- 성공적으로 발행되면 릴레이는 이벤트를 데이터베이스에 발행한 것으로 표시하거나 완전히 삭제.
- 관계형DB를 사용할 때는 애그리게이트 상태를 저장하는 테이블과 메시지 저장을 위한 전용 테이블을 각각 두는 것이 좋다.
발행되지 않은 이벤트 가져오기
발행 릴레이는 풀(Pull) 기반 또는 푸시(Push) 기반 방식으로 새 도메인 이벤트를 가져올 수 있다.
- 풀: 발행자 폴링.
- 릴레이는 발행되지 않은 이벤트에 대해 데이터베이스를 지속해서 질의.
- 지속적인 폴링으로 인한 DB부하를 줄이기 위해서는 적절한 인덱스 필요.
- 푸시: 트랜잭션 로그 추적
- DB의 기능을 활용하여 새 이벤트가 추가될 때마다 발행 릴레이를 호출.
- AWS DynamoDB(NoSQL) Streams를 활용하면, DB의 커밋된 변경사항을 이벤트 스트림으로 노출할 수 있음.
아웃박스 패턴은 적어도 한 번은 메시지 배달을 보장한다는 점에 유의.
- 데이터베이스에 발행한 것으로 표시하기 전에 릴레이가 실패할 경우, 다음 이터레이션에서 같은 메시지가 발행됨.
사가
- 핵심 애그리게이트 설계 원칙 중 하나는 각 트랜잭션을 애그리게이트의 단일 인스턴스로 제한하는 것.
- 그러나 여러 애그리게이트에 걸쳐 있는 비즈니스 프로세스를 구현해야하는 경우가 있음.
- 이러한 케이스는 사가(saga)를 통해 구현될 수 있음.
- 사가는 관련 컴포넌트에서 발생하는 이벤트를 수신하고 다른 컴포넌트에 후속 커맨드를 실행한다.
- 실행 단계 중 하나가 실패하면 사가는 그 시스템 상태를 일관되게 유지하도록 적절한 보상 조치를 내리는 일을 담당.
- 사가는 Campaign 애그리게이트로부터 CampaignActivated 이벤트를, AdPublishing 바운디드 컨텍스트로부터 PublishingConfirmed와 PublishingRejected 이벤트를 기다린다.
- 사가는 AdPublishing에서 SubmitAdvertisement 커맨드를 실행하고 Campaign 애그리게이트에서 TrackPublishingConfirmation과 TrackPublishingRejection 커맨드를 실행해야 한다.
- TrackPublishingRejection 커맨드는 광고 캠페인이 활성 상태가 되지 않도록 하는 보상조치를 실행하는 역할을 함.
- 사가는 이벤트 소싱 애그리게이트로 구현되어 수신된 이벤트와 실행된 커맨드의 전체 기록을 유지할 수 있음.
- 그러나 커맨드 로직은 도메인 이벤트가 아웃박스 패턴으로 전달하는 방식과 유사하게 사가 패턴 자체에서 벗어나 비동기적으로 실행.
일관성
- 사가 패턴이 다중 컴포넌트의 트랜잭션을 조율하지만 관련된 컴포넌트의 상태는 궁극적 일관성을 갖는다.
(즉, 원자적 트랜잭션이 아님)
애그리게이트 경계 내의 데이터만 강한 일관성을 가진다.
외부의 모든 것은 궁극적으로 일관성을 갖는다.
프로세스 관리자
- 사가 패턴은 단순하고 선형적인 흐름을 관리.
- 사가는 이벤트를 해당 커맨드와 일치시킨다.
- CampaignActivated 이벤트와 PublishingService.SubmitAdvertisement 커맨드
- PublishingConfirmed 이벤트와 Campaign.TrackConfirmation 커맨드
- PublishingRejected 이벤트와 Campaign.TrackRejection 커맨드
- 프로세스 관리자 패턴은 비즈니스 로직 기반 '프로세스'를 구현하기 위한 것이다.
- 프로세스 관리자는 시퀀스의 상태를 유지하고 다음 처리 단계를 결정하는 중앙 처리 장치로 정의한다.
- 경험상 올바른 동작과정을 선택하는 if-else문이 포함되어있다면 그것은 프로세스 관리자 일 것.
- 프로세스 관리자는 사가와는 달리 '여러 단계'로 구성된 응집된 비즈니스 프로세스이다.
- 예시를 통해 알아본다.
- 출장 예약 시작 > 효과적인 비행 경로 선택 > 직원에게 승인 요청 (> 지원이 다른 경로를 선호하는 경우, 직속관리자가 승인.) > 항공편을 예약 > 사전 승인된 호텔 중 하나를 적절한 날짜에 예약 (> 이용 가능한 호텔이 없다면 항공권 취소)
public class BookingProcessManager
{
private readonly IList<IDomainEvent> _events;
private BookingId _id;
private Destination _destination;
private TripDefinition _parameters;
private EmployeeId _traveler;
private Route _route;
private IList<Route> _rejectedRoutes;
private IRoutingService _routing; ...
public void Initialize(Destination destination,
TripDefinition parameters,
EmployeeId travler)
{
_destination = destination;
_parameters = parameters;
_traveler = traveler;
_route = _routing.Calculate(destination, parameters);
var routeGenerated = new RouteGeneratedEvent(
BookingId: _id, Route: _route);
//선택된 비행경로로 직원에게 승인 요청.
var commandIssuedEvent = new CommandIssuedEvent(
command: new RequestEmployeeApproval(_traveler, _route)
);
_events.Append(routeGenerated);
_events.Append(commandIssuedEvent);
}
public void Process(RouteConfirmed confirmed)
{
var commandIssuedEvent = new CommandIssuedEvent(
command: new BookFlights(_route, _parameters)
);
_events.Append(confirmed);
_events.Append(commandIssuedEvent);
}
public void Process(RouteRejected rejected)
{
//직원이 다른 경로를 선호하는 경우.
var commandIssuedEvent = new CommandIssuedEvent(
command: new RequestRerouting(_traveler, _route)
);
_events.Append(rejected);
_events.Append(commandIssuedEvent);
}
public void Process(ReroutingConfirmed confirmed)
{
//다른 루트를 생성하여 재 승인 요청.
_rejectedRoutes.Append(route);
_route = _routing.CalculateAltRoute(destination,
parameters, rejectedRoutes);
var routedGenerated = new RouteGeneratedEvent(
BookingId: _id,
Route: _route);
var commandIssuedEvent = new CommandIssuedEvent(
command: new RequestEmployeeApproval(_traveler, _route)
);
_events.Append(confirmed);
_events.Append(routeGenerated);
_events.Append(commandIssuedEvent);
}
public void Process(FlightBooked booked)
{
//항공편 예약이 된 후, 호텔을 예약하도록 이벤트 발행.
var commandIssuedEvent = new CommandIssuedEvent(
command: new BookHotel(_destination, _parameters)
);
_events.Append(booked);
_events.Append(commandIssuedEvent);
}
...
}
'Design > DDD' 카테고리의 다른 글
[DDD 첫걸음] 2-4. 전술적 설계 - 아키텍처 패턴 (0) | 2023.07.14 |
---|---|
[DDD 첫걸음] 2-3. 전술적 설계 - 시간 차원의 모델링. (0) | 2023.06.19 |
[DDD 첫걸음] 2-2. 전술적 설계 - 복잡한 비즈니스 로직 다루기. (0) | 2023.06.11 |
[DDD 첫걸음] 2-1. 전술적 설계 - 간단한 비즈니스 로직 구현. (0) | 2023.04.25 |
[DDD 첫걸음] 1-4. 전략적 설계 - 바운디드 컨텍스트 연동. (0) | 2023.04.16 |