Best Practice

DDD(Domain Driven Design) 소개

David Laribee

이 기사에서는 다음 내용에 대해 설명합니다.

  • 보편적 언어에서의 모델링
  • 바인딩된 컨텍스트와 루트 집계
  • 단일 책임 원칙 사용
  • 리포지토리와 데이터베이스
이 기사에서 사용하는 기술:
Visual Studio

목차

플라톤의 모델
대화의 이해
컨텍스트
스스로의 가치 제안 알기
단일 책임의 시스템
ID와 수명을 가지는 엔터티
대상을 설명하는 값 개체
엔터티를 결합하는 집계 루트
도메인 서비스 모델 기본 작업
리포지토리 저장 및 집계 루트 분배
데이터베이스에 대한 사항
DDD 시작하기

DDD(Domain Driven Design)는 정교한 개체 시스템을 제작하는 데 도움이 되는 원칙과 패턴의 집합입니다. 이를 올바르게 적용하면 도메인 모델이라고 하는 소프트웨어 추상화를 달성할 수 있습니다. 이러한 모델을 복잡한 비즈니스 논리를 캡슐화하고 비즈니스 현실과 코드 간의 격차를 완화할 수 있습니다.

이 기사에서는 DDD와 밀접하게 연관된 기본 개념과 디자인 패턴을 살펴보겠습니다. 이 기사는 풍부한 도메인 모델의 설계 및 발전을 위한 기본적인 소개라고 보면 적당합니다. 논의와 관련된 약간의 배경 정보를 제공하기 위해 필자가 잘 알고 있는 복잡한 비즈니스 영역인 보험 정책 관리에 대해 다루겠습니다.

이 기사에서 다루는 주제에 대해 더 자세히 알아보려면 Eric Evans의 Domain-Driven Design: Tackling Complexity in the Heart of Software라는 책을 읽어 보십시오. 이 책은 DDD에 대한 최초의 입문서일 뿐만 아니라 풍부한 경험을 갖춘 소프트웨어 디자이너가 제공하는 귀중한 정보의 보고입니다. 이 기사에서 설명할 DDD의 패턴과 핵심적인 특성은 이 책에 설명된 개념을 기반으로 합니다.

구조적 필요에 따른 컨텍스트 잘라내기

바인딩된 컨텍스트를 전적으로 응용 프로그램의 기능 영역에 따라서 구성할 필요는 없습니다.. 바인딩된 컨텍스트는 바람직한 구조적 예를 확보하기 위해 시스템을 분할하는 데 유용합니다. 이러한 방식의 전형적인 예는 견고한 트랜잭션 영역과 보고서 포트폴리오를 모두 가진 응용 프로그램입니다.

이러한 상황(비교적 자주 발생함)에는 트랜잭션 데이터베이스로부터 보고 데이터베이스를 분리하는 것이 바람직한 경우가 많습니다. 여러분은 신뢰할 수 있는 보고서를 개발하기 위해 적당한 수준의 정규화를 추구할 자유를 원하고, 트랜잭션 비즈니스 논리를 계속 개체 지향 패러다임으로 코딩할 수 있도록 개체 관계형 매퍼 사용을 원합니다. MSMQ(Microsoft Message Queue)와 같은 기술을 사용하면 모델의 데이터 업데이트를 게시하고 보고 및 분석을 위해 최적화된 데이터 웨어하우스에 이를 통합할 수 있습니다.

놀라운 사실로 받아들이는 사람도 있겠지만, 데이터베이스 관리자와 개발자는 사이좋게 지낼 수 있습니다. 바인딩된 컨텍스트를 통해 이 약속된 땅을 희미하게 볼 수 있습니다. 구조적인 바인딩된 컨텍스트에 관심이 있다면 Greg Young의 블로그를 방문해 보십시오. Greg은 이 방식에 대해 풍부한 경험과 지식을 가지고 있으며 풍부한 관련 콘텐츠를 제공합니다.

플라톤의 모델

아직은 시작하는 단계이므로 모델의 의미부터 확인하는 것이 좋겠습니다. 이 질문에 답하기 위해서는 형이상학적인 짧은 여행을 떠나야 하며 이러한 여행에 플라톤보다 더 좋은 안내자는 없을 것입니다.

소크라테스의 가장 유명한 제자인 플라톤은 우리가 인식하고 인지하는 개념, 사람, 장소, 그리고 사물이 단순히 진실의 그림자라고 주장했습니다. 그는 이러한 실체의 개념을 형상(Form)이라고 했습니다.

플라톤은 형상을 설명하기 위해 동굴의 비유라고 하는 이야기를 사용했습니다. 이 비유에는 깊고 어두운 동굴에 사는 사람들이 등장합니다. 이 동굴인들은 동굴 입구를 통해 들어오는 빛에 의해 동굴 벽에 비치는 모양만 볼 수 있습니다. 입구를 통해 동물이 들어오면 동굴인들은 동굴 내부로 투영되는 동물의 그림자를 봅니다. 동굴인들에게는 이러한 그림자가 실체입니다. 사자가 들어오면 이들은 사자의 그림자를 가리키며 "숨어라!"하고 소리칩니다. 그러나 이들이 가리키는 것은 실제 형상인 사자의 그림자일 뿐입니다.

플라톤의 형상 이론은 DDD와 연결할 수 있습니다. 형상 이론의 지침 중 많은 부분이 점차적으로 이상적인 모델에 접근하는 데 도움이 됩니다. 코드를 통해 설명하고자 하는 형상에 이르는 길은 도메인 전문가의 마음, 이해관계자의 욕구, 그리고 여러분이 일하고 있는 업계의 요구 사항에 분산되어 있습니다. 이러한 항목들은 플라톤이 생각한 동굴인들의 그림자라고 할 수 있습니다.

