본문 바로가기

BackEnd

도메인 모델을 함수형 아키텍처로 - 컨텍스트 간 통신 2

반응형

https://fsharpforfunandprofit.com/books/

 

Books | F# for fun and profit

This book starts with a discussion of Domain Driven Design, and then shows to how to model a design using types. The last part shows how to implement the design using functional programming with F# (composition of functions, “railway-oriented programming

fsharpforfunandprofit.com

바운디드 컨텍스트 간 계약

이벤트 및 관련 DTO는 바운디드 컨텍스트 간의 일종의 계약을 형성합니다.

성공적인 의사 소통을 위해서는 두 컨텍스트가 공통 형식에 동의해야 합니다.

컨텍스트 사이에는 다양한 관계가 있으며 DDD 커뮤니티는 일반적인 용어에 대한 몇 가지 용어를 개발했습니다.

 

  • Shared Kernel :
    • 두 도메인이 공통적인 비즈니스 개념을 사용하는 경우입니다.
    • 예를 들어 주문 접수 컨텍스트와 배송 컨텍스트가 동일한 주소 양식을 사용하기로 했다면, 둘은 동일한 유효성 검사 로직을 사용해야 합니다.
    • 이 관계에서 이벤트 또는 DTO의 정의를 변경하는 것은 영향을 받는 다른 컨텍스트의 소유자와 협의한 경우에만 수행되어야 합니다. 해당 개념 외에는 독립적으로 진화할 수 있습니다.
  • A Customer/Supplier or Consumer Driven Contract relationship : 
    • 다운스트림 컨텍스트는 업스트림 컨텍스트가 제공하기를 원하는 계약을 정의합니다.
    • 청구 컨텍스트는 계약을 정의할 수 있으며("이것이 고객에게 청구하기 위해 필요한 것입니다") 주문 접수 컨텍스트는 해당 정보만 제공하고 추가 정보는 제공하지 않습니다.
    • 해당 계념 외에는 독립적으로 진화할 수 있습니다.
  • A Conformist relationship
    • 고객 주도 계약 관계의 역입니다.
    • 다운스트림은 업스트림에서 주는대로 받아야 합니다. 흔한 API 컨슈머 관계입니다.
    • 주문 접수 컨텍스트는 외부의 제품 카탈로그에 정의된 계약을 수락하고 코드를 그대로 사용하도록 조정할 수 있습니다.

 

Anti-Corruption Layers (부패 방지 계층)

종종 외부 시스템과 통신할 때 사용 가능한 인터페이스가 도메인 모델과 전혀 일치하지 않습니다.

이 경우 상호 작용 및 데이터는 경계 컨텍스트 내에서 사용하기에 더 적합한 것으로 변환되어야 합니다.

컨텍스트 간의 이러한 추가 분리 수준을 DDD 용어로 Anti-Corruption Layer라고 하며 종종 "ACL"로 축약됩니다.

Anti-Corruption Layer는 주로 유효성 검사를 수행하거나 데이터 손상을 방지하는 것이 아니라

두 가지 다른 언어(업스트림 컨텍스트에서 사용되는 언어) 간의 번역기 역할을 합니다.

ex) 주문 접수 컨텍스트의 고객 = 주문인 => 배송 컨텍스트의 고객 = 수령인

위의 다이어그램에서 "입력 게이트"는 종종 ACL의 역할을 합니다. 이는 내부의 순수한 도메인 모델이 외부 세계에 대한 지식으로 "손상"되는 것을 방지합니다. 입력 게이트는 DTO로 deSelialize 하는 역할도 수행합니다.

컨텍스트 맵 간 관계

컨텍스트 맵이 더 이상 컨텍스트 간의 기술적인 관계만 보여주는 것이 아니라
컨텍스트를 소유한 팀간의 관계와 협업 방식도 보여주고 있음을 알 수 있습니다.

 

위의 그림을 분석해 봅시다.

OrderTaking 및 Billing 컨텍스트 간의 관계는 "Shared Kernel"이 됩니다. 즉, 통신 계약을 공동으로 소유하게 됩니다.

  • OrderTaking와 Billing 사이의 관계는 "Customer Drivem Comtract" 관계가 될 것입니다. 즉, Billing 컨텍스트가 계약을 결정하고 OrderTaking시스템이 Billing 컨텍스트에 필요한 데이터를 정확히 제공합니다.
  • Order-taking와 Product-Catalog 간의 관계는 "conformist" 관계가 됩니다. 즉, OrderTaking 컨텍스트가 ProductCagalog와 동일한 모델을 사용하도록 합니다.
  • 마지막으로 외부 Address Checking 서비스에는 우리 도메인과 전혀 유사하지 않은 모델이 있으므로 상호 작용에 명시적 Anti-Corruption Layer를 삽입합니다. 이것은 서드파티 컴포넌트를 사용할 때 일반적인 패턴입니다. 이를 통해 공급업체 종속을 방지하고 나중에 다른 서비스로 교체할 수 있습니다.
