[Domain Driven Design] 커뮤니케이션 패턴

이홍준·2023년 7월 7일

DDD

목록 보기
9/10

커뮤니케이션 패턴

단일 컴포넌트 경계를 넘어 시스템 요소 전반의 커뮤니케이션 흐름을 구성하는 패턴에 대해 알아 보고자 한다. 이번 페이지에서 소개할 패턴은 바운디드 컨텍스트 간 커뮤니케이션을 용이하게 하고, Aggregate 설계 원칙에 의해 부과된 제한 사항을 해결하고, 여러 시스템 컴포넌트에 걸쳐 비즈니스 프로세스를 조율한다.

1. 모델 변환

  1. Stateless 모델 변환

    • 해당 변환을 소유하는 바운디드 컨텍스트는 프록시 패턴을 구현하여 수신과 발신 요청을 삽입하고 소스 모델을 바운디드 컨텍스트의 목표 모델에 매핑한다.
      • request 모델→ proxy → 목표 모델
    • proxy 구현은 바운디드 컨텍스트가 동기식으로 통신하는지 또는 비동기식으로 통신하는 지에 따라 다르다.
    • 동기 통신
      • 일반적인 방법: 바운디드 컨텍스트의 코드베이스에 변환 로직을 포함
      • 다른 방법: 변환 로직을 API 게이트웨이 패턴과 같은 외부 컴포턴트로 넘기는 것이 비용적으로 더 효과적이고 편할 수 있다. (AWS API Gateway, Google Apigee, Azure API Management)
      • API 게이트웨이를 사용하여 구현된 충돌 방지 계층은 여러 다운스트림 컨텍스트에서 사용될 수 있다. 이러한 바운디드 컨텍스트는 주로 다른 컴포넌트에서 편하게 사용할 수 있게 모델을 변환하는 역할을 하며, 종종 교환 컨텍스트(interchange context)라고도 부른다.
    • 비동기 통신
      • 비동기 통신에 사용하는 모델을 변환하기 위해 Message proxy를 구현할 수 있다.
      • message proxy
        • 소스 바운디드 컨텍스트에서 오는 메시지를 구독하는 중개 컴포넌트
        • 필요한 모델 변환을 적용하고 결과 메시지를 대상 구독자에게 전달한다.
        • 메시지 모델을 변환하는 것 외에도 관련 없는 메시지를 필터링 하여 목표 바운디드 컨텍스트의 노이즈를 줄일수 있다.
      • 오픈 호스트 서비스를 구현할 때 비동기식 모델 변환은 반드시 필요하다. → 도메인 이벤트를 가로채서 공표된 언어로 변환함으로써 바운디드 컨텍스트의 구현 상세를 캡슐화할 수 있다.
      • 바운디드 컨텍스트의 내부 요구사항을 위한 private event와 다른 바운디드와 연동하기 위한 public event를 구분할 수 있다.
  2. Stateful 변환

    원천 데이터를 집계하거나 여러 개의 요청에서 들어오는 데이터를 단일 모델로 통합해야 하는 변환 매커니즘의 경우와 같이 중요한 모델 변환은 stateful 변환이 필요할 수 있다.

    • 유입되는 데이터를 집계하는 모델변환은 API 게이트웨이를 사용하여 구현할 수 없으므로 좀 더 정교한 stateful 처리가 필요하다. 들어오는 데이터를 추적하고 그에 따라 처리하려면 변호나 로직 자체 영구 저장소가 필요하다.
    • 일부 유스케이스에서는 상용 제품을 사용함으로써 stateful 변환을 위한 맞춤 제작 솔루션을 구현하지 않는 경우도 있다. → 스트림 처리 플랫폼(Kafaka, AWS kinesis 등) 또는 일괄처리 솔루션(Apache Nifi, AWS Glue, Spark 등)을 사용할 수 있다.

2. Aggregate 연동