또한 형상에 도달하는 과정에서 프로그래밍 언어 및 시간과 예산의 고려 사항으로 제약을 받는 경우가 많습니다. 이러한 제약은 동굴인들이 동굴 벽에 비친 그림자만 볼 수 있다는 제약과 비슷합니다.

좋은 모델에는 구현에 관계없이 몇 가지 특성이 있습니다. 중요한 사실은 도메인 모델러라면 모든 사람들이 생각하는 모델과 여러분이 코딩하려는 모델 간의 불일치를 이해해야 한다는 것입니다.

여러분이 개발하는 소프트웨어는 실제 모델이 아니며 그림자, 즉 여러분이 달성하려는 응용 프로그램 형상의 표현일 뿐입니다. 완벽한 솔루션의 모조품이지만 점차적으로 실제 형상에 가깝게 만들 수 있습니다.

DDD에서는 이 개념을 모델 기반 설계라고 합니다. 모델에 대한 이해는 코드에서 발전됩니다. 도메인 기반 디자이너는 설명서나 복잡한 다이어그램 도구에 신경을 쓰기보다는 도메인에 대한 자신의 이해를 직접 코드로 구현할 방법을 탐구합니다

모델을 캡처하는 코드 개념이 DDD의 핵심입니다. 소프트웨어의 초점을 당면한 문제와 이 문제를 해결하는 데 두면 새로운 통찰과 개명의 순간을 받아들이는 소프트웨어를 얻게 됩니다. 필자는 지식을 모델에 갈아 넣는다고 했던 Eric Evans의 표현이 마음이 듭니다. 도메인에 대한 중요한 무엇인가를 배운다면 여러분은 어디로 가야 하는지 알게 될 것입니다.

대화의 이해

이러한 목표를 달성하기 위해 DDD에서 제공하는 몇 가지 기술을 살펴보겠습니다. 개발자로 일하다 보면 제공해야 하는 기능을 이해하기 위해 코딩과는 무관한 사람과 협조할 일이 많습니다. 프로세스를 갖춘 조직에서 일하고 있다면 사용자 스토리, 작업 또는 사용 사례 등으로 표현된 요구 사항이 있을 것입니다. 요구 사항이나 사양은 어떤 종류든 완벽한 경우는 없습니다.

일반적으로 요구 사항은 다소 정리가 되어 있지 않고 개략적인 이해로 표현됩니다. 솔루션 설계와 구현 과정에서 개발자가 대상 도메인에 대한 전문 지식을 갖춘 사람과 함께 작업할 수 있다면 도움이 됩니다. 바로 이것이 사용자 스토리의 요점이며, 일반적으로 "나는 [역할]로서 [기능]을 통해 [혜택]을 얻기를 원한다"는 형태의 템플릿으로 표현됩니다.

보험 정책 관리 도메인에 해당되는 예로 "나는 보험업자로서 정책에 대한 승인 제어 기능을 통해 안전한 노출은 서명하고 위험한 노출은 거부할 수 있기를 원한다"를 살펴보겠습니다.

이 요구 사항의 의미를 이해하실 수 있습니까? 필자는 이것이 쓰여지고 우선 순위가 할당된 것을 보았을 때 그 의미를 이해하지 못했습니다. 이렇게 추상화된 설명만으로 이를 지원하는 소프트웨어를 제공하는 데 필요한 모든 요소를 이해하기란 불가능합니다.

제대로 작성된 사용자 스토리는 이 스토리의 저자, 즉 사용자와 대화할 수 있는 초대장입니다. 이는 정책 승인/거부 기능을 다룰 때는 보험업자와 함께 작업하는 것이 이상적임을 의미합니다. 익숙하지 않은 독자를 위해 소개하자면 보험업자는 특정 노출 범주를 보험 회사가 보장해도 안전한지 여부를 결정하는 도메인 전문가입니다.

보험업자(또는 해당 프로젝트의 다른 도메인 전문가)와 기능에 대해 논의할 때는 보험업자가 사용하는 용어에 주의를 기울여야 합니다. 이러한 도메인 전문가는 회사 또는 업계 표준 용어를 사용합니다. DDD에서는 이러한 용어를 보편적 언어라고 합니다. 개발자는 이러한 용어를 이해하고 도메인 전문가와 대화할 때 이러한 용어를 사용하는 것은 물론 코드에도 같은 용어를 반영해야 합니다. 대화에 "등급 코드"나 "보험료율" 또는 "노출"과 같은 용어가 자주 등장한다면 코드에도 이에 해당하는 클래스 이름이 나와야 한다는 것입니다.

이것은 DDD의 지극히 기본적인 패턴입니다. 얼핏 보기에 보편적 언어는 당연한 것처럼 보이며, 이미 여러분 중 일부는 이러한 용어를 직관적으로 사용하고 있을 것입니다. 개발자가 코드에 비즈니스 용어를 의식적으로, 그리고 통제된 규칙에 따라 사용하는 것이 중요합니다. 이렇게 함으로써 비즈니스 용어와 기술 용어 간의 격차를 줄일 수 있습니다. "어떻게"는 "무엇"의 아래에 들어가고, 여러분은 비즈니스 가치 제공이라는 업무의 이유에 가까워질 수 있습니다.

컨텍스트