도메인 상호 작용 방식을 결정하는 것은 종종 기술적인 문제만큼이나 조직적인 문제이며,
역 콘웨이 전략 -조직 구성을 보고 시스템 만들기-를 적용하는 경우도 있습니다 

바운디드 컨텍스트 내의 워크플로

디스커버리 단계에서 비즈니스 워크플로를 하나 이상의 도메인 이벤트를 생성하는 하나의 커맨드에 의해 시작된 미니 프로세스로 정의했습니다.

함수형 아키텍처에서 이러한 각 워크플로는 단일 기능에 매핑되며, 여기서 입력은 커맨드 객체이고 출력은 이벤트 객체 리스트입니다.

다이어그램을 만들 때 워크플로를 입력 및 출력이 있는 작은 파이프로 나타냅니다.

공개 워크플로(바운딩된 컨텍스트 외부에서 트리거되는 워크플로)는 그림과 같이 경계를 약간 넘어 "삐져나온" 것으로 표시됩니다.

워크플로는 항상 단일 경계 컨텍스트 내에 포함되며, 여러 컨텍스트를 통한 "end to end" 시나리오를 의미하는 것이 아닙니다.

워크플로의 입출력

워크플로 함수의 입력은 커맨드 객체입니다.

아웃풋은 다른 컨텍스트와 통신하기 위한 이벤트 집합입니다.

예를 들어 OrderTaking 워크플로에서 입력은 PlaceOrder 명령과 연결된 데이터이고 출력은 OrderPlaced 이벤트와 같은 이벤트 집합입니다.

그러나 청구 컨텍스트와 "customer/supplier" 관계가 있음을 확인했음을 기억하십시오.
즉, 일반 OrderPlaced 이벤트를 청구 컨텍스트로 보낼 때, 청구에 필요한 정보만 보내야 하며 더 이상은 보내지 않아도 됩니다.
예를 들어, 이것은 청구서 수신 주소와 청구 총액일 수 있지만 배송 주소나 배송 목록은 아닐 수 있습니다.
 
즉, 다음과 같은 구조로 워크플로에서 새 이벤트(BillableOrderPlaced)를 내보내야 합니다.
​ 	data BillableOrderPlaced =
​ 	    OrderId
​ 	    AND BillingAddress
​ 	    AND AmountToBill

 OrderAcknowledgementSent 이벤트도 내보내고 싶을 수도 있습니다.

앞의 다이어그램에서 워크플로 기능은 도메인 이벤트를 "퍼블리시"하지 않고 단순히 리턴한다는 점에 유의해야 합니다. 그들이 어떻게 퍼블리시되는지는 별개의 관심사입니다. (퍼블리시 방법 - 메세지큐, API 제공, 공유 파일 등등...)

바운디드 컨텍스트 내부의 도메인 이벤트 피하기

객체 지향 설계에서는 바운디드 컨텍스트 내에서 도메인 이벤트가 내부적으로 발생하는 것이 일반적입니다.
(주 : 커맨드를 워크플로 객체가 수신 -> 워크플로 객체가 이벤트 발행 -> 핸들러가 이벤트 수신)
이 접근 방식에서 워크플로 개체는 OrderPlaced 이벤트를 발생시킵니다.
다음으로 핸들러는 해당 이벤트를 수신하고 주문 승인을 보낸 다음 다른 핸들러가 BillableOrderPlaced 이벤트를 생성하는 식입니다. 다음과 같이 보일 수 있습니다.
객체 지향 바운디드 컨텍스트

함수형 디자인에서 우리는 이 접근 방식이 숨겨진 종속성을 생성하기 때문에 사용하지 않는 것을 선호합니다.

대신 이벤트에 대한 "리스너"가 필요한 경우 다음과 같이 워크플로 끝에 추가하기만 하면 됩니다.

함수형 바운디드 컨텍스트

이 접근 방식은 변경 가능한 상태를 가진 전역 이벤트 관리자가 없으므로 보다 명확하므로 이해하고 유지 관리하기가 더 쉽습니다.

바운디드 컨텍스트 내부 핵심 구조

