Blog

Read

Domain Modeling Made Functional

Aug 24, 2022

FP with DDD ¹ 타입과 함수를 통해서 도메인 모델을 구현하는 방법을 알아본다.

소프트웨어 개발을 입력(모듈 · 컴포넌트 · 요구 사항) 과 출력(프로그램 · 최종 결과)이 있는 파이프라인 이라 생각하자.

Understanding the Domain

  • 거대한 도메인을 독립적으로 구현하고 진화할 수 있는 작은 컴포넌트로 분해해야 한다.
  • 도메인과 일치하는 코드구현을 목표로 하고 텍스트처럼 읽히고 개발자가 아닌 사람들이 이해할 수 있는 코드를 만들 수 있도록 해야한다.
  • 도메인 이외의 용어를 도입하는 것을 지양해야 한다.
  • 함수형 프로그래밍에서는 데이터를 타입으로 표현하고 동작을 함수로 구현한다.
  • 함수형 프로그래밍이라고 해서 OOP의 모든 개념을 배척하는 것이 아닌 적절한 조합이 필요하다.


Domain Model

도메인 모델은 주어진 도메인의 객체를 모델링하는 방식을 나타낸다. 도메인을 구성하는 엔티티 · 해당 엔티티의 속성 · 엔티티간의 관계 그리고 엔티티의 동작과 상호작용을 구성하고 정의하는 개념 모델이다.



Focus on business event

기업은 데이터를 가지고 있을 뿐만 아니라 데이터를 어떻게든 변화시킨다. 즉 일반적인 비즈니스 프로세스를 일련의 데이터 또는 문서 변환이라고 생각할 수 있다. 비즈니스의 가치는 이러한 변환 과정에서 창출되므로 이러한 변환이 어떻게 작동하고 서로 어떻게 관련되는지 이해하는 것이 매우 중요하다.


그리고 이러한 구조를 파악하는데에 데이터 구조보다 도메인 이벤트에 집중해야 한다. 도메인 이벤트는 모델링 하려는 거의 모든 비즈니스 프로세스의 시작점이다. 이벤트를 도출하기 위해 이벤트 스토밍을 사용한다.



Event Storming

events-storming DDD 접근 방식에서 도메인 이벤트를 발견하는 가장 적합한 방법은 이벤트 스토밍을 활용하는 것이다. 혼자 이벤트 스토밍을 연습한다면 콘웨이의 법칙을 따라서 비즈니스에 필요한 팀 (배송 · 결제 등) 이라고 생각하며 요구사항을 고민해보자. 도출한 이벤트는 타임라인으로 그룹화 할 수 있으며 이는 종종 한 팀의 출력이 다른 팀의 입력임을 분명히 한다.



Documenting Commands

이벤트 스토밍을 통해 이벤트를 식별했다면 해당 이벤트를 촉발시키는 요청인 커맨드에 대해서 알아야 한다. 커맨드는 물론 실패할 수 도 있지만 성공하면 해당 도메인 이벤트를 생성하는 워크플로우를 실행한다.

  • 만약 커맨드가 "T사에 주문 양식 보내기" 라면 워크플로우가 주문을 보낸 경우 해당 도메인 이벤트는 "주문 양식이 전송됨" 이 된다.
  • 명령 : 주문하기 · 도메인 이벤트 : 주문이 접수됨.

책에서는 대부분의 비즈니스 프로세스를 이런 방식으로 모델링한다. 이벤트는 일부 비즈니스 워크플로우를 시작하는 명령을 트리거하고 워크플로우는 몇 가지 더 많은 이벤트를 출력한다. 물론 이러한 이벤트는 추가 커맨드를 트리거할 수 있다.

events-storming

커맨드와 도메인 이벤트는 서로 인풋이 될 수도 아웃풋이 될 수 도 있다. 커맨드에 의해서 이벤트가 트리거될 수 도 있고 이벤트에 의해서 커맨드가 트리거 될 수 도 있다.