개발자는 어떤 의미에서 조직자라고 할 수 있습니다. 개발자는 문제를 해결하는 추상화로 코드를 던져넣습니다(물론 여기에는 명확한 의도가 있는 것이 좋겠지요). 디자인 패턴, 계층 아키텍처, 개체 지향 원칙과 같은 도구는 끊임없이 복잡해지고 있는 시스템에 질서를 두기 위한 프레임워크를 제공합니다.

DDD는 조직적 도구를 확대하고 잘 알려진 업계 패턴을 차용합니다. DDD가 제공하는 조직적 패턴에서 필자가 가장 마음에 드는 부분은 시스템의 모든 세부 수준에 맞는 솔루션이 있다는 것입니다. 바인딩된 컨텍스트는 소프트웨어를 모델의 포트폴리오처럼 생각할 수 있는 방법을 안내합니다. 모듈은 큰 단일 모델을 작은 조각으로 조직화하는 데 도움을 줍니다. 뒷부분에서는 몇 가지 관련 클래스 간의 소규모 공동 작업을 조직화하기 위한 기술인 집계 루트에 대해 살펴보겠습니다.

대부분의 엔터프라이즈 시스템에는 책임이 성긴 영역이 있습니다. DDD는 이러한 조직의 최상위 수준을 바인딩된 컨텍스트라고 합니다.

노동자 재해 보장 보험 정책의 경우 다음과 같은 요소를 고려해야 합니다.

  • 견적 및 영업
  • 일반 정책 워크플로(갱신, 종결)
  • 급료 추정치 감사
  • 분기별 자체 평가
  • 보험료 설정 및 관리
  • 대리점 및 중개인 수수료 지불
  • 고객 과금
  • 일반 회계
  • 수용 가능한 노출 결정(보험업)

네, 정말 많습니다. 이러한 모든 사항을 하나의 단일 시스템에 통합할 수 있지만 그렇게 하면 알아보기 어렵고 형태가 정확하지 않은 결과를 얻게 됩니다. 일반 워크플로 컨텍스트의 정책과 급료 감사 컨텍스트의 정책은 같은 정책이라도 그 내용은 완전히 다릅니다. 같은 정책 클래스를 사용한다면 해당 클래스의 프로필이 비대해지는 것은 물론이고 SRP(단일 책임 원칙)와 같은 신뢰할 수 있는 최선의 방법과 멀어지게 됩니다.

바인딩된 컨텍스트를 격리하고 차단하지 않으면 시스템은 커다란 진흙덩이(Big Ball of Mud)라는 우스꽝스런 아키텍처 스타일을 갖게 됩니다. 커다란 진흙덩이는 1999년 Brian Foot와 Joseph Yoder가 같은 이름의 논문에서 정의한 아키텍처 스타일(또는 아키텍처에 반하는 스타일)의 이름입니다.

DDD는 컨텍스트를 식별하고 특정 컨텍스트로 모델링 노력을 제한하도록 권장합니다. 컨텍스트 맵이라고 하는 간단한 다이어그램을 사용하여 시스템의 경계를 탐색할 수 있습니다. 필자는 완전한 기능을 갖춘 보험 정책 관리 시스템과 연관된 컨텍스트를 나열했으며 그림 1은 텍스트 설명을 부분적인 그래픽 컨텍스트 맵으로 만들어 보여 줍니다.

fig01.gif

그림 1 바인딩된 컨텍스트에서 컨텍스트 맵으로

다양한 바인딩된 컨텍스트 간의 몇 가지 핵심적인 관계를 볼 수 있을 것입니다. 이러한 정보는 패키징 및 배포 설계, 모델 간 메시지 마샬링에 사용할 기술 선택, 그리고 무엇보다 마일스톤을 설정하고 노력, 시간, 인력을 투입할 위치 선택과 같은 비즈니스 의사 결정과 아키텍처 의사 결정을 충분한 정보를 바탕으로 내릴 수 있도록 해 주는 귀중한 정보입니다.

바인딩된 컨텍스트에 대한 마지막으로 중요한 개념은 각 컨텍스트가 자체적인 보편적 언어를 가진다는 것입니다. 감사 하위 시스템의 정책과 핵심 워크플로의 정책은 다른 것이므로 개념을 차별화하는 것이 중요합니다. 값 개체와 자식 엔터티(자세한 내용은 조금 뒤에 설명)는 동일한 ID를 가질 수는 있지만 서로 전혀 다른 경우가 많습니다. 컨텍스트 내에서 모델링하는 것이므로 도메인 전문가와 팀 내의 구성원 간에 생산적인 의사 소통을 위해 언어가 컨텍스트 내에서 정확성을 제공하기를 원할 것입니다.

모델 내의 일부 영역은 다른 영역보다 밀접하게 그룹화됩니다. 모듈은 이러한 그룹을 특정 컨텍스트 내에 정리하기 위한 방법이며 다른 모듈과의 연결에 대해 생각해 볼 수 있는 작은 경계의 역할을 합니다. 또한 "작은 진흙덩이"를 피할 수 있는 또 다른 조직 기술이기도 합니다. 기술적으로 말해 모듈을 만들기는 쉽습니다. Microsoft .NET Framework에서 모듈은 네임스페이스입니다. 그러나 모듈을 식별하기 위해서는 코드에 조금 더 시간을 투자해야 합니다. 모델 내에서 몇 가지 항목들이 작은 모델을 이루는 경우가 있으며, 이 경우 항목을 네임스페이스로 분할하는 것을 고려할 수 있습니다.

모델을 밀착된 모듈로 분리하는 방법은 IDE에도 긍정적인 영향을 미칩니다. 즉, 모듈을 명시적으로 포함하기 위해 여러 개의 using 문을 사용해야 하므로 훨씬 깔끔한 IntelliSense 환경이 만들어집니다. 또한 NDepend와 같은 정적 분석 도구를 사용하여 보다 큰 시스템의 개념적 청크 간 연결을 볼 수 있는 방법도 제공합니다.