기존의 "레이어드 어프로치 - 레이어드 아키텍처"에서 코드는 코어 도메인 또는 비즈니스 로직 계층, 데이터베이스 계층, 서비스 계층, API 또는 사용자 인터페이스 계층(또는 이들의 일부 변형)과 같은 계층으로 나뉩니다.
워크플로는 맨 위 레이어에서 시작하여 데이터베이스 레이어까지 내려갔다가 맨 위로 돌아갑니다.

객체 지향 레이어드 아키텍처
위에서 위로 돌아감

그러나 이 접근 방식에는 많은 문제가 있습니다.
한 가지 특정 문제는 "함께 변경되는 코드는 함께 속한다"는 중요한 설계 원칙을 위반한다는 것입니다.
레이어가 "수평으로" 조립되기 때문에 워크플로 작동 방식이 변경되면 모든 레이어를 터치해야 합니다. (흠...)
더 나은 방법은 "수직" 조각으로 전환하는 것입니다.
각 워크플로에는 작업을 완료하는 데 필요한 모든 코드가 포함되어 있으며, 워크플로에 대한 요구 사항이 변경되면 다음과 같이 특정 수직 조각의 코드만 변경하면 됩니다.
그러나 이것은 여전히 ​​​​이상적이지 않습니다. 이를 보기 위해 워크플로를 수평 파이프로 확장하고 그런 방식으로 레이어를 살펴보겠습니다.
레이어가 복잡하게 얽혀있어 이해하기 어렵습니다.

The Onion Architecture (오니온-헥사고날 아키텍처)

대신 도메인 코드를 중앙에 놓고 각 레이어가 더 멀리 있는 레이어가 아닌 내부 레이어에만 의존할 수 있다는 규칙을 사용하여 그 주위에 다른 측면을 조합하도록 합시다.

즉, 모든 종속성은 내부를 가리켜야 합니다. 이것을 "양파 아키텍처"라고 합니다.

클린 아키텍처, 헥사고날 아키텍처도 비슷한 개념입니다.

모든 종속성이 안쪽을 가리키도록 하려면 종속성 주입과 같은 기능을 사용해야 합니다.
(파라미터로 의존성을 먼저 주입받도록 하면 됨)

Keep I/O at the Edges - 입출력을 가장자리로

함수형 프로그래밍의 주요 목표는 내부를 볼 필요 없이 예측 가능하고 추론하기 쉬운 함수로 작업하는 것입니다.

이를 위해 가능한 한 불변 데이터로 작업하고 함수에 숨겨진 종속성 대신 명시적 종속성이 있는지 확인하려고 노력할 것입니다.

가장 중요한 것은 무작위성, 함수 외부 변수의 뮤테이션(수정), 모든 종류의 I/O를 포함하여 함수에서 부작용을 피하려고 노력하는 것입니다.

 

예를 들어, 데이터베이스나 파일 시스템을 읽거나 쓰는 기능은 "불순한" 것으로 간주되므로 핵심 도메인에서 이러한 종류의 기능을 피하려고 합니다.

그러면 어떻게 데이터를 읽거나 쓸 수 있습니까?

대답은 I/O를 양파의 가장자리로 푸시하는 것입니다.

예를 들어 워크플로 내부가 아니라 워크플로의 시작 또는 끝에서만 데이터베이스에 액세스합니다.

이것은 우리가 서로 다른 관심사를 분리하도록 강요하는 추가적인 이점이 있습니다.

코어 도메인 모델은 비즈니스 로직에만 관련되는 반면 지속성 및 기타 I/O는 인프라 관련 문제입니다.

실제로 I/O 및 데이터베이스 액세스를 에지로 이동하는 관행은 이전 장에서 소개한 지속성 무지(Persistene ignorance)의 개념과 매우 잘 맞습니다.

워크플로 내부에서 데이터베이스에 액세스할 수 없는 경우 데이터베이스를 사용하여 도메인을 모델링할 수 없습니다.

 

정리

  • 도메인 개체는 데이터 전송 개체(DTO)와 달리 컨텍스트 경계 내에서만 사용하도록 설계된 개체입니다.
  • 데이터 전송 개체(DTO)는 직렬화에 사용하고, 컨텍스트 간에 공유를 위해 설계된 개체입니다.
  • Shared Kernel, Customer/Supplier 및 Conformist는 바운디드 컨텍스트 간의 다른 종류의 관계입니다.
  • ACL(Anti-Corruption Layer)은 결합을 줄이고 도메인이 독립적으로 발전할 수 있도록 한 도메인에서 다른 도메인으로 개념을 변환하는 구성 요소입니다.
  • 지속성 무지는 도메인 모델이 도메인 자체의 개념에만 기초해야 하며 데이터베이스 또는 기타 지속성 메커니즘에 대한 인식을 포함하지 않아야 함을 의미합니다.

 

 

 



반응형