Skip to main content

DDD(Domain-Driven Design)


warning

DDD를 적용하는 것이 항상 좋은것은 아닙니다.

CRUD만으로 해결되지 않는 경우, 복잡한 사용자 스토리, 잦은 변경 및 기능 추가, 팀원 모두 생소한 도메인 등 여러 조건에 따라 필요한 곳에 부분적으로 적용하는 것이 좋습니다.

Domain layer

Domain

개발하고자 하는 구체적인 비지니스 영역(현실세계의 문제)를 도메인이라고 합니다. 도메인은 여러 개의 서브도메인으로 이뤄져 있습니다. 해당 분야에 대한 지식을 가지고 있는 전문가, 실무자 등을 도메인 전문가(Domain expert)라고 부릅니다.

Domain model

모형(模型, Model, 모델)은 객체, 시스템, 또는 개념에 대한 구조나 작업을 보여주기 위한 패턴, 계획, 또는 설명이다. - Wikipedia

도메인 모델은 특정 도메인을 개념적으로 표현한 것입니다. 도메인 모델링을 하면서 도메인 전문가와 소프트웨어 개발자들 사이에서 개발되고 공유된 언어를 유비쿼터스 언어(Ubiquitous Language)라고 합니다.

Entity

작게 나눈 도메인 논리와 그와 관련된 데이터를 하나의 객체로 만들었을 때, 이 객체가 식별성을 갖는다면 엔티티(Entity)라고 합니다.

  • 엔티티는 프로그램 내에서 식별할 수 있는 ID를 가지며, 이는 개념적 식별성과 같을 수도 다를 수도 있습니다.
  • 엔티티는 생성되서 삭제되는 시점까지 연속성을 가지므로 갑자기 사라지지 않습니다. 따라서 삭제 전까지는 프로그램이 종료되더라도 연속성을 유지해야하므로 영속성(Persistence)을 갖습니다.
  • 엔티티는 데이터베이스, 사용자 인터페이스, 서드파티 프레임워크 등의 변화에 영향을 받지 않도록 해야합니다.

Value Object

식별성은 없지만 도메인 논리와 그와 관련된 데이터로 이루어진 도메인 객체가 있다면 이를 값 객체(Value Object)라고 합니다.

  • 값 객체가 공유되면 한 객체에서의 변경이 의도하지않은 결과를 초래할 수 있으므로 불변적(Immutable)이어야 합니다.
  • 한 객체가 속성으로 값 객체를 가질 때, 이를 외부에 노출 시켜야하는 경우가 있는데, 이 경우 소유 객체의 불변식(Invariant)을 위반 할 수 있으므로 불변적이어야 합니다.
  • 개발자들이 공유 객체의 불변성을 엄격하게 지킨다는 가정하에, 성능 개선이나 메모리 관리 등을 위해 값 객체를 공유할 수 있습니다.

Aggregate

복잡한 연관 관계를 맺는 객체들이 생기고 이를 대상으로 변경의 일관성(Consistency)을 보장하기란 쉽지 않습니다. 이 문제를 해결하기위해 연관된 객체를 모아 만든 객체를 애그리거트(Aggregate)라고 합니다.

  • 애그리거트의 생성이 복잡한 경우 팩토리(Factory) 패턴을 사용할 수 있습니다.
  • 애그리거트는 연관된 객체를 감싸는 경계(Boundary)를 갖습니다.
  • 경계 외부에 노출되는 하나의 루트(Root) 엔티티를 갖습니다.
    • 루트는 전역으로 식별되는 ID를 갖습니다.
    • 루트는 경계 내의 객체들을 변경할 수 있습니다. 따라서 불변식(Invariant)을 검사해야할 책임이 있습니다.
    • 루트는 내부 엔티티 참조를 반환할 수 있습니다. 참조된 객체의 불변성을 엄격히 지켜야 합니다.
    • 루트는 내부 값 객체의 복사본을 반환할 수 있습니다.
  • 애그리거트 안의 객체들은 외부 애그리거트 루트만 참조할 수 있습니다.
  • 리포지터리(Repository)를 만들 때 애그리거트 루트를 반환하도록 만듭니다.
  • 삭제 연산 시 애그리거트의 모든 객체를 제거해야 합니다.