모델이 조직적 변경을 추가할 때는 실용적인 비용 대비 장점에 대한 고려를 해야 합니다. 모듈(또는 네임스페이스)을 사용하여 모델을 나눌 경우 별개의 컨텍스트를 다루고 있는 것인지 여부를 질문해야 합니다. 다른 컨텍스트를 분리해 내는 데 따르는 비용은 일반적으로 훨씬 더 큽니다. 분리한 후에는 모델이 두 개가 되고 어셈블리도 두 개가 될 가능성이 높습니다. 그리고 여기에 응용 프로그램 서비스, 컨트롤러 등을 연결해야 합니다.

ACL(Anti-Corruption Layer)

ACL(Anti-Corruption Layer)은 도메인에 속하지 않는 개념이 모델로 유출되지 않도록 방지하는 문지기를 만들도록 권장하는 다른 DDD 패턴입니다. 이를 통해 모델을 깨끗하게 유지할 수 있습니다.

기본적으로 리포지토리는 ACL의 한 유형입니다. 모델 외부에 SQL이나 ORM(개체 관계형 매핑) 구조를 유지합니다.

ACL은 Michael Feathers가 그의 저서 Working Effectively With Legacy Code에서 이음새라고 부르는 것을 도입하기 위해 훌륭한 기술입니다. 이음새란 기존의 일부 코드를 분리하고 변경 도입을 시작할 수 있는 영역입니다. DDD 기술을 사용하여 코드에서 가장 가치가 높은 부분을 리팩터링 및 강화할 때는 코드 도메인 격리 및 이음새 찾기가 매우 유용하게 사용됩니다.

스스로의 가치 제안 알기

대부분의 개발 업체에서 문제를 격리 및 설명할 수 있고 유지 관리가 용이한 정교한 개체 지향 솔루션을 구축할 수 있는 최고 수준의 개발자와 숙련된 비즈니스 인력은 소수입니다. 고객에게 최대한의 이익을 제공하기 위해서는 응용 프로그램의 핵심 도메인을 확실하게 이해해야 합니다. 핵심 도메인은 DDD 적용에 가장 많은 가치를 제공하는 바인딩 컨텍스트입니다.

모든 엔터프라이즈 시스템에는 다른 영역보다 더 중요한 영역이 있습니다. 이러한 중요 영역은 클라이언트의 핵심 경쟁력과 일치하는 경향이 있습니다. 기업에서 사용자 지정 범용 회계 소프트웨어를 사용하는 경우는 드뭅니다. 그러나 앞에서 설명한 예와 같이 업무가 보험이고 모든 구성원 간에 책임이 분산되는 위험 풀을 관리하여 이윤을 창출하는 기업이라면 좋지 않은 위험은 거부하고 추세를 식별하는 데 탁월한 능력을 발휘해야 합니다. 또는 클라이언트가 의료 분쟁 처리 업체이고, 이들의 전략이 지급을 자동화하여 결제 부서의 능률을 강화함으로써 가격 경쟁력을 갖추는 것일 수 있습니다.

어떤 업계든 여러분의 고용주나 클라이언트는 시장에서 나름의 강점이 있을테고, 일반적으로 사용자 지정 소프트웨어는 이러한 강점에서 발견할 수 있습니다. 대부분의 경우 이 사용자 지정 소프트웨어에서 핵심 도메인을 찾고 모델링합니다.

다른 차원, 즉 기술적 우위를 얻기 위해 지적 자산을 투자하는 곳에서 투자의 가치를 측정할 수 있습니다. 선임 개발자가 새로운 기술에 집착하는 부류의 사람들인 경우가 많습니다. 이러한 현상은 어느 정도는 당연한 것입니다. 업계에서는 빠른 속도로 혁신이 이루어지고, 공급업체는 고객의 요구에 부응하고 경쟁력을 유지하기 위해 빈번하게 새로운 기술을 출시해야 하기 때문입니다. 여기에서 과제는 선임 개발자가 시스템의 핵심에 가치를 부여할 수 있는 기본 원칙과 패턴을 완벽하게 익히는 것입니다. 새로운 프레임워크나 플랫폼을 사용하여 성급하게 작업을 완료하고 싶은 생각이 들 수도 있지만 공급업체에서 이러한 제품을 개발하는 이유는 제품을 안심하고 사용할 수 있도록 하기 위한 것임을 기억할 필요가 있습니다.

단일 책임의 시스템

앞서 필자는 DDD에서 풍부한 도메인 모델을 구성하기 위한 패턴 언어를 제공한다고 언급했습니다. 이러한 패턴을 구현하면 어느 정도 수준까지 비용을 들이지 않고도 SRP를 준수할 수 있으며 이것은 분명 상당히 유용합니다.

SRP는 인터페이스나 클래스의 핵심 목적에 도달하도록 지원하며 높은 응집성을 확보할 수 있도록 안내합니다. 높은 응집성은 코드의 검색, 재사용 및 유지 관리의 용이성을 높여 주는 매우 바람직한 특성입니다.

DDD는 패턴의 핵심 컬렉션에서 특정 유형의 클래스 책임을 식별합니다. 여기에서는 몇 가지 기본적인 부분에 대해 설명하겠습니다. 클래스 수준부터 아키텍처에 이르는 다양한 패턴에 대한 내용은 Eric Evans의 저서를 참조하십시오. 이 기사에서는 소개라는 목적에 맞게 엔터티, 값 개체, 집계 루트, 도메인 서비스 및 리포지토리를 다루는 클래스 수준에 대해서만 설명하겠습니다. 또한 각 패턴의 책임은 각각 한두 개의 코드 예와 팁을 통해 다루겠습니다.