이를 통해 비즈니스 프로세스를 인풋과 아웃풋이 있는 파이프라인으로 생각할수 있다. 성공하는 워크플로우 뿐만 아니라 실패시 워크플로우도 구성해야 한다. 또한 모든 이벤트가 커맨드와 연관있는것은 아니다. 몇몇 이벤트는 다른 외부 시스템 에 의해서 트리거되기도 한다.



Partitioning the Domain to Subdomains

기업이 이미 서비스별 부서를 갖고 있는 것처럼 도메인을 분리할 수 있다.

도메인이라는 용어를 일관성있는 지식의 영역으로 정의하자.



Creating a Solution Using Bounded Contexts

실제세계를 모델링한 문제 도메인을 바운디드 컨텍스트로 매핑한다. 각각의 컨텍스트는 문제해결을 위한 특별한 지식을 나타낸다.

왜 경계를 식별해야 하는가?
실제세계의 도메인은 매우 모호한 경계를 갖고 있는 반면 소프트웨어 세계에서는 분리된 서브시스템의 응집력을 줄이고 독립적으로 진화하기를 원한다. 이를 위해 서브시스템 간의 명시적 API를 사용하고 공유 코드와 같은 종속성을 피하는 것과 같은 소프트웨어 관행을 사용하여 이를 수행할 수 있다.

올바른 바운디드 컨텍스트를 설정하는 것은 도메인 주도 개발에서 가장 중요한 과제 중 하나다.

  • 모든 사람을 행복하게 하려고 하는 하나의 메가 컨텍스트보다 독립적으로 발전할 수 있는 개별적이고 자율적인 경계 컨텍스트를 갖는 것이 항상 더 좋다.
  • 정적인 디자인은 없으며 모든 모델은 비즈니스 요구사항 · 시간이 지남에 따라 진화해야 한다.


Creating Context Maps

컨텍스트를 정의했다면 디자인 세부사항에 얽매이지 않고 컨텍스트 간의 상호작용을 전달할 수 있는 방법이 필요하다. DDD에서는 이를 컨텍스트 맵 이라고 부른다. 컨텍스트 맵의 목표는 모든 세부사항을 캡처하는 것이 아니라 시스템 전체에 대한 뷰를 제공하는 것이다. 컨텍스트간에 메시지를 교환하기 위해서는 공유 포맷에 대한 동의가 필요하다.



Thinking About Inputs and Outputs

위에서 구현한 도메인 에서 인풋과 아웃풋을 보다 확실히 구분하기 위해 이 프로세스를 시작하기 위해서 어떠한 정보가 필요한가 라는 질문을 던질 수 있다. 워크플로우(시나리오)의 아웃풋은 항상 워크플로우가 생성하는 이벤트, 즉 다른 바운디드 컨텍스트에서 액션을 트리거하는 이벤트여야 한다.

storming

위 처럼 이벤트 스토밍을 마쳤다면 워크플로우에 대해서도 세분화가 필요하다. 예를들어 워크플로우에서 전달받은 이벤트(인풋)를 바로 사용하지 않고 이에 대한 검증 로직등을 추가할 수 있기 때문이다. 또 이러한 과정 중에 인풋과 아웃풋을 분할할 수 있다.

FP Looks like a Pipe-and-Filter Architecture



A Functional Architecture

빠른 개발 주기에서 도메인 일부를 이해하기 전에 구현하기 시작해야 하므로 다양한 컴포넌트를 구축하기 전에 서로 맞추기 위한 계획이 필요하다. 좋은 아키텍처의 목표중 하나는 컨테이너 · 컴포넌트 · 모듈 사이의 다양한 경계를 정의하여 변화(새로운 요구사항)에 유연하고 비용을 최소화 하는 것이다. 먼저 모놀리스 하게 시스템을 구축하고 이후에 필요시에 리팩토링하는 것이 좋다.


바운디드 컨텍스트가 아키텍처와 어떻게 연관이 있는지 에서부터 시작한다.



Communicating Between Bounded Contexts

