DDD(Domain Driven Design, 도메인 주도 설계) 는 도메인을 중심으로 설계하는 방법론이다.
소프트웨어 개발하면서 방대하고 복잡한 지식으로 인해 어려움이 생기는데, 도메인을 중심으로 설계하면 이 부담을 해소할 수 있다.
도메인 모델 만들기
도메인 모델은 도메인과 관련된 지식을 엄격하게 구성하고 선택적으로 추상화 한 것이다.
적절한 모델을 통해 정보를 이해하고 해결하고자 하는 문제 자체에 집중할 수 있다.
DDD 에서는 다음과 같은 세 가지 용도에 따라 모델을 선택하게 된다.
- 핵심 설계를 위한 도메인 모델
- 도메인 모델과 설계 및 구현은 긴밀한 관계를 가짐
- 모델 이해를 근거하여 코드를 해석할 수 있기 때문에 유지보수와 기능 개선에 도움이 됨
- 팀 구성원들의 중추적인 언어를 위한 도메인 모델
- 이 모델을 토대로 프로그램 의견을 나눌 수 있음
- 번역 절차가 필요하지 않음
- 지식의 정수만을 추출하기 위한 도메인 모델
- 용어를 선택, 개념 분류, 지식들을 연관 시키면서 팀원들의 사고 방식을 담을 수 있음
- 많은 정보를 모델로 만들어서 효과적인 협업 가능
Domain (도메인)
- 소프트웨어로 해결하고자 하는 문제 영역
- 한 도메인은 다시 하위 도메인으로 나누어질 수 있음
Domain Model (도메인 모델)
- 도메인을 개념적으로 표현한 것 (ex. 클래스 다이어그램, 상태 다이어그램, 그래프 등)
- 여러 관계자들이 도메인을 이해하고 공유하는데 도움
Ubiquitous Language(보편 언어)
- 도메인 모델에 따라 모든 팀원 간의 활동을 연계하는데 사용되는 공통 언어
- 언어가 분열되면 의사소통이 무뎌지고 빈약해지기 때문에 조화가 깨지고, 소프트웨어의 신뢰도가 떨어짐
- 지속적으로 사용하면 모델의 취약점이 드러나고, 개선이 필요한 지점을 찾을 수 있음
- 의사소통과 코드에 끊임없이 동일한 언어를 적용
- 도메인 전문가: 도메인을 이해하는데 부자연스럽고 부정확한 용어나 구조에 대해 반대 의사를 표명해야 함
- 개발자: 설계를 어렵게 만드는 모호하거나 불일치 요소를 찾아내야 함
모델 주도 설계의 기본 요소
내비게이션 맵을 통해 도메인 주도 설계 과정에 사용되는 패턴들이 서로 어떻게 관계를 맺는지 알아본다.
Layered Architecture(계층형 아키텍처)
도메인과 관련된 코드가 관련 없는 코드를 통해 널리 확산된다면 도메인에 대해 추론하기 어려워진다.
복잡한 작업을 처리하는 소프트웨어를 만들기 위해서는 관심사의 분리를 통해 격리를 시켜야 각 설계 요소에 집중할 수 있다.
분리하는 많은 방법들 중에서는 Layered Architecture 가 널리 사용되고 있는데, 대다수는 네 가지 개념적 계층으로 나뉘어진다.
- 표현 계층(사용자 인터페이스)
- 사용자에게 정보를 보여주고 사용자의 명령을 해석하는 계층
- 사람이 아닌 다른 컴퓨터 시스템이 외부 행위자가 될 수 있음
- 응용 계층(애플리케이션)
- 소프트웨어가 수행할 작업을 정의하고 도메인 객체가 문제를 해결하는 계층
- 업무상 중요하거나 다른 시스템의 응용 계층과 상호작용
- 업무 규칙이나 지식이 포함되지 않고 얇게 유지되어야 함
- 도메인 계층
- 업무 개념, 업무 상황, 업무 규칙을 표현하는 계층
- 업무용 소프트웨어의 핵심 계층
- 인프라스트럭처 계층
- 일반화된 기술적 기능 제공 (ex. 메시지 전송, 도메인 영속화, UI 위젯 그리기 등)
- 네 가지 계층에 대한 상호작용 패턴 지원 가능
- 도메인의 구체적인 지식을 포함하지 않아야 함
Entity(엔티티, 참조객체)
- 일차적으로 해당 객체의 식별성으로 정의한 객체
- 객체의 생명주기 동안 형태와 내용이 바뀔 수 있지만 연속성은 유지되어야 함
- 클래스 정의를 단순하게 하고 생명주기의 연속성과 식별성에 집중
- 특정 속성보다는 정체성에 대해 초점을 맞춰야 함
- 식별 수단은 모델에서 식별성을 구분하는 방법과 일치해야 함
- 특정 규칙에 따라 생성
- UUID 사용
- 값 직접 입력
- 일련번호 사용(시퀀스나 DB 자동 증가)
Value Object(값 객체)
- 개념적 식별성을 갖지 않으면서 도메인의 서술적 측면을 나타내는 객체
- 어떤 요소의 속성에만 관심이 있다면 Value Object 로 구분하고 의미와 관련 기능 부여
- Value Object 가 Entity 를 참조할 수 있음
- 불변적(immutable)으로 다뤄야 함
- 식별성을 부여하면 안 됨
- Value Object 간의 양방향 연관관계는 제거해야 함
Service(서비스)
- Entity 나 Value Object 에서 구현하지 못하는 도메인 연산
- Entity 나 Value Object 의 일부를 구성하는 것이 아님
- 상태를 캡슐화하지 않음
- 연산의 명칭은 Ubiquitous Language 에서 유래되거나 반영되어야 함
- 서비스는 응용, 도메인, 인프라스트럭처로 여러 계층으로 분할 가능
Module(모듈, 패키지)
- 모듈화를 통해 해당 모듈의 세부사항을 보거나, 모듈 간의 관계를 확인할 수 있음
- 하나의 의사소통 메커니즘
- Ubiquitous Language 를 구성하는 것으로 모듈 이름 부여
- 모듈 간에는 결합도가 낮아야 함
- 사람이 생각할 수 있는 양에 한계가 있음
- 모듈화를 통해 다른 코드로부터 도메인 계층을 분리해야 함
- 모듈의 내부는 응집도가 높아야 함
- 일관성이 없는 단편적인 생각은 이해가 어려움
- 하나의 개념적 객체를 구현하는 코드는 모두 같은 모듈에 위치
Aggregate(집합체)
- 데이터 변경의 단위로 다루는 연관 객체의 모음
- 일관성 규칙의 경계로 적용
- 각 Aggregate 는 루트(root) 와 경계(boundary) 가 존재
- 루트는 Aggregate 마다 하나만 존재하며, 특정 Entity 를 가리킴
- 경계 바깥쪽의 객체는 루트를 통해서만 참조 가능
- 루트를 통해서만 Aggregate 상태 변경 가능
- 루트는 Value Object 복사본을 다른 객체에 전달할 순 있음
- 루트 Entity 는 전역 식별성을 지니며, 불변식 검사 책임이 있음
- 삭제 연산은 Aggregate 경계 안의 모든 요소를 한 번에 제거해야 함
통찰력을 위한 리팩터링
리팩터링이란 소프트웨어의 기능을 수정하지 않고 설계를 다시 하는 것이다.
지속적으로 리팩터링을 수행하려면 설계가 유연하고 명확하여 표현할 수 있어야 한다.
Factory (팩토리)
- 객체나 전체 Aggregate 의 복잡한 생성을 캡슐화
- 설계하는 방법으로 팩토리 메서드(factory method), 추상 팩토리(abstract factory), 빌더(builder) 패턴 등이 존재
- Entity Factory 와 Value Object Factory
- Entity Factory: 필요한 필수 속성만 받아들이는 경향이 있음
- Value Object Factory: 불변젹이므로 최정적인 형태로 만들어짐
Factory 설계 기본 요건
- 생성 방법은 원자적, 불변식을 모두 지켜야 함
- 생성하고자 하는 타입으로 추상화 되어야 함 (Factory 패턴)
Repository (리파지터리)
- 특정 타입의 모든 객체를 개념적 집합 (객체 컬렉션처럼 동작)
- 데이터에 대한 실제 저장소와 질의 기술을 캡슐화하여 모델에 집중할 수 있게 도와줌
- 데이터 소스로부터 도메인 설계를 분리
- 저장∙조회∙검색 행위를 캡슐화
- 영속 객체는 해당 객체의 속성으로 전역적으로 접근할 수 있어야 함
- 직접 접근 해야하는 Aggregate 루트에 대해서만 Repository 를 제공
- 마음대로 데이터베이스 질의를 한다면 도메인 객체와 Aggregate 캡슐화가 깨질 수 있음
- 질의의 수가 많으면 Specification(명세) 기반하여 질의를 수행
- Specification: 특정 조건을 만족하는 객체를 찾는데 사용되는 객체
- Specification 을 이용하면 유연한 질의를 수행 가능
Factory 와 Repository 차이
- Factory: 새로운 객체 생성
- 객체 생애의 초기 단계
- Repository: 기존 객체 조회
- 객체 생애의 중간과 마지막 단계
- 메모리 상에 객체가 존재하고 있는 것처럼 동작
Specification (명세)
- 객체가 특정 기준을 만족하는지 판단하는 술어
- 특별한 목적을 위해 술어와 비슷한 명시적인 Value Object
- 규칙을 도메인 계층에 유지할 수 있음
Specification 용도
- 검증(validation)
- Specification 의 개념을 가장 직관적으로 설명해주는 방식
- 특정한 조건에 부합하는지 여부를 판단하기 위해 개별 객체 테스트
- 선택 (질의)
- 특정한 조건을 기반으로 객체 컬렉션의 일부 선택 (필터링)
- Specification 에 메서드를 추가하여 질의문(query) 을 캡슐화하여 사용할 수 있음
- 요청 구축(생성)
- 명시된 조건을 만조가는 새로운 객체나 객체 집합을 새로 만들거나 재구성
- 존재하지 않는 객체에 대한 기준을 명시
유연한 설계를 위한 패턴
Intention-Revealing Interface (의도를 드러내는 인터페이스)
- 설계에 포함된 모든 요소(타입, 메서드, 인자 이름)가 인터페이스를 구성하고, 설계 의도를 드러냄
- 수행 방법이 아닌 결과와 목적만을 표현하도록 클래스와 연산의 이름을 부여 (Ubiquitous Language 용어를 따름)
- 연산을 추가하기 전에 행위에 대한 테스트 우선 작성
Side-Effect-Free Function (부수효과가 없는 함수)
- 함수(function)는 부수효과를 일으키지 않고 항상 동일한 값 반환
- 명령과 질의를 엄격하게 분리된 다른 연산으로 유지해야 함
- 변경을 발생시키는 메서드는 데이터를 반환하지 않도록 함
- 명령과 질의 분리 대신 불변 객체 Value Object 활용할 수 있음
Assertion (단언)
- 프로그램이 어느 시점에 지녀야 할 정확한 상태를 나타내는 문장
- 자동화된 테스트나 프로그램 코드에 직접 연산의 사후조건과 클래스 및 Aggregate 의 불변식을 표현
- Assertion 을 의도적으로 추측할 수 있게 하고 응집도 높은 개념을 포함된 모델을 만들어야 함
Conceptual Contour (개념적 윤곽)
- 도메인 자체의 근원적인 일관성
- 도메인의 설계 요소(연산, 인터페이스, 클래스, Aggregate) 를 응집력 있는 단위로 분해
- 리팩터링을 통해 변경되는 부분과 변경되지 않는 부분을 중심 축으로 식별하고 분리해야 함
Standalone Class (독립형 클래스)
- 무관한 모든 개념을 제거하여 클래스를 독립적(self-contained)으로 유지해야 함 (낮은 결합도)
- 어떤 것도 참조하지 않은 상태에서 이해하고 테스트가 가능한 클래스
- 의존성이 증가할수록 설계를 파악하기 어려워져 복잡도가 매우 높아짐
Closure Of Operation (연산의 닫힘)
- 반환 타입과 인자 타입이 동일한 연산
- 구현자(implementer)가 연산에 사용된다면 인자, 반환, 구현자의 타입을 동일하게 정의
- 부차적인 개념을 사용하지 않고도 고수준의 인터페이스 제공
- Value Object 연산을 정의하는데 주로 사용
전략적 설계
시스템이 복잡해질수록 커다란 모델을 다루고 이해하기 위한 기법이 필요하다.
전략적 설계는 프로젝트를 교착상태에 빠지지 않고 시스템의 핵심 개념과 비전을 포착할 수 있어야 한다.
도메인 주도 설계에서는 이 목표를 다루기 위해 컨텍스트, 디스틸레이션, 대규모 구조를 다룬다.
모델의 무결성 유지
모델의 용어는 언제나 의미가 동일하고, 모순되는 규칙도 없도록 일관성(단일화, unification)을 유지해야 한다.
하지만 대규모 시스템에서 도메인 모델을 단일화하는 것은 어렵기 때문에 의식적으로 전략을 결정하고 따라야 한다.
Bounded Context (제한된 컨텍스트)
- 사용된 용어를 특정한 의미로 의사소통하기 위한 조건의 집합
- 다수의 모델이 한데 섞이면 신뢰성이 떨어지고 이해하기 힘들어짐 (중복된 개념, 허위 동적 언어)
- 특정 모델에 포함된 범위가 정해진 컨텍스트
- 컨텍스트의 경계를 물리적인 형태(조직, 코드 기반, 데이터베이스 스키마 등)의 관점에서 명시적으로 설정
- 컨텍스트 경계는 대개 팀 조직의 윤곽을 따라 정해짐
- 경계 내에서는 모델을 일관된 상태로 유지하고 경계 바깥으로 인해 혼란이 생기지 않아야 함
- Bounded Context 간에는 코드를 재사용하지 않아야 함
Continuous Integration (지속적인 통합)
- 내부적인 균열을 빠르게 포착하고 정정할 수 있도록 컨텍스트 내의 모든 작업을 병합해서 일관성을 유지하는 것
- 하나의 Bounded Context 내에서만 필수적
- 모델 개념의 통합 (통합 방법이 용이해짐)
- 변화하는 모델을 함께 이해하고 발전하면서 Ubiquitous Language 유지
- 구현 수준에서의 통합 (유효성과 일관성 입증, 균열 포착)
- 단계적이고 재생 가능한 병합/빌드
- 자동화된 테스트 스위트
- 수정사항이 통합되지 않은 상태로 존재할 수 있는 시간을 적당히 짧게 유지
Context Map (컨텍스트 맵)
- 서로 다른 컨텍스트 간의 관계를 정의하고 모든 모델 컨텍스트를 아우르는 전체적인 뷰
- 컨텍스트 간의 번역에 대한 윤곽을 명확하게 하고 만나는 경계 지점에 대한 공유 정보 강조 필요
- Bounded Context 의 명확한 이름을 제공하고 경계 지점의 특성을 명확하게 표현해야 함
- 다이어그램들이 맵을 가시화하고 의사소통하는 데 유용
Shared Kernel (공유 커널)
- 공유하기로 한 도메인 모델의 부분집합
- 모델 요소, 연관 코드, 데이터베이스 설계 등이 포함
- core domain, generic subdomain 의 일부인 경우가 대부분
- 변경하기 위해서는 다른 팀과의 협업이 필요
- 기능 시스템을 통합할 때는 양 팀에서 작성한 테스트 모두 실행 필요
- 일반적으로 각 팀은 별도의 Kernel 복사본을 변경하고 다른 팀과 통합
Customer/Supplier development(고객/공급자 개발 팀)
- 두 팀 간에 고객/공급자 관계를 확립 필요
- 하류 팀이 상류 팀에 대한 고객 역할
- 고객의 요구사항이 가장 중요
- 인터페이스를 검증하는 인수 테스트 작성 필요
- 테스트 스위트에 추가하여 상류팀은 자유롭게 코드 변경 가능
Conformist (준수자)
- 연결되는 지점에서 상류팀의 도메인 모델을 그대로 따르는(준수하는) 모델
- 상류팀의 모델을 준수하여 하류 팀의 설계 형식이 상류 팀에 속박 됨
- 이상적인 모델을 만드는 것은 어렵지만 통합은 단순해짐
- 모델을 공유한다는 점이 shared kernel 과 유사
- shared kernel: 밀접하게 조율하는 두 팀 간의 협력관계를 다룸
- conformist: 협력에 관심 없는 팀과의 통합 문제를 다룸
Anticorruption Layer (오류 방지 계층)
- 클라이언트 도메인 모델 측면에서 기능을 제공하는 격리 계층
- 다른 시스템과 상호작용하는 모델로 인해 도메인 모델의 의도가 매몰되는 것을 방지
- 객체와 행위를 다른 모델과 프로토콜로 변환하기 위한 메커니즘
- 공용 인터페이스는 보통 Service (간혹 Entity) 의 집합으로 표현
- 두 Bounded Context 를 잇는 수단
Anticorruption Layer 구현
통신 및 전송 메커니즘, Facade, Adapter, 번역기 조합으로 설계
- Facade
- 한쪽 모델에서 다른 모델로 번역하는 것을 담당
- 하위 시스템에 대한 클라이언트 접근을 단순화하는 인터페이스
- 다른 시스템 모델에 따라 엄격하게 작성해야 함
- Adapter
- 클라이언트에서 구현된 행위를 사용할 수 있게 해주는 래퍼(wrapper)
- Facade 에 상응하는 요청을 수행하는 행위를 구현
- 번역기
- 개념 객체나 데이터의 변환
- Adapter 에 속하는 요소로 상태가 필요 없음
Separate Ways (각자의 길)
- Bounded Context 가 다른 것과 관계를 맺지 않도록 선언하여 범위 내에서 해결책을 찾음
- 통합에도 큰 비용이 발생되기 때문에 관계를 끊을 수 있음
- 통합이 필요해진다면 번역 계층이 필요해질 수 있음
Published Language (공표된 언어)
- Bounded Context 모델 간에 소통되는 공통의 언어
- 필요한 도메인 정보를 표현하는 공유 언어를 공통의 의사소통 매개체로 사용 또는 번역
- 기존 모델을 직접 번역하면 복잡하고 문서화가 어려움
- 시스템 간의 상호작용이 많아지면 Published Language 를 갖추고 관계 공식화가 필요
- Published Language 는 안정적이어야 함
- 호스트 모델은 리팩토링하면서 자유로이 변경할 수 있어야 함
Distillation (디스틸레이션)
- 혼합된 요소를 분리하여 본질을 더 값지고 유용한 형태로 뽑아내는 과정
- 도메인 지식과 중요한 우선순위를 추상화
- 모델의 산만한 요소를 없애고 중요한 부분에 집중하게 만듦
Core Domain (핵심 도메인)
- 애플리케이션의 목적에 특유하고 중심적인 모델
- 모델을 요약하고 가치 있고 전문화된 개념을 부각
- 심층 모델을 찾고 유연한 설계를 개발할 수 있어야 함
- Core 는 작게 유지해야 함
Generic Subdomain (일반 하위 도메인)
- 응집력 있는 하위 도메인을 식별하여 별도 module 로 분리
- 전문지식을 전달하지 않고 복잡성을 더하는 부수적인 요소
- core domain 보다 낮은 우선 순위 부여
- 재사용을 목표로 설계하지 않아도 일반적인 개념의 범위 내에서 설계를 유지하는 것은 엄격해야 함
Domain Vision Statement (도메인 비전 선언문)
- Core Domain 을 짧게 기술하고 해당 모델이 가져올 가치에 대해 작성한 선언문
- 약 한페이지 분량이 적당
- 새로운 통찰력을 얻을 때마다 개정 필요
- 디스틸레이션 과정에서 공통적인 방향으로 향하는 이정표가 될 수 있음
Highlighted Core (강조된 핵심)
- 핵심적인 부분을 쉽게 파악할 수 있도록 Core Domain 을 더 잘 보이게끔 만드는 과정
- 디스틸레이션 문서 작성 기법
- Core Domain 과 Core 의 구성요소 사이에 일어나는 상호작용 기술
- 표시된 Core 기법
- 모델 주요 저장소안에 있는 Core Domain 구성 요소에 대해 역할 표시
Cohesive Mechanism (응집력 있는 메커니즘)
- 응집력이 있는 부분을 뽑아 별도의 경량 프레임워크로 분할해야 함
- 프레임워크 기능은 Intention-Revealing Interface 로 노출
- Generic Subdomain 과 비슷할 수 있지만 도메인 모델과 섞이지 않아야 함
- Generic Subdomain: 도메인의 일부 측면을 표현하는 모델 (덜 중요한 Core domain)
- Cohesive Mechanism: 도메인을 나타내지 않고 일부 문제 해결
Segregated Core (분리된 핵심)
- 보조적인 역할로부터 Core의 개념을 분리한 모델
- 일반적이거나 보조적인 역할의 구성요소는 다른 객체로 추출하여 다른 패키지에 배치
- Core와 다른 코드의 결합도는 감소하고 응집력은 강화됨
Abstract Core (추상화된 핵심)
- 모델의 근본적인 개념을 식별하여 클래스, 추상 클래스, 인터페이스로 추출
- 하위 도메인 간에 참조나 상호작용이 많으면 수평적으로 자르는 것을 고려
- 추상 모델은 컴포넌트 간에 발생하는 상호작용을 표현
- 추상적이고 전체적인 모델은 자체적인 모듈에 배치
대규모 구조
시스템을 깊게 파악하지 않아도 각 부분이 담당하는 역할에 대해 이해할 수 있어야 한다.
대규모 구조는 시스템을 넓은 시각으로 이해하게끔 돕는 언어로
전체적인 관점에서 각 부분을 이해하기 위한 규칙이나 패턴을 고려해본다.
Evolving Order (발전하는 질서)
- 발전 과정에서 전혀 다른 구조로도 유연하게 변화할 수 있어야 함
- 설계 및 모델과 관련된 의사결정이 제약되어서는 안됨
- 대규모 구조는 어떤 모델을 개발하는데 부자연스러운 제약조건 없이 명확한 시스템을 만들 수 있을 때 적용해야 함
System Metaphor (시스템 은유)
- 전체 설계의 중심 주제를 전달하고 이해하는 공유할 수 있는 은유 (ex. 방화벽)
- 객체 패러다임과 조화를 이루고, 쉽게 이해할 수 있는 대규모 구조
- 구체적인 비유가 유용한 사고를 이끌어 낸다면 대규모 구조로 채택할 수 있음
- 의사소통 및 개발 촉진
- Ubiquitous language 로 흡수될 수 있음
Responsibility Layer (책임 계층)
- 책임 주도 설계와 계층화 원칙이 합쳐친 개념적 계층
- 각 도메인 객체의 책임이 한 계층의 책임안에서 이뤄지는 구조
- 응집력 강화
- 모듈의 책임을 쉽게 이해할 수 있음
Knowledge Level (지식 수준)
- 모델의 구조와 행위를 서술하고 제약하는 데 쓸 수 있는 객체 집합
- Knowledge Level 은 일반 객체로 만들어야 함
- 모델의 특정 부분을 클라이언트에게 제공할 때 생기는 문제를 해결
- 모델에서 자기 규정적(self-defining) 측면을 분리하여 제약 조건을 명시적으로 만듦
Pluggable Component Framework (착탈식 컴포넌트 프레임워크)
- 인터페이스와 상호작용에 대한 Abstract Core 를 정제하고 구현을 자유롭게 대체할 수 있는 프레임워크
- 단점
- 심층적인 모델이 필요하므로 적용이 힘든 패턴
- 컴포넌트의 프로토콜을 변경하지 않고 Abstract Core 변경 불가 (심층적인 리팩터링이 어려움)
출처
- 도메인 주도 설계 소프트웨어의 복잡성을 다루는 지혜 / Eric Evans