ID와 수명을 가지는 엔터티

엔터티는 여러분의 시스템에서 "어떤 것"입니다. 사람, 장소, 그리고 물건과 같은 명사라고 생각하면 이해하기 쉽습니다.

엔터티는 ID와 수명 주기를 가집니다. 예를 들어 필자의 시스템에서 특정 고객에 액세스하려면 번호를 사용하여 고객을 지정할 수 있습니다. 계약이 완료되면 시스템에서 사용할 필요가 없게 되므로 장기 저장소(히스토리 보고 시스템)로 보낼 수 있습니다.

엔터티는 데이터 단위라기보다는 동작의 단위로 생각할 수 있습니다. 이를 소유하는 엔터티에 논리를 넣도록 해야 합니다. 대부분의 경우 모델에 추가하려는 작업을 받아야 하는 엔터티가 있거나 새로운 엔터티를 작성 또는 추출해야 합니다. 취약한 코드의 경우 엔터티 외부에서 유효성을 검사하는 서비스나 관리자 클래스를 많이 볼 수 있습니다. 일반적으로 필자는 엔터티 내에서 이와 같은 작업을 하는 것을 선호합니다. 이렇게 하면 캡슐화의 기본 원칙에서 제공되는 모든 혜택을 얻을 수 있으며 엔터티를 동작 기반으로 만들 수 있습니다.

일부 개발자는 엔터티에 종속성을 넣는 일에 신경을 씁니다. 시스템의 다양한 엔터티 간의 연결을 만들 필요는 분명히 있습니다. 예를 들어 정책의 적절한 기본값을 결정하기 위해 Policy 엔터티에서 Product 엔터티를 가져와야 하는 경우가 있습니다. 사람들이 어려움을 겪는 경우는 엔터티 내부의 작업을 수행하기 위해 일부 외부 서비스가 필요한 경우입니다.

필자라면 엔터티가 아닌 다른 클래스가 필요하다는 사실에 신경을 쓰지 않고 엔터티 외부에서 중심 동작을 가져오는 것을 피하기 위해 노력할 것입니다. 엔터티는 본질적으로 동작 단위라는 것을 기억해야 합니다. 이러한 동작은 상태 시스템의 한 종류로 구현되는 경우가 많지만(엔터티에서 명령을 호출하면 이 명령이 엔터티의 내부 상태 변경을 책임짐) 추가 데이터를 가져오거나 외부에 부수적 효과를 유발해야 하는 경우가 있습니다. 이러한 목표를 위해 필자가 선호하는 기술은 명령 메서드에 종속성을 제공하는 것입니다.

public class Policy {
  public void Renew(IAuditNotifier notifier) {
    // do a bunch of internal state-related things,
    // some validation, etc.
    ...
    // now notify the audit system that there's
    // a new policy period that needs auditing
    notifier.ScheduleAuditFor(this);
  }
}

이 방식의 장점은 엔터티를 생성하기 위해 IOC(제어 반전) 컨테이너가 필요 없다는 것입니다. 서비스 검색기를 사용하여 메서드 내에서 IAuditNotifier를 확인하는 방법도 필자가 보기에는 충분히 선택 가능한 방법입니다. 이 기술은 인터페이스를 깨끗하게 유지하는 장점이 있지만 전자의 전략을 사용하면 더 높은 수준에서 종속성에 대해 더 많은 것을 알 수 있습니다.

대상을 설명하는 값 개체

값 개체는 모델링하는 도메인의 중요한 설명자 또는 속성입니다. 엔터티와는 달리 ID를 가지지 않으며 ID를 가지는 대상을 설명하는 역할을 합니다. "35달러"라는 엔터티를 변경할까요, 아니면 잔고를 늘릴까요?

값 개체의 장점 중 하나는 훨씬 정교하며 의도가 드러나는 방식으로 엔터티의 속성을 설명한다는 것입니다. 일반적인 값 개체인 돈의 경우 십진수보다는 자금 이체 API의 함수 매개 변수가 더 적절합니다. 인터페이스나 엔터티 메서드에서 이러한 매개 변수를 보면 어떤 의미인지 즉시 알 수 있습니다.

값 개체는 변경할 수 없습니다. 일단 생성된 후에는 변경 기능이 없습니다. 변경 불가능한 것이 중요한 이유는 무엇일까요? 값 개체를 사용하면 DDD에서 가져온 다른 개념인 부수적 효과가 없는 함수를 추구할 수 있습니다. 20달러에 20달러를 더하면 20달러를 변경하는 것일까요? 아닙니다. 40달러의 새로운 돈 설명자를 만드는 것입니다. C#에서는 그림 2에 나오는 것처럼 public 필드에 readonly 키워드를 사용하여 불변성과 부수적 효과가 없는 함수를 강제할 수 있습니다.

그림 2 readonly를 사용하여 불변성 적용

public class Money {
  public readonly Currency Currency;
  public readonly decimal Amount;

  public Money(decimal amount, Currency currency) {
    Amount = amount;
    Currency = currency;
  }

  public Money AddFunds(Money fundsToAdd) {
    // because the money we're adding might
    // be in a different currency, we'll service 
    // locate a money exchange Domain Service.
    var exchange = ServiceLocator.Find<IMoneyExchange>();
    var normalizedMoney = exchange.CurrentValueFor(fundsToAdd,         this.Currency);
    var newAmount = this.Amount + normalizedMoney.Amount;
    return new Money(newAmount, this.Currency);
  }
}