바운디드 컨텍스트 간에 커뮤니케이션은 이벤트를 통해 이뤄진다. 이벤트는 큐(Kafka · RabbitMQ)를 통해 전송할 수도 있고 모놀리식 함수형 프로그래밍 에서는 함수 호출을 통해서 업스트림 컴포넌트에서 다운스트림 컴포넌트로 이벤트를 전송할 수도 있다. 이에 대한 선택은 구현하고자 하는 아키텍처에 따라 달라진다. 항상 그렇듯이 컴포넌트를 분리하도록 설계하는 한 지금 바로 이를 선택할 필요는 없다. 컨텍스트 간에는 서로 인지하지 못하며 이벤트를 통해서만 의사소통 한다. 이러한 분리는 의존성을 줄여 독립적인 개발이 가능하게 한다.



Transferring Data Between Bounded Contexts

일반적으로 컨텍스트 간에 소통을 위해 사용되는 이벤트는 단순 신호가 아닌 다운스트림 컴포넌트에서 이벤트 처리를 위해 필요한 모든 데이터를 포함한다. 이벤트는 바운디드 컨텍스트에서 도메인 객체(타입)로 표현되고 이는 DTO로 변환되어 다른포맷으로 직렬화 된다.



Types are not classes

A type is a just a name for a set of things. 도메인 모델을 타입을 통해서 표현할 수 있다.

1 data order_id
2 // opaque -- don't care about representation
3 data product_id
4 // opaque -- don't care about representation
5 data order_quantity is int
6 // constrained to be between 1 and 100
7 data phone_number is string
8 // Must not be null or empty. Must only contain digits
9 data order_line is order_id AND product_id AND order_quantity

도메인 개념을 사용한 표현을 타입을 통해 정의할 수 있다. 이는 일반 DDD에서 value-object를 표한하는 것과 유사하다. ADT를 활용하여 표현력을 높인다.



Fighting the impulse to do class-drvien design

DDD의 핵심 원칙 중 하나는 persistence ignorance 다. 데이터베이스의 데이터 표현에 대해 걱정하지 않고 도메인을 정확하게 모델링하는 데 집중해야 하기 때문에 중요한 원칙이다. 실제로 객체지향 역시 데이터베이스 모델에 치우치지 않는다. 의존성 주입 같은 기술은 데이터베이스 구현을 비즈니스 로직과 분리하도록 지원한다. 그러나 도메인이 아닌 객체와 클래스의 관점에서 생각한다면 디자인에 편향을 도입하는 것에 조심해야 한다.


예를들어 도메인 전문가가 다양한 종류의 연락 메서드를 설명할때 아래처럼 클래스 계층을 구현하고 싶어질 수 있다.

1 // represents all kinds of contact methods
2 class ContactMethodBase ...
3
4 // represents email contact method
5 class EmailAddressContactMethod extends ContactMethodBase ...
6
7 // represents phone contact method
8 class PhoneNumberContactMethod extends ContactMethodBase ...
  • 클래스가 실제세계에 존재하지 않는 인공적인 클래스를 도입한다.
  • 특정 클래스에 종속되어 있는 메서드는 재사용을 어렵게 한다.

States and lifecycles

대부분의 비즈니스 엔티티는 라이프사이클을 갖고 있다. 상태 및 라이프사이클 측면에서 생각해야한다.

  • 인보이스는 미지급 상태로 시작하여 지급 상태로 전환된다.
  • 구매자는 게스트로 시작한 다음 등록된 회원으로 전환된다.


Configuration

DDD와 FP를 활용한 코드에서의 구성에 대한 고민이 필요하다. 기존 DDD에서 도메인을 표현하는 entity · value-object · aggregate · hexagonal architecture ( port and adapter ) · DIP 등등 수많은 패턴과 구성 · 표현 방법 등을 FP에 알맞게 적용시켜야 한다. ( 필요 없는 기능은 제외하는 등 )

  1. 책에서 나온 방식과 비슷한 Pipe-and-Filter Architecture 형식의 코드를 구현할 것인지
  2. OOP를 적절하게 적용하여 FP스러운 클래스를 생성할 것인지


  1. Domain Modeling Made Functional - Scott Wlaschin