Domain Service

엔티티나 값 객체에 속하지는 않지만 이들을 조합하여 수행되어야 하는 도메인 논리들을 도메인 서비스(Domain Service)라고 합니다.

  • 상태를 갖지 않습니다.
  • 어떤 도메인 객체에 포함시키기 어렵습니다.
  • 다음의 경우 사용할 수 있습니다.
    • 중요한 비지니스 프로세스를 수행하는 경우(예를 들어 둘 이상의 도메인 객체에서 필요로하는 입력 값을 계산)
    • 어떤 컴포지션(조합)에서 다른 컴포지션(조합)으로 도메인 객체를 변경하는 경우

Domain Event

Factory

Repository

Application layer

Application Service

도메인 객체를 사용하여 특정 시나리오를 수행하거나 도메인 객체와 상관없이 애플리케이션의 편의를 위해 제공되는 기능들, 즉 애플리케이션에 특화된 논리들을 애플리케이션 서비스(Application Service)라고 합니다.

  • 애플리케이션 서비스의 인터페이스는 도메인 모델의 외적 요소의 측면에서 정의되어야 합니다.
  • 매개변수와 반환 타입은 도메인 객체로 구성되어야 합니다.
  • 서비스 자체의 상태는 갖지 않습니다.

애플리케이션 서비스에서 자주 등장하는 형식은 아래와 같습니다.

  1. 리포지터리에서 엔티티 또는 애그리거트를 얻어옵니다.
  2. 도메인 서비스, 애그리거트, 엔티티 등을 사용하여 일련의 도메인 논리를 수행합니다.
  3. 리포지터리를 사용하여 변경사항을 저장합니다.

Port-Adapter

Port

포트는 수행해야 하는 작업을 추상화한 인터페이스(Interface)입니다. 단순히 도구의 API를 모방하는 것이 아니라 도메인 요구사항에 맞는 인터페이스로 구성되어야 다양한 어댑터를 붙일 수 있습니다.

Primary/Driving Adapter

1차 어댑터의 시그니처는 HTTP, gRPC, CLI 등 외부 요청에 맞춰 작성되어야 합니다.

warning

어댑터의 시그니처에 파라미터와 리턴 타입을 도메인 객체로하면 클라이언트 들이 도메인 객체에 대한 의존성을 갖게 될 수있습니다.

Secondary/Driven Adapter

Directory Structure

디렉토리, 파일 명은 의미에 맞게 변경될 수 있습니다.

context/
├── domain/
│ ├── event/
│ │ ├── event_1.go
│ │ └── ...
│ ├── factory/
│ │ ├── entity_1.go
│ │ └── ...
│ ├── repository/ # 인터페이스
│ │ ├── entity_1.go
│ │ └── ...
│ ├── service/
│ │ ├── service_1.go
│ │ └── ...
│ ├── valueobject/
│ │ ├── valueobject_1.go
│ │ ├── valueobject_2.go
│ │ └── ...
│ ├── entity_1.go # 애그리거트 루트
│ ├── entity_2.go
│ └── ...
├── application/
│ ├── service/ # service, notification 등의 인터페이스
│ └── usecase/ # domain과 application의 인터페이스를 사용하여 usecase 구현
│ ├── usecase_1.go
│ └── ...
└── adapter/
├── orm/ # orm, postgresql 등의 구현
│ ├── model/
│ │ ├── entity_1.go
│ │ └── ...
│ └── client.go # 클라이언트 사용을 위한 초기화와 싱글톤 객체를 생성합니다.
├── repository/ # 구현
│ ├── entity_1.go
│ └── ...
├── service/ # 구현
└── controller.go
warning
  • domain 디렉토리 내의 코드는 domain 내에서만 의존성을 가져야합니다.
  • application 디렉토리 내의 코드는 domain, application 내에서만 의존성을 가져야합니다.
  • adapter 디렉토리 내의 코드는 다양한 의존성을 가질 수 있지만 repository를 제외한 곳에서는 domain에 대한 의존성을 최소화해야합니다.

Reference