public enum Currency {
  USD,
  GBP,
  EUR,
  JPY
}

엔터티를 결합하는 집계 루트

집계 루트는 소비자가 직접 참조하는 특별한 종류의 엔터티입니다. 집계 루트를 식별하면 몇 가지 간단한 규칙을 적용함으로써 모델을 구성하는 개체의 과도한 연결을 방지할 수 있습니다. 집계 루트는 하위 엔터티를 적극적으로 보호합니다.

가장 큰 원칙은 집계 루트가 소프트웨어에서 참조를 저장할 수 있는 유일한 엔터티 종류라는 것입니다. 이를 통해 모든 요소가 상호 연결되는 밀결합 시스템을 방지하는 제약 조건을 확보하므로 커다란 진흙덩이가 되는 것을 차단할 수 있습니다.

Policy라는 엔터티가 있다고 가정해 보겠습니다. 정책은 매년 갱신되므로 Period라는 엔터티도 있을 것입니다. Period는 Policy 없이는 존재할 수 없고, Policy를 통해 Period에 작업을 수행할 수 있기 때문에 Policy는 집계 루트라고 할 수 있으며 Period는 이러한 항목의 자식입니다.

필자는 집계 루트가 스스로 작업을 처리하는 방식을 선호합니다. Policy 집계 루트에 액세스하는 다음과 같은 소비자 코드를 살펴보겠습니다.

Policy.CurrentPeriod().Renew() 

여기에서는 보험 정책을 갱신하려고 합니다. 보험 정책 관리의 핵심 도메인에 대한 클래스 다이어그램을 다시 떠올려 보십시오. 호출하려는 동작에 어떻게 연결하는지 확인하십시오.

이 방법에는 두 가지 문제가 있습니다. 첫째, 여기에서는 명백하게 데메테르의 법칙을 위반하고 있습니다. 개체 O의 메서드 M은 자체, 매개 변수, 생성 또는 인스턴스화하는 개체 또는 직접 구성 요소 개체에 해당하는 개체의 메서드만 호출할 수 있습니다.

이러한 본격적인 연결 기능이 편리하지 않을까요? IntelliSense는 Visual Studio와 최신 IDE의 훌륭하고 유용한 기능입니다. 그러나 호출하려는 함수를 연결하는 경로를 연결하면 시스템에 불필요한 결합이 추가됩니다. 이전 예의 경우에는 Policy 클래스와 Period 클래스에 의존하는 것입니다.

이와 관련된 보다 자세한 의미, 이론, 도구 및 유의할 사항에 대해서는 데메테르의 법칙에 대한 Brad Appleton의 기사를 읽어 보십시오.

과도하게 결합된 시스템에서 발생할 수 있는 관리상의 어려움은 흔한 말로 "고문"과도 같습니다. 여기저기에 불필요한 참조를 만들면 한곳에서 변경을 수행할 때 모든 소비자 코드에 걸쳐 연쇄적인 변경이 일어나는 융통성 없는 모델이 만들어집니다. 다음과 같이 나타내는 것이 분명한 간단한 코드로 같은 목표를 달성할 수 있습니다.

Policy.Renew()

집계가 어떻게 방법을 알아내는지 보이십니까? 집계는 내부적으로 현재 기간과 새로운 기간이 이미 있는지 여부, 그리고 필요한 다른 사항을 알아낼 수 있습니다.

BDD(Behavior Driven Development)와 같은 기술을 사용하여 집계 루트를 단위 테스트하면 테스트가 블랙박스 및 상태 테스트 패러다임에 가까워지는 경향이 있습니다. 집계 루트와 엔터티는 최종적으로 상태 시스템이 되는 경우가 많으며 동작은 적절하게 일치합니다. 최종적으로 상태 유효성 검사, 더하기 및 빼기가 됩니다. 그림 3에 있는 갱신 예를 보면 상당히 많은 동작이 수행되고 있으며 이를 BDD 스타일의 테스트로 나타내는 방법은 쉽게 알 수 있습니다.

그림 3 집계 루트 테스트

public class 
  When_renewing_an_active_policy_that_needs_renewal {

  Policy ThePolicy;
  DateTime OriginalEndingOn;

  [SetUp]
  public void Context() {
    ThePolicy = new Policy(new DateTime(1/1/2009));
    var somePayroll = new CompanyPayroll();
    ThePolicy.Covers(somePayroll);
    ThePolicy.Write();
    OriginalEndingOn = ThePolicy.EndingOn;
  }

  [Test]
  public void Should_create_a_new_period() { 
    ThePolicy.EndingOn.ShouldEqual(OriginalEndingOn.AddYears(1));
  }
}

도메인 서비스 모델 기본 작업

도메인에 ID나 수명 주기가 없는 작업이나 프로세스가 있는 경우가 있습니다. 도메인 서비스는 이러한 개념을 모델링하는 도구를 제공합니다. 이들은 일반적으로 상태를 저장하지 않고 응집성이 매우 높으며 단일 공용 메서드를 제공하는 경우가 많고 집합에 대해 작업을 수행하는 오버로드를 제공하는 경우도 있습니다.

필자는 몇 가지 이유 때문에 서비스를 사용하는 것을 선호합니다. 필자는 동작에 여러 종속성이 연관되어 있고 엔터티에서 해당 동작을 배치할 자연스러운 위치를 찾을 수 없는 경우 서비스를 사용합니다. 보편적 언어가 1차 개념으로서 프로세스나 작업에 대해 이야기하는 경우에는 서비스가 모델 조율의 중심점으로 적당한지를 질문합니다.