Aggregate가 시스템의 나머지 부분과 통신하는 방법중 하나는 도메인 이벤트를 발행하는 것이다. 외부 컴포넌트는 이러한 도메인 이벤트를 구독하고 해당 로직을 실행할 수 있다. 도메인 이벤트가 메시지 버스에 어떻게 발행되어야 하는지 알아보자

  1. 기존의 연동 방식

    public class Campain{
    ...
    List<DomainEvent> _events;
    IMessageBus _messageBus;
    ...
    
    	public void Deactivate(String reason){
    		for(l in _ locations.Values()){
    			l.Deactivate();
    		}
    	ISACtivate = false;
    		var newEvent = new CampaignDeactivated(_id, reason);
    		_events.Append(newEvent);
    		_messageBus.Publish(newEvent);
    	}
    }
    • Aggregate에서 바로 도메인 이벤트를 발행하는 것은 두가지 문제를 야기한다.
      • Aggregate의 새 상태가 DB에 커밋되기 전에 이벤트가 전달된다. 구독자는 비활성화되었다는 알림을 받을 수 있지만 실제 캠페인 상태와 모순된다.
      • 경합 조건, 후속 Aggregate 로직으로 인해 작업이 무효화되거나 단순히 DB의 기술적인 문제로 인해 DB 트랜잭션이 커밋되면 롤백되더라도 이벤트는 이미 발행되어 구독자에게 전달되어 철회할 방법이 없어진다.
  2. 새 도메인 이벤트를 발행할 책임을 애플리케이션 계층으로 이전한 방법

    public class ManagementAPI{
    ...
    private readonly IMessageBus _messageBus;
    private readonly ICampaignRepository _repository;
    ...
    
    	public ExecutionResult DeactivateCampaign(CapaignId id, String reason){
    		try{
    			var campaign = repository.Load(id);
    			campaign.Deactivate(reason);
    			_repository.CommitChanges(campaign);
    			
    			var events = campaign.GetUnpublishedEvents();
    			for (IDomainEvent e in events){
    				_messageBus.publish(e);
    			}
    			campaign.ClearUnpublishedEvents();
    		}catch(Exception e){
    			...
    		}
    	}
    }
    • 어떤 이유로 로직을 실행할 프로세스가 도메인 이벤트를 발행하지 못할 수 있다. 혹은 메시지 버스가 다운되었을 수도 있다. 또는 실행하는 서버가 DB 트랜잭션을 커밋한 직후 이벤트를 발행하기 전에 실패하면 시스템은 여전히 일관성 없는 상태로 종료된다.
    • DB 트랜잭션은 커밋되지만 도메인 이벤트는 발행되지 않는다. → 아웃박스 패턴으로 해결
  3. Outbox 패턴

    • 알고리즘 과정
      • 업데이트 된 Aggregate 상태와 새 도메인 이벤트는 모두 동일한 원자성 트랜잭션으로 커밋된다.
      • 메시지 릴레이는 DB에서 새로 커밋된 도메인 이벤트를 가져온다.
      • 릴레이는 도메인 이벤트를 메시지 버스에 발행한다.
      • 성공적으로 발행되면 릴레이는 이벤트를 DB에서 발행한 것으로 표시하거나 완전히 삭제한다.
    • RDB를 사용할 때 두 개의 테이블에 원자적으로 커밋하고 메시지를 저장하기 위한 전용 테이블을 사용하는 DB의 기능을 활용하는 것이 좋다고 한다.
    • 다중 문서 트랜잭션을 지원하지 않는 NoSQL를 사용할 때 메시지 버스로 전달될 도메인 이벤트는 Aggregate 레코드에 포함되어야 한다.
    // Aggregate 레코드
    {
    	"campaign-id": "364b33cdasd-dsadasdsa-dasdas"
    	"state": {
    		"name": "Axca 2022",
    		"publishing-state": "DEACTIVATED",
    		"ad-locations": [
    			...
    		]
    		...
    	},
    	// 추가
    	"outbox": [
    		{
    			"campaign-id": "364b33cdasd-dsadasdsa-dasdas",
    			"type": "campaign-deactivated",
    			"reason": "Goals met",
    			"published": false
    		}
    	]
    
    }
    • 발행되지 않은 이벤트를 가져오는 방법
      • pull : 발행자 폴링
        • 릴레이는 발행되지 않은 이벤트에 대해 DB를 지속해서 질의할 수 있다. 지속적인 풀링으로 인한 DB 부하를 최소화려면 적절한 인덱스가 필요하다.
      • push: 트랜잭션 로그 추적
        • DB의 기능을 활용하여 새 이벤트가 추가될 때마다 발행 릴레이를 호출할 수 있다. 예를 들어, 일부 RDB는 트랜잭션 로그를 추적하여 update/insert 된 레코드에 대한 알림을 받을 수 있다.
        • 일부 NoSQL DB는 커밋된 변경사항을 이벤트 스트림으로 노출하기도 한다. (AWS DynamoDB Streams)
      • 아웃박스 패턴은 적어도 한 번은 메시지 배달을 보장한다는 점에 유의해야 한다.
      • 메시지 발행을 한 후 릴레이가 실패했지만 DB에서 발행한 것으로 표시하기 전에 릴레이가 실패하면 다음 이터레이션에서 같은 메시지가 다시 발행된다.
  4. Saga 패턴

    긴 지속성을 가진 트랜잭션을 처리하는 방법이다. 여러 서비스나 DB간에 트랜잭션을 조율하는 방법을 제공한다.

    • 큰 트랜잭션을 여러 작은 트랜잭션으로 나누는 것이다. 하나당 하나의 로컬 트랜잭션을 처리하며, 전체 트랜잭션은 모든 로컬 트랜잭션이 성공적으로 완료될 때만 완료된다.
    • 시스템 상태를 일관되게 유지하도록 적절한 보상 조치를 내리는 일을 담당한다. → 보상 트랜잭션
    • 분산 트랜잭션을 조율하고 일관성을 유지하는 데 도움이 된다. 하지만 복잡성을 증가시키며, 특히 오류 상황에서 보상 트랜잭션을 올바르게 설계하고 실행하는 것은 매우 어려울 수 있다.

3. 결론

시스템 컴포넌트를 연동하기 위해 다양한 패턴이 있다. 충돌 방지 계층(ACL) 또는 오픈 호스트 서비스를 구현하는 데 사용할 수 있는 모델 변환 패턴을 살펴보는 것으로 시작했다. 즉석으로 변환될 수 있거나 상태 추적이 필요한 경우 좀 더 복잡한 로직을 구현해야 한다.

  • Outbox 패턴: Aggregate의 도메인 이벤트를 발행하는 안정적인 방법이다. 다른 프로세스 실패에 직면해도 도메인 이벤트를 항상 발행한다.
  • Saga 패턴 : 간단한 교차 컴포넌트 비즈니스 프로세스를 구현하는 데 사용할 수 있다. 프로세스 관리자 패턴을 사용하여 좀 더 복잡한 비즈니스 프로세스를 구현할 수 있다.

두 패턴 모두 도메인 이벤트에 대한 비동기식 반응과 커맨드 실행에 의존한다.

profile
I'm not only a web developer.

0개의 댓글