갱신의 경우에는 도메인 서비스를 사용할 수 있습니다. 이것은 대안 스타일입니다. Policy 엔터티의 Renew 메서드의 메서드로 직접 IAuditNotifier를 주입하는 것이 아니라 도메인 서비스를 추출하도록 선택하여 종속성 확인을 처리할 수 있습니다. 엔터티보다는 IOC 컨테이너에서 도메인 서비스를 확인하는 것이 더 자연스럽습니다. 여러 종속성이 있는 경우에는 이 전략이 타당성이 있지만 대안을 소개하겠습니다.

다음은 도메인 서비스의 간략한 예입니다.

public class PolicyRenewalProcesor {
  private readonly IAuditNotifier _notifier;

  public PolicyRenewalProcessor(IAuditNotifier notifier) {
    _notifier = notifier;
  }
  public void Renew(Policy policy) {
    policy.Renew();
    _notifier.ScheduleAuditFor(policy);
  }
}

서비스라는 단어는 개발자의 세계에서 과용되는 측면이 있습니다. SOA(서비스 지향 아키텍처)에서의 서비스를 떠올리는 경우도 있지만 응용 프로그램에서 특정 인물, 장소 또는 대상을 나타내지는 않지만 종종 프로세스를 구체화하는 작은 클래스를 서비스로 생각하는 경우도 있습니다. 도메인 서비스는 일반적으로 후자의 범주에 해당하며 도메인 전문가가 보편적 언어로 전달하는 동사나 비즈니스 작업에 따라 명명됩니다.

반면에 응용 프로그램 서비스는 계층 아키텍처를 도입하는 훌륭한 방법입니다. 클라이언트 응용 프로그램에 필요한 형태로 도메인 모델 내부로 데이터를 매핑하는 데 사용할 수 있습니다. 예를 들어 DataGrid에 표 형식 데이터를 표시해야 하지만 모델의 세분화되고 거친 개체 그래프를 유지하고 싶은 경우가 있습니다.

응용 프로그램 서비스는 예를 들어 정책 감사와 핵심 정책 워크플로 사이의 변환과 같이 여러 모델을 통합하는 데도 상당히 유용합니다. 이와 비슷하게 인프라 종속성을 혼합하는 데도 이를 사용합니다. WCF(Windows Communication Foundation)를 사용하여 도메인 모델을 공개하는 일반적인 시나리오를 가정해 보겠습니다. 필자의 순수한 도메인 모델에 WCF가 유출되도록 하기보다는 WCF 특성을 지정한 응용 프로그램 서비스를 사용하여 이러한 시나리오를 구현할 수 있습니다.

응용 프로그램 서비스는 광범위하고 단순한 경향이 있으며 응집성 있는 기능을 구체화합니다. 그림 4에 나오는 인터페이스와 부분적인 구현을 응용 프로그램 서비스의 좋은 예로 생각할 수 있습니다.

그림 4 간단한 응용 프로그램 서비스

public IPolicyService {
  void Renew(PolicyRenewalDTO renewal);
  void Terminate(PolicyTerminationDTO termination);
  void Write(QuoteDTO quote);
}

public PolicyService : Service {
  private readonly ILogger _logger;
  public PolicyService(ILogger logger, IPolicyRepository policies) {
    _logger = logger;
    _policies = policies;
  }

  public void Renew(PolicyRenewalDTO renewal) {
    var policy = _policies.Find(renewal.PolicyID);
    policy.Renew();
    var logMessage = string.Format(
      "Policy {0} was successfully renewed by {1}.", 
      Policy.Number, renewal.RequestedBy);
    _logger.Log(logMessage);
  }
}

리포지토리 저장 및 집계 루트 분배

엔터티를 검색하려면 어디로 가야 할까요? 그리고 이를 저장하려면 어떻게 해야 할까요? 리포지토리 패턴이 이러한 질문에 대한 답이 될 수 있습니다. 리포지토리는 메모리 내 컬렉션을 나타내며 최종적으로 집계 루트당 하나의 리포지토리를 사용하는 것이 적당하다는 것을 짐작할 수 있을 것입니다.

리포지토리는 슈퍼 클래스 또는 Martin Fowler가 Layer Supertype 패턴이라고 지칭한 것의 좋은 후보입니다. 이전 예에서는 간단하게 제네릭을 사용하여 기본 리포지토리 인터페이스에서 파생할 수 있습니다.

public interface IRepository<T>
  where T : IEntity
{
  Find<T>(int id);
  Find<T>(Query<T> query);
  Save(T entity);
  Delete(T entity);
}

리포지토리는 SQL 문이나 저장 프로시저와 같은 데이터베이스나 지속성 개념이 모델과 혼합되고 도메인 캡처라는 당면한 임무를 방해하지 않도록 방지합니다. 이러한 인프라로부터의 모델 코드 분리는 바람직한 특성입니다. 보다 자세한 논의는 "ACL(Anti-Corruption Layer)" 보충 기사를 참조하십시오.

지금쯤 아마 독자 여러분은 필자가 집계 루트, 하위 엔터티 및 연결된 값 개체가 어떻게 디스크에 유지되는지에 대한 이야기를 하는 것이 아님을 눈치챘을 것입니다. 이는 의도적인 것입니다. 모델에서 동작을 수행하는 데 필요한 데이터를 저장하는 것은 모델 자체와 직접적으로 엮이는 사항입니다. 지속성은 인프라입니다.

이러한 기술에 대한 설명은 DDD를 소개하는 이 기사의 범위를 많이 벗어납니다. 모델의 데이터를 저장하기 위한 옵션에는 ORM(개체 관계형 매핑) 프레임워크부터 간단한 시나리오에서 "직접 해결하는" 데이터 매퍼를 위한 문서 지향 데이터베이스에 이르기까지 적합하고 발전된 옵션이 많다는 정도만 알아 두십시오.

DDD 리소스

DDD 공식 사이트

Dan North - 사용자 스토리 작성 방법

커다란 진흙덩이 아키텍처 스타일

Greg Young 블로그 - CodeBetter

단일 책임 원칙에 대한 Robert C. Martin의 논문

Brad Appleton - 데메테르의 법칙에 대한 소개

Martin Fowler - 계층 슈퍼 형식 패턴에 대한 설명

Robert C. Martin - S.O.L.I.D. 원칙에 대한 설명

데이터베이스에 대한 사항

아마도 지금쯤에는 엔터티를 저장하는 방법이 궁금할 것입니다. 물론 이 중요한 사항은 반드시 처리해야 하지만 모델을 지속하는 방법이나 위치는 DDD 개요와는 큰 관련이 없습니다.

많은 개발자와 데이터베이스 관리자는 데이터베이스가 모델이라고 가정하는 경우가 많습니다. 그리고 부분적으로는 이것이 사실입니다. 데이터베이스를 높은 수준으로 정규화하고 다이어그램 도구를 사용하여 시각화하면 도메인의 정보와 관계에 대한 많은 내용을 전달할 수 있습니다.

그러나 기본 기술로서의 데이터 모델링에는 아쉬운 부분이 있습니다. 같은 도메인의 기본적인 동작을 이해하려는 경우 ERD(엔터티 관계 다이어그램)나 엔터티 관계 모델 및 클래스 다이어그램과 같이 데이터만 사용하는 기법으로는 결과를 얻을 수 없습니다. 응용 프로그램 부분이 작동하는 방법과 작업을 수행하기 위해 형식이 공동 작업하는 방법을 볼 수 있어야 합니다.

필자는 모델링할 때 의사 전달 도구로 화이트보드에 시퀀스 다이어그램을 사용하는 경우가 많습니다. 이렇게 하면 UML(Unified Modeling Language)이나 모델 기반 아키텍처와 같은 복잡한 과정 없이도 동작 설계나 문제에 대한 의사 전달의 핵심을 대부분 충족할 수 있습니다. 화이트보드에 그린 다이어그램을 곧 지울 계획이라면 굳이 복잡한 과정을 거칠 필요가 없습니다. 이러한 다이어그램은 UML을 완벽하게 준수할 필요가 없으며 필자는 빠르게 그릴 수 있는 간단한 상자, 화살표, 물결선을 선호합니다.

아직 팀에서 시퀀스 다이어그램을 사용하고 있지 않다면 이 기술을 배울 것을 권장합니다. 이 기술을 활용하면 팀 구성원들이 SRP, 계층 아키텍처, 데이터 모델링에서의 동작 설계, 그리고 전반적인 아키텍처 고려 사항에 이르기까지 문제점을 충분히 생각하고 해결하는 데 상당히 도움이 됩니다.

DDD 시작하기

개체 지향 프로그래밍에 익숙해지는 것은 절대 쉬운 과정이 아닙니다. 전문 개발자라면 대부분 자질을 갖추고 있겠지만 노력과 책을 통한 학습, 그리고 반복적인 연습이 필요합니다. 또한 장인 정신과 끊임없는 배움의 자세를 갖추고 있다면 도움이 됩니다.

그러면 어떻게 시작해야 할까요. 간단히 말해 여러분이 해야 할 일을 하십시오. S.O.L.I.D. 원칙과 같은 내용을 배우고 Eric Evans의 서적으로 공부하십시오. 이러한 시간 투자의 대가는 충분할 것입니다. InfoQ에서는 몇 가지 핵심 개념을 소개하는, 좀더 작은 범위의 DDD 서적을 출간했습니다. 예산이 충분하지 않거나 아직 더 알아보고자 하는 경우에는 이 책부터 시작하기를 권장합니다. 기반 지식을 갖춘 다음에는 Yahoo! DDD 그룹에서 동료 디자이너가 겪고 있는 문제에 대해 알아보고 대화에 참여하십시오.

DDD는 새로운 원칙이나 방법론이 아니며 오랫동안 증명된 전략의 모음입니다. 연습을 시작할 준비가 되면 여러분의 상황에 가장 적합한 원칙, 기술 및 패턴부터 적용해 보십시오. DDD의 일부 요소는 다른 요소에 비해 더 보편적으로 적용됩니다. 보편적 언어를 알아내고 사용하여 핵심 도메인의 존재 이유를 이해하고 모델링하는 컨텍스트를 식별하는 일은 완전히 불투명하고 천편일률적인 리포지토리를 고정하는 것보다 훨씬 더 중요한 일입니다.

솔루션을 설계할 때는 가치를 높이는 설계를 하십시오. 디자이너가 예술을 하는 사람이고 개발자가 일종의 디자이너라고 한다면 우리의 매체는 비즈니스 가치가 되어야 합니다. 원칙의 준수나 지속성 기술의 선택과 같은 고려 사항도 때로는 중요해 보이지만 가치에 대한 인식은 이러한 사항보다 더 중요합니다.

Dave Laribee는 VersionOne에서 제품 개발 팀을 지도하고 있습니다. 그는 지역 및 국내 개발자 행사에 자주 강연자로 참여하고 있으며 2007년과 2008년에는 Microsoft Architecture MVP로 선정되기도 했습니다. 또한 thebeelog.com의 CodeBetter 블로그 네트워크에서 블로그를 운영하고 있습니다.