도메인 주도 설계(Domain-Driven Design, DDD)는 복잡한 소프트웨어의 설계와 개발에 사용되는 접근 방식이다. 이 방법론은 에릭 에반스(Eric Evans)가 2003년 저술한 동명의 책을 통해 체계화되었다. DDD의 핵심은 소프트웨어의 구조와 언어가 비즈니스 도메인의 복잡성에 직접적으로 대응하도록 하는 것이다.
주요 목표는 소프트웨어 개발자와 도메인 전문가 사이의 효과적인 협업을 촉진하고, 변화하는 비즈니스 요구사항에 유연하게 대응할 수 있는 유지보수성이 높은 시스템을 만드는 것이다. 이를 위해 도메인 지식에 초점을 맞추고, 해당 지식을 코드와 설계에 반영하는 데 중점을 둔다.
DDD는 크게 전략적 설계와 전술적 설계라는 두 가지 차원으로 구성된다. 전략적 설계는 시스템의 큰 그림을 다루며, 제한된 컨텍스트와 유비쿼터스 언어 같은 개념을 통해 도메인을 이해하고 경계를 정의한다. 전술적 설계는 각 컨텍스트 내부에서 구현 세부사항을 다루며, 엔티티, 값 객체, 애그리게이트 같은 패턴을 사용해 풍부한 도메인 모델을 구축한다.
이 접근법은 특히 규모가 크고 복잡한 비즈니스 로직을 가진 엔터프라이즈 애플리케이션 개발에 적합하다. 반면, 비교적 단순한 CRUD(Create, Read, Update, Delete) 위주의 애플리케이션에는 과도한 설계가 될 수 있다는 지적도 있다.
도메인 주도 설계의 핵심 개념은 복잡한 소프트웨어를 도메인 전문가의 지식과 언어에 맞춰 설계하는 데 필요한 기본적인 구성 요소와 원칙을 정의한다. 이 개념들은 소프트웨어의 구조와 의사소통 방식을 도메인의 복잡성에 맞게 정렬하는 데 초점을 맞춘다.
첫 번째 핵심 개념은 도메인과 하위 도메인이다. 도메인은 소프트웨어가 해결하려는 문제 영역, 즉 비즈니스나 사용자의 활동 영역을 의미한다. 예를 들어, 은행 시스템의 도메인은 '금융'이다. 이렇게 넓은 도메인은 더 관리하기 쉬운 단위로 나눌 수 있으며, 이를 하위 도메인이라고 한다. 하위 도메인은 핵심, 지원, 일반의 세 가지 유형으로 분류된다. 핵심 하위 도메인은 비즈니스의 경쟁력을 결정짓는 고유한 영역이며, 지원 하위 도메인은 핵심을 돕는 보조 영역, 일반 하위 도메인은 표준화된 솔루션을 적용할 수 있는 영역이다[1].
두 번째 개념은 유비쿼터스 언어이다. 이는 도메인 전문가와 개발 팀이 프로젝트 전반에 걸쳐 공유하는 정확하고 모호함이 없는 언어 체계이다. 유비쿼터스 언어는 설계 문서, 코드, 대화에서 일관되게 사용되며, 도메인 모델 자체가 이 언어를 표현한다. 이를 통해 의사소통의 오류를 줄이고, 소프트웨어 모델이 비즈니스 요구사항을 정확히 반영하도록 보장한다.
세 번째 핵심 개념은 제한된 컨텍스트이다. 하나의 거대한 모델로 복잡한 도메인 전체를 표현하는 것은 불가능하며, 모델의 경계가 모호해질 수 있다. 제한된 컨텍스트는 특정 하위 도메인 내에서 유비쿼터스 언어가 적용되는 명시적인 경계를 설정한다. 각 제한된 컨텍스트는 독립적인 모델을 가지며, 다른 컨텍스트와는 잘 정의된 인터페이스를 통해 통신한다. 이는 시스템을 응집력 높은 모듈로 분해하는 전략적 설계의 핵심 도구이다.
개념 | 정의 | 주요 목적 |
|---|---|---|
소프트웨어가 다루는 문제 영역과 그 세부 구분 | 복잡한 비즈니스를 이해하고, 개발 우선순위를 설정하는 데 도움을 줌 | |
도메인 전문가와 개발 팀이 공유하는 일관된 언어 | 의사소통 장벽을 해소하고, 모델과 코드의 명확성을 높임 | |
특정 모델과 언어가 적용되는 명시적인 경계 | 모델의 응집력을 유지하고, 시스템을 관리 가능한 단위로 분리함 |
도메인 주도 설계에서 도메인은 소프트웨어가 해결하려는 특정 비즈니스 분야나 문제 영역을 가리킨다. 예를 들어, 은행 시스템의 도메인은 '금융'이며, 전자상거래 시스템의 도메인은 '판매'와 '고객 관리'가 될 수 있다. 도메인은 단순히 기술적인 기능의 집합이 아니라, 해당 비즈니스의 규칙, 프로세스, 전문 지식이 담겨 있는 핵심 영역이다.
복잡한 하나의 도메인은 보다 작고 관리 가능한 단위인 하위 도메인으로 분해될 수 있다. 하위 도메인은 도메인 내에서 특정한 책임이나 기능을 담당하는 부분 영역이다. 일반적으로 하위 도메인은 다음과 같은 세 가지 유형으로 분류된다.
유형 | 설명 | 예시 (전자상거래 시스템) |
|---|---|---|
핵심 하위 도메인 | 비즈니스의 경쟁 우위를 결정짓는 고유한 영역. 가장 많은 투자와 관심이 필요하다. | 상품 추천 알고리즘, 결제 프로세스 |
지원 하위 도메인 | 비즈니스 운영을 지원하지만 핵심은 아닌 영역. 외부 솔루션 구매나 단순화된 구현이 가능하다. | 고객 지원 포털, 배송 추적 시스템 |
일반 하위 도이민 | 업계에서 널리 통용되는 표준화된 솔루션이 존재하는 영역. 직접 개발보다는 구매나 오픈소스 활용이 효율적이다. | 사용자 인증, 결제 게이트웨이 연동 |
도메인을 하위 도메인으로 식별하고 분류하는 것은 설계의 첫걸음이다. 이 과정을 통해 팀은 어디에 집중해야 하는지(핵심 하위 도메인)와 어디는 표준 솔루션을 적용할 수 있는지(일반 하위 도메인)를 명확히 이해하게 된다. 각 하위 도메인은 이후 서로 다른 제한된 컨텍스트로 발전하여 독립적으로 모델링되고 구현되는 경우가 많다.
유비쿼터스 언어는 특정 도메인 내에서 모든 이해관계자들이 공통으로 사용하는 정제되고 통일된 언어 체계를 의미한다. 이 언어는 도메인 전문가와 개발 팀 사이의 의사소통 장벽을 해소하기 위해 만들어지며, 도메인에 대한 논의, 설계 문서, 코드, 심지어 테스트 케이스에 이르기까지 모든 곳에서 일관되게 사용된다. 유비쿼터스 언어의 핵심 목표는 도메인 지식을 명확하고 모호함 없이 표현하여 소프트웨어 모델이 실제 비즈니스 개념을 정확하게 반영하도록 하는 것이다.
유비쿼터스 언어는 자연스럽게 형성되지 않으며, 지속적인 협업과 리팩토링을 통해 발전한다. 도메인 전문가와 개발자는 도메인에 대한 대화를 통해 핵심 개념, 규칙, 프로세스를 정의하고, 이를 정확한 용어와 문구로 정리한다. 이 과정에서 발견된 모호하거나 중복된 용어는 제거되거나 명확히 정의된다. 예를 들어, '주문'이라는 개념이 'Order', 'Purchase', 'Transaction' 등 여러 용어로 혼용된다면, 그 중 가장 정확한 하나를 선택하고 그 의미와 사용 조건을 명시한다.
이 언어는 단순한 용어집을 넘어서, 도메인의 복잡한 규칙과 관계를 표현하는 문장 구조까지 포함한다. "주문은 최소 한 개 이상의 품목을 가져야 한다" 또는 "확정된 주문은 취소될 수 없다"와 같은 비즈니스 규칙은 유비쿼터스 언어의 일부가 되어 코드의 도메인 모델에 직접 반영된다. 결과적으로 소프트웨어의 클래스, 메서드, 변수의 이름은 모두 이 공통 언어에서 비롯되며, 이는 코드를 읽는 것만으로도 비즈니스 로직을 이해할 수 있게 만든다.
구성 요소 | 설명 |
|---|---|
용어(Term) | |
규칙(Rule) | 도메인의 제약 조건이나 비즈니스 로직을 표현하는 문장 (예: "주문 금액이 5만 원 이상이면 배송비가 무료이다") |
컨텍스트(Context) | 용어가 사용되는 특정 제한된 컨텍스트를 명시하여 의미의 혼란을 방지함 |
유비쿼터스 언어를 효과적으로 구축하고 유지하려면, 이를 문서화하고 팀원 모두가 쉽게 접근할 수 있도록 해야 한다. 또한 언어는 고정된 것이 아니라 도메인에 대한 이해가 깊어짐에 따라 진화하며, 이에 맞춰 설계와 코드도 함께 리팩토링되어야 한다. 이렇게 함으로써 소프트웨어는 항상 최신의 도메인 지식을 반영하는 살아있는 모델이 된다.
제한된 컨텍스트는 도메인 주도 설계에서 특정 하위 도메인 내에서 유비쿼터스 언어가 명확한 의미와 경계를 가지는 구체적인 경계를 정의한다. 복잡한 소프트웨어 시스템은 하나의 통일된 모델로 표현하기 어려운 여러 하위 도메인으로 구성된다. 제한된 컨텍스트는 이러한 각 하위 도메인에 대해 명확한 경계를 설정하여, 그 안에서만 모델이 일관되고 모호하지 않게 유지되도록 한다.
예를 들어, 전자상거래 시스템에서 '상품'이라는 용어는 재고 관리 컨텍스트와 카탈로그 컨텍스트에서 서로 다른 의미와 속성을 가질 수 있다. 재고 관리 컨텍스트에서는 SKU, 재고 수량, 창고 위치 등이 핵심 속성이다. 반면, 카탈로그 컨텍스트에서는 상품명, 설명, 가격, 이미지 URL 등이 중요하다. 하나의 거대한 '상품' 모델로 두 관심사를 모두 포함하면 모델이 복잡해지고 유지보수가 어려워진다. 따라서 두 개의 별도 제한된 컨텍스트를 정의하고, 각 컨텍스트 내에서 '상품'에 대한 고유한 모델을 발전시키는 것이 효과적이다.
제한된 컨텍스트 간의 통신은 명시적인 메커니즘을 통해 이루어진다. 가장 일반적인 통합 방식은 다음과 같다.
통합 방식 | 설명 | 예시 |
|---|---|---|
공유 커널 | 두 컨텍스트가 하나의 공통 모델(도메인 모델의 일부)을 공유한다. | 주문 컨텍스트와 결제 컨텍스트가 공통의 '고객' 모델을 공유할 수 있다. |
소비자/공급자 | 한 컨텍스트(공급자)가 다른 컨텍스트(소비자)가 사용할 API를 제공한다. | 배송 컨텍스트가 주문 컨텍스트로부터 배송 정보를 API로 수신한다. |
순응자 | 하류 컨텍스트가 상류 컨텍스트의 모델을 자체적으로 변형하여 따르는 방식이다. | 별도의 팀이 개발한 외부 결제 시스템의 모델에 자체 모델을 맞춘다. |
분리된 통로 | 컨텍스트 간에 직접적인 통합을 하지 않고, 필요시 수동으로 데이터를 동기화한다. | 매우 독립적인 두 시스템이 배치 작업을 통해 간헐적으로 데이터를 교환한다. |
개방형 호스트 서비스 | 일관된 프로토콜(일반적으로 REST API)을 통해 시스템을 외부에 서비스로 공개한다. | 여러 외부 파트너사가 표준화된 API를 통해 상품 정보를 조회한다. |
이러한 경계를 명확히 함으로써 시스템의 복잡성을 관리 가능한 단위로 분해하고, 각 부분의 독립적인 개발과 진화를 가능하게 한다. 제한된 컨텍스트는 대규모 팀 조직과도 밀접한 관련이 있으며, 하나의 팀이 하나의 제한된 컨텍스트를 책임지는 것이 이상적인 구조로 간주된다[2].
전략적 설계는 도메인 주도 설계에서 복잡한 비즈니스 도메인을 이해하고, 그 경계를 식별하며, 핵심 구성 요소를 정의하는 상위 수준의 접근법이다. 이는 소프트웨어의 구조를 비즈니스의 실제 구조와 일치시키는 데 초점을 맞춘다. 전략적 설계의 주요 산출물은 제한된 컨텍스트의 명확한 식별과 그들 사이의 관계 맵핑이며, 이를 통해 대규모 시스템의 복잡성을 관리 가능한 단위로 분해한다.
전략적 설계의 핵심 구성 요소는 엔티티, 값 객체, 애그리게이트이다. 엔티티는 고유한 식별자를 가지며 시간에 따라 상태가 변할 수 있는 객체이다. 예를 들어, '주문'이나 '고객'은 각각 고유한 ID를 가지며 상태가 변경되므로 엔티티로 모델링된다. 반면 값 객체는 식별자가 없으며 속성값으로 정의되는 불변 객체이다. '금액'이나 '주소'는 그 값이 동일하면 서로 교체 가능한 값 객체의 전형적인 예이다.
애그리게이트는 연관된 엔티티와 값 객체들을 하나의 군집으로 묶은 경계를 정의한다. 애그리게이트는 데이터 변경의 일관성 단위가 되며, 하나의 루트 엔티티를 통해 외부와 상호작용한다. 예를 들어, '주문' 애그리게이트는 주문 루트 엔티티와 여러 '주문 항목' 값 객체들을 포함할 수 있으며, 주문 항목에 대한 직접적인 접근은 루트를 통해서만 이루어진다. 이는 복잡한 관계 속에서도 도메인 불변식을 보장하는 데 핵심적인 역할을 한다.
개념 | 핵심 특징 | 주요 목적 | 예시 |
|---|---|---|---|
고유 식별자, 생명주기, 가변 상태 | 비즈니스에서 추적 가능한 객체 모델링 | 고객(ID: C001), 주문(ID: O1001) | |
불변성, 식별자 없음, 속성 기반 동등성 | 개념적으로 하나인 값을 모델링하여 부작용 방지 | 금액(1000원, KRW), 주소(서울시 강남구) | |
일관성 경계, 루트 엔티티, 불변식 집합 | 관련 객체 군집을 하나의 단위로 관리하여 복잡성 제어 | 주문(루트)과 그에 속한 주문 항목들 |
이러한 구성 요소 외에도, 특정 작업이나 행위가 단일 엔티티나 값 객체에 속하지 않을 때 도메인 서비스가 사용된다. 도메인 서비스는 상태를 가지지 않으며, 순수한 도메인 로직을 캡슐화한다. '이체 서비스'는 '출금 계좌'와 '입금 계좌' 두 엔티티를 조정하여 이체라는 도메인 작업을 수행하는 전형적인 예이다. 전략적 설계는 이러한 개념들을 활용하여 도메인 전문가와 개발자가 공유하는 유비쿼터스 언어를 기반으로 모델을 구축하고, 시스템의 전반적인 구조를 결정한다.
엔티티는 도메인 모델 내에서 고유한 식별성을 가지는 객체이다. 엔티티의 생명주기 동안 속성은 변경될 수 있지만, 그 정체성을 정의하는 식별자는 변하지 않는다. 예를 들어, '고객' 엔티티는 이름이나 주소가 바뀌더라도 고객 번호라는 식별자로 동일한 객체임을 추적한다. 이 식별성은 두 엔티티의 모든 속성 값이 동일하더라도 서로 다른 존재로 구분하는 근간이 된다.
반면, 값 객체는 식별성보다는 그 자체의 속성에 의해 정의되는 객체이다. 값 객체는 불변성을 가지는 것이 일반적이며, 속성 값이 같으면 서로 교환 가능한 것으로 간주한다. '주소'나 '금액'과 같은 개념이 값 객체의 전형적인 예이다. 주소는 도로명, 번지, 우편번호로 구성되며, 이 값들이 모두 같다면 두 주소는 논리적으로 동일한 것으로 판단한다. 값 객체는 엔티티의 속성으로 포함되거나, 다른 값 객체의 일부가 될 수 있다.
두 개념의 차이는 아래 표로 정리할 수 있다.
특성 | 엔티티 | 값 객체 |
|---|---|---|
식별성 | 고유 식별자(예: ID)에 의해 구분됨 | 속성 값의 조합에 의해 정의됨 |
동등성 비교 | 식별자가 같으면 동일한 객체 | 속성 값이 모두 같으면 동일한 객체 |
생명주기 | 지속적이며 상태가 변할 수 있음 | 불변성이 일반적이며, 필요시 새로 생성됨 |
예시 | 고객, 주문, 상품 | 주소, 금액, 날짜 범위 |
설계 시 엔티티와 값 객체를 올바르게 구분하는 것은 모델의 명확성과 무결성을 높이는 데 중요하다. 값 객체를 적극적으로 사용하면 불필요한 식별자 관리 부담을 줄이고, 도메인의 개념을 더 순수하게 표현할 수 있다. 예를 들어, '송장 주소'를 별도의 엔티티가 아닌 값 객체로 모델링하면, 주소 자체의 변경은 새로운 값 객체를 생성함으로써 처리하고, 식별자를 관리할 필요가 없어진다.
애그리게이트는 도메인 주도 설계에서 제한된 컨텍스트 내의 엔티티와 값 객체를 하나의 군집으로 묶는 단위이다. 이는 복잡한 도메인 모델 내에서 데이터 변경의 일관성을 보장하고, 모델의 경계를 명확히 정의하는 데 사용된다. 애그리게이트는 하나의 루트 엔티티를 가지며, 이 루트를 통해서만 내부 객체에 접근하고 상태를 변경할 수 있다.
애그리게이트의 주요 규칙은 다음과 같다.
* 루트 엔티티: 각 애그리게이트는 단 하나의 루트 엔티티를 가진다. 이 루트는 고유한 식별자를 가지며, 애그리게이트의 대표자 역할을 한다.
* 일관성 경계: 애그리게이트는 트랜잭션 일관성을 보장해야 하는 범위를 정의한다. 애그리게이트 내부의 모든 불변식은 단일 트랜잭션 내에서 항상 만족되어야 한다.
* 참조 규칙: 외부 객체는 애그리게이트의 루트에만 직접 참조를 가질 수 있다. 내부의 다른 엔티티나 값 객체는 루트를 통해서만 접근 가능하다. 애그리게이트 간 참조는 루트의 식별자를 사용하는 것이 일반적이다.
애그리게이트를 설계할 때는 가능한 한 작게 유지하는 것이 좋다. 이는 트랜잭션 충돌을 줄이고 시스템의 성능과 확장성을 높이는 데 도움이 된다. 예를 들어, '주문' 애그리게이트는 '주문' 루트 엔티티와 '주문 항목' 값 객체들을 포함할 수 있지만, '고객'이나 '상품' 정보는 별도의 애그리게이트로 분리하고 식별자로 참조한다.
도메인 서비스는 엔티티나 값 객체에 속하기 어려운 도메인 로직이나 작업을 캡슐화하는 객체입니다. 이는 특정 애그리게이트에 자연스럽게 속하지 않거나, 여러 애그리게이트를 조정해야 하는 복잡한 비즈니스 규칙을 표현할 때 사용됩니다. 도메인 서비스의 핵심은 상태를 가지지 않고(무상태), 순수한 도메인 로직만을 제공하는 데 있다[3].
주요 사용 사례는 다음과 같습니다.
* 여러 애그리게이트에 걸친 로직: 예를 들어, 한 은행 계좌(애그리게이트)에서 다른 계좌로 자금을 이체하는 작업은 출금과 입금이라는 두 개의 별도 애그리게이트를 변경하고, 일관성 규칙을 적용해야 합니다. 이 로직은 '이체 서비스'라는 도메인 서비스에 위치하는 것이 적합합니다.
* 외부 시스템과의 상호작용을 추상화: 도메인 관점에서 필요한 계산(예: 세금 계산, 배송비 계산)이 외부 시스템에 의존할 때, 그 상호작용 인터페이스를 도메인 서비스로 정의합니다. 실제 구현은 인프라 계층에서 제공합니다.
* 순수한 계산 또는 변환 로직: 복잡한 알고리즘 또는 정책(예: 할인 정책 평가, 신용 점수 계산)이 값 객체로 표현하기에 너무 복잡할 때 사용됩니다.
도메인 서비스를 설계할 때는 엔티티나 값 객체로 표현할 수 있는지 먼저 고려해야 합니다. '고객'이나 '주문'과 같은 명사적 개념은 엔티티가 적합하며, '송금하기'나 '계산하기'와 같은 동사적 개념이나 프로세스가 도메인 서비스 후보가 됩니다. 서비스의 인터페이는 도메인 계층에 정의되며, 구현은 도메인 계층이나 인프라 계층에 위치할 수 있습니다. 이를 통해 도메인 모델은 외부 시스템의 구체적 세부 사항에 의존하지 않고 핵심 비즈니스 개념에 집중할 수 있습니다.
전술적 설계는 제한된 컨텍스트 내에서 도메인 모델을 구체적이고 효과적으로 구현하기 위한 기술적 구성 요소와 패턴을 다룬다. 이는 전략적 설계에서 정의된 개념적 경계와 구조를 실제 코드로 옮기는 단계에 해당한다. 주로 엔티티, 값 객체, 애그리게이트 같은 도메인 객체들의 생성, 저장, 조회, 그리고 상호작용 방식을 설계하는 데 초점을 맞춘다.
핵심 구성 요소로는 리포지토리 패턴, 도메인 이벤트, 팩토리가 있다. 리포지토리 패턴은 애그리게이트의 영속성을 관리하는 객체로, 도메인 객체를 데이터 저장소에 저장하거나 조회하는 복잡성을 캡슐화한다. 이를 통해 도메인 계층은 데이터 접근 기술(예: SQL, NoSQL)에 의존하지 않고 순수한 도메인 논리에 집중할 수 있다. 도메인 이벤트는 시스템 내에서 발생한 중요한 사건을 기록하는 객체이다. 예를 들어, '주문이 접수되었다' 또는 '결제가 완료되었다' 같은 이벤트는 다른 제한된 컨텍스트나 시스템 컴포넌트에 상태 변화를 알리는 데 사용되어 느슨한 결합과 비동기 통신을 가능하게 한다.
팩토리는 복잡한 애그리게이트나 객체의 생성 로직을 캡슐화하는 디자인 패턴이다. 특히 엔티티나 값 객체의 생성 규칙이 복잡하거나, 생성 과정 자체가 중요한 도메인 지식을 포함할 때 유용하다. 팩토리를 사용하면 클라이언트 코드가 객체의 내부 구조를 알 필요 없이 일관된 방식으로 객체를 생성할 수 있으며, 도메인의 불변식을 보장하는 데 도움을 준다.
이러한 전술적 패턴들은 함께 작동하여 견고한 도메인 계층을 구성한다. 아래 표는 주요 패턴들의 역할을 요약한다.
패턴 | 주요 역할 |
|---|---|
애그리게이트의 영속성 관리와 데이터 저장소 접근 추상화 | |
도메인 내 중요한 상태 변화의 기록과 발행, 시스템 간 통신 촉진 | |
복잡한 도메인 객체의 생성 로직 캡슐화 및 불변식 보장 |
전술적 설계의 궁극적 목표는 유비쿼터스 언어가 코드에 직접적으로 반영되도록 하여, 도메인 전문가와 개발자 간의 간극을 줄이고 유지보수성이 높은 소프트웨어를 만드는 것이다.
리포지토리 패턴은 도메인 모델과 데이터 저장소(주로 데이터베이스) 사이의 매개체 역할을 하는 객체를 정의하는 패턴이다. 이 패턴은 데이터 접근 로직을 캡슐화하여 도메인 계층이 인프라스트럭처 세부 사항에 의존하지 않도록 한다. 리포지토리는 도메인 객체의 컬렉션을 관리하는 것처럼 동작하며, 도메인 계층에서는 객체를 저장하거나 조회하는 구체적인 방법을 알 필요가 없다.
리포지토리의 주요 책임은 애그리게이트 루트 객체의 영속성을 관리하는 것이다. 일반적으로 제공하는 인터페이스 메서드에는 findById, save, delete 등이 포함된다. 리포지토리는 도메인 객체를 데이터베이스의 레코드 형태로 저장하거나, 반대로 질의 결과를 도메인 객체로 재구성하여 반환한다. 이를 통해 도메인 계층은 순수한 비즈니스 로직에 집중할 수 있고, 데이터 저장소를 메모리 데이터베이스에서 관계형 데이터베이스로 변경하더라도 도메인 코드는 영향을 받지 않는다.
리포지토리 구현 시 고려해야 할 중요한 원칙은 영속성 무지를 유지하는 것이다. 즉, 도메인 객체는 자신이 어떻게 저장되고 조회되는지 알지 못해야 한다. 또한, 리포지토리는 제한된 컨텍스트 단위로 정의되며, 한 컨텍스트의 리포지토리가 다른 컨텍스트의 내부 모델을 직접 조회해서는 안 된다. 필요하다면 애플리케이션 서비스를 통해 다른 컨텍스트의 공개된 인터페이스를 이용해야 한다.
구현 방식 | 설명 | 주의사항 |
|---|---|---|
범용 리포지토리 |
| 과도하게 일반화되면 도메인 특화 질의를 표현하기 어려울 수 있다. |
도메인 특화 리포지토리 |
| 유비쿼터스 언어를 반영하여 도메인 전문가와 소통에 용이하다. |
컬렉션 지향 리포지토리 | 메모리 컬렉션을 다루듯이 동작하며, | 구현이 간단하지만, 명시적인 저장 호출이 필요하다는 점을 도메인 계층이 인지해야 한다. |
리포지토리 패턴을 적용할 때는 객체-관계 매핑 도구의 사용을 고려할 수 있다. 그러나 ORM의 세션 또는 영속성 컨텍스트 개념이 리포지토리의 추상화를 누수시킬 수 있으므로 주의가 필요하다. 예를 들어, 지연 로딩은 도메인 객체가 프록시와 같은 인프라 관심사를 알게 만들어 영속성 무지 원칙을 훼손할 수 있다[4].
도메인 이벤트는 도메인 내에서 발생한 중요한 사건이나 상태 변화를 나타내는 객체입니다. 이는 단순히 시스템의 기술적 로그가 아니라, 비즈니스적 의미를 가진 사건을 의미합니다. 예를 들어, '주문이 접수되었다', '계좌 이체가 완료되었다', '배송이 시작되었다' 등이 도메인 이벤트에 해당합니다. 이벤트는 일반적으로 과거 시제의 동사로 명명되며, 이벤트가 발생한 시점과 관련된 엔티티의 상태 정보를 담고 있습니다.
도메인 이벤트의 주요 목적은 제한된 컨텍스트 내부 또는 다른 컨텍스트 간에 일관성과 느슨한 결합을 유지하는 것입니다. 한 애그리게이트가 자신의 상태를 변경한 후, 관련된 도메인 이벤트를 발행합니다. 그러면 다른 애그리게이트나 도메인 서비스가 이 이벤트를 구독하여 후속 작업을 트리거할 수 있습니다. 이 방식은 시스템 컴포넌트들이 직접 서로를 호출하지 않고도 협력할 수 있게 합니다.
도메인 이벤트를 구현할 때는 일반적으로 다음과 같은 구성 요소를 고려합니다.
구성 요소 | 설명 |
|---|---|
이벤트 발행자 | 상태 변경을 완료한 애그리게이트 루트가 이벤트 객체를 생성하고 발행합니다. |
이벤트 핸들러 | 발행된 이벤트를 구독하여, 이메일 발송, 다른 애그리게이트 상태 변경, 통계 업데이트 등의 작업을 수행합니다. |
이벤트 디스패처 | 발행된 이벤트를 등록된 핸들러들에게 전달하는 메커니즘입니다. |
이 패턴을 적용하면 비즈니스 규칙이 명확해지고, 시스템의 유연성과 확장성이 향상됩니다. 또한, 이벤트 저장소에 모든 이벤트를 순차적으로 기록함으로써 이벤트 소싱 패턴의 기반이 되거나, 시스템의 상태 변화 내역을 추적하는 감사 로그 역할을 할 수 있습니다.
팩토리는 도메인 주도 설계에서 복잡한 객체 또는 애그리게이트의 생성 로직을 캡슐화하는 도메인 객체 생성 전용 객체 또는 메서드이다. 객체 생성 책임을 명시적으로 분리함으로써 클라이언트 코드가 도메인 객체의 내부 구조나 유효성 검사 규칙을 알 필요 없이 안전하게 객체를 생성할 수 있게 한다.
팩토리의 주요 목적은 생성 로직의 복잡성을 숨기고, 유효한 상태의 객체만을 보장하는 것이다. 특히 다음과 같은 경우에 팩토리를 사용하는 것이 유리하다.
* 객체 생성 과정이 복잡하거나(예: 여러 하위 객체 조립)
* 생성 시 특정 비즈니스 규칙을 적용해야 하거나
* 생성하는 객체의 구체적인 타입을 클라이언트가 결정하지 않아야 할 때
팩토리는 정적 메서드, 별도의 클래스, 또는 애그리게이트 루트 내의 메서드 형태로 구현될 수 있다. 예를 들어, Order 애그리게이트 내에 createOrder라는 정적 팩토리 메서드를 두어 주문 항목 리스트와 고객 정보를 받아 유효성을 검증한 후 새로운 Order 인스턴스를 반환하도록 할 수 있다. 이는 생성자보다 더 표현력이 풍부한 인터페이스를 제공할 수 있다.
팩토리와 리포지토리 패턴은 역할이 명확히 구분된다. 팩토리는 새로운 객체의 생성을 담당하는 반면, 리포지토리는 기존에 영속화된 객체의 조회, 저장, 삭제를 담당한다. 둘 다 도메인 모델의 일관성을 유지하는 데 기여하지만, 그 책임의 영역은 다르다.
도메인 주도 설계는 특정 아키텍처 패턴을 강제하지 않지만, 복잡한 도메인 모델을 효과적으로 구성하고 유지하기 위해 몇 가지 패턴이 널리 채택된다. 이 패턴들은 도메인 로직의 순수성과 의존성 방향을 관리하는 데 중점을 둔다.
계층형 아키텍처는 전통적으로 널리 사용되는 패턴이다. 일반적으로 표현 계층, 응용 계층, 도메인 계층, 인프라스트럭처 계층으로 구성된다. 핵심 원칙은 의존성 방향이 항상 위에서 아래로 흐르는 것이다. 즉, 도메인 계층은 인프라스트럭처 계층에 의존해서는 안 되며, 그 반대 방향으로 의존성이 형성되어야 한다. 이는 도메인 로직이 기술적 세부 사항(예: 데이터베이스, 외부 서비스)에 오염되는 것을 방지하는 데 목적이 있다. 그러나 엄격한 계층 분리가 지켜지지 않을 경우, 도메인 계층이 인프라스트럭처에 묶이는 문제가 발생할 수 있다.
보다 진화된 접근법으로 헥사고날 아키텍처(육각형 아키텍처)가 있다. 이 패턴은 도메인 모델을 시스템의 중심에 두고, 외부와의 모든 상호작용을 어댑터를 통해 처리한다. 내부의 도메인 계층은 외부 세계(사용자 인터페이스, 데이터베이스, 외부 API 등)에 대한 구체적인 구현으로부터 완전히 독립적이다. 외부 구성 요소는 '주도 어댑터'(사용자나 프로그램이 시스템을 호출)나 '구동 어댑터'(시스템이 외부 자원을 호출)를 통해 도메인과 소통한다. 이 구조는 도메인 모델의 테스트 용이성과 기술 스택 변경에 대한 유연성을 극대화한다.
패턴 | 핵심 아이디어 | 주요 구성 요소 | 장점 |
|---|---|---|---|
계층형 아키텍처 | 수직적 계층 분리와 단방향 의존성 | 표현, 응용, 도메인, 인프라스트럭처 계층 | 구조가 직관적이고 이해하기 쉬움 |
헥사고날 아키텍처 | 도메인 중심과 포트-어댑터 모델 | 도메인 코어, 포트, 어댑터 | 도메인 로직의 고립도가 높고, 테스트 및 유지보수가 용이함 |
이러한 아키텍처 패턴의 선택은 프로젝트의 복잡도와 팀의 숙련도에 따라 결정된다. 공통된 목표는 도메인 지식과 비즈니스 규칙을 명확하게 표현하는 도메인 모델을 설계의 중심에 견고하게 위치시키는 것이다.
계층형 아키텍처는 소프트웨어를 논리적으로 구분된 계층으로 구성하는 전통적인 아키텍처 패턴이다. 도메인 주도 설계에서도 초기 구현 접근법으로 자주 활용된다. 일반적으로 프레젠테이션 계층, 애플리케이션 계층, 도메인 계층, 인프라스트럭처 계층으로 나뉜다. 각 계층은 명확한 책임을 가지며, 의존성은 항상 하위 계층 방향으로 흐른다. 즉, 상위 계층은 하위 계층에 의존하지만, 하위 계층은 상위 계층을 알지 못한다.
도메인 계층은 엔티티, 값 객체, 도메인 서비스 등 핵심 비즈니스 로직과 규칙을 포함한다. 이 계층은 다른 계층에 대한 의존성을 최소화하여 순수한 도메인 개념을 표현하는 데 집중한다. 애플리케이션 계층은 도메인 계층을 조정하여 사용자 요청을 처리하는 작업을 담당한다. 여기에는 트랜잭션 관리, 도메인 이벤트 발행, 보안 검사 등이 포함된다. 프레젠테이션 계층은 사용자 인터페이스를 제공하고, 인프라스트럭처 계층은 데이터베이스 접근이나 외부 서비스 연동과 같은 기술적 관심사를 처리한다.
계층 | 주요 책임 | 예시 구성 요소 |
|---|---|---|
프레젠테이션 | 사용자 인터페이스 제공, 요청 변환 | 웹 컨트롤러, API 엔드포인트, 뷰 템플릿 |
애플리케이션 | 작업 조정, 트랜잭션 관리 | 애플리케이션 서비스, 작업 처리기 |
도메인 | 핵심 비즈니스 로직과 규칙 | |
인프라스트럭처 | 기술적 세부사항 구현 | 데이터베이스 리포지토리 구현체, 외부 API 클라이언트 |
이 패턴의 주요 장점은 관심사의 분리로 인해 코드의 이해도와 유지보수성이 향상된다는 점이다. 각 계층을 독립적으로 개발하고 테스트할 수 있다. 그러나 단점도 존재한다. 계층 간의 엄격한 규칙이 지켜지지 않으면, 특히 도메인 계층이 인프라스트럭처 계층에 직접 의존하게 되어 도메인 로직이 기술적 세부사항과 뒤섞일 수 있다. 이는 도메인 모델의 순수성을 해치고 테스트를 어렵게 만든다. 또한, 모든 의존성이 단방향이라는 규칙이 복잡한 상호작용을 필요로 하는 현대 애플리케이션에서는 제약이 될 수 있다. 이러한 한계를 보완하기 위해 헥사고날 아키텍처와 같은 더 유연한 아키텍처 패턴이 대안으로 제시된다.
헥사고날 아키텍처는 포트와 어댑터 아키텍처라고도 불리며, 도메인 주도 설계의 구현을 지원하는 아키텍처 패턴이다. 이 아키텍처의 핵심 목표는 도메인 모델을 애플리케이션의 중심에 두고, 외부의 기술적 세부 사항(예: 데이터베이스, 웹 프레임워크, UI)에 대한 의존성을 분리하는 것이다. 이를 통해 비즈니스 로직의 순수성을 유지하고 테스트 용이성과 시스템 유지보수성을 크게 향상시킨다.
이 패턴은 육각형 모양의 다이어그램으로 표현되며, 중앙에 도메인 모델이 위치한다. 도메인 모델 주변에는 외부 세계와의 상호작용을 정의하는 포트가 존재한다. 포트는 인터페이스로, 애플리케이션의 핵심이 제공하거나 필요로 하는 기능을 추상적으로 정의한다. 예를 들어, 데이터를 영구 저장하는 기능은 '저장소 포트'로, 외부 시스템에서 메시지를 받는 기능은 '메시지 수신 포트'로 정의될 수 있다.
포트를 실제 기술로 구현하는 부분은 어댑터이다. 어댑터는 포트 인터페이스를 구체적으로 구현하여 외부 요소와의 연결을 담당한다. 어댑터는 주로 두 가지 유형으로 구분된다.
* 주도형(Driving) 어댑터: 외부에서 애플리케이션을 호출하는 어댑터이다. 예를 들어 REST API 컨트롤러, CLI 인터페이스, 웹 페이지 등이 이에 해당한다.
* 주도되는(Driven) 어댑터: 애플리케이션이 외부를 호출하는 어댑터이다. 예를 들어 데이터베이스 접근 구현체, 외부 API 클라이언트, 메시지 발행 구현체 등이 이에 해당한다.
어댑터 유형 | 역할 | 구현 예시 |
|---|---|---|
주도형(Driving) | 외부에서 애플리케이션의 도메인 모델을 호출/사용함 | |
주도되는(Driven) | 애플리케이션의 요청에 따라 외부 시스템/자원을 사용함 |
이 구조의 가장 큰 장점은 의존성 방향이 항상 외부에서 내부(도메인 모델)로 향한다는 점이다. 즉, 데이터베이스, UI, 프레임워크와 같은 외부 기술은 모두 교체 가능한 부품이 된다. 이로 인해 도메인 로직은 외부 변화로부터 보호받으며, 통합 테스트 시 실제 인프라 대신 가짜(Mock) 어댑터를 쉽게 주입하여 도메인 로직만을 격리하여 테스트할 수 있다. 결과적으로 헥사고날 아키텍처는 도메인 주도 설계가 추구하는 도메인 중심의 설계를 실현하는 강력한 기술적 토대를 제공한다.
구현 접근법은 도메인 주도 설계의 이론적 개념을 실제 코드로 옮기는 실천적 단계를 의미한다. 이 과정은 도메인 전문가와 개발팀의 긴밀한 협업을 바탕으로 유비쿼터스 언어를 구체화하고, 이를 제한된 컨텍스트 내의 소프트웨어 모델로 발전시킨다.
도메인 모델링 과정은 반복적이고 점진적이다. 초기에는 도메인 전문가와의 대화를 통해 핵심 엔티티, 값 객체, 그리고 그들 간의 관계를 식별한다. 이 단계에서 이벤트 스토밍[5] 같은 워크숍 기법이 유용하게 활용된다. 식별된 개념들은 유비쿼터스 언어의 어휘가 되며, 이 언어는 코드, 문서, 대화에서 일관되게 사용되어 모델과 구현 사이의 간극을 줄인다. 모델은 단순한 다이어그램이 아니라, 시스템의 핵심 로직을 담는 살아있는 설계 도구로 간주된다.
리팩토링과 점진적 개선은 구현 접근법의 필수 요소이다. 초기 모델은 불완전할 수 있으며, 새로운 요구사항이나 도메인에 대한 이해가 깊어짐에 따라 지속적으로 진화해야 한다. 개발자는 코드 냄새[6]를 식별하고, 모델의 표현력을 높이기 위해 리팩토링을 수행한다. 예를 들어, 처음에는 단순한 속성으로 표현되던 개념이 복잡한 행동을 갖게 되면, 별도의 값 객체나 도메인 서비스로 추출될 수 있다. 이 과정은 모델이 도메인의 본질을 더 정확하게 반영하도록 돕는다.
접근 단계 | 주요 활동 | 산출물/결과 |
|---|---|---|
모델 탐색 | 도메인 전문가 협업, 이벤트 스토밍 | 공유된 이해, 초기 유비쿼터스 언어 목록 |
모델 설계 | 개념적 모델, 도메인 이벤트 정의 | |
모델 구현 | 실행 가능한 도메인 계층 코드 | |
모델 정제 | 지속적인 리팩토링, 피드백 반영 | 진화된 모델, 개선된 코드 품질 |
도메인 모델링 과정은 도메인 주도 설계의 핵심 실천법으로, 복잡한 비즈니스 문제를 소프트웨어 모델로 점진적으로 구체화하는 일련의 활동을 의미한다. 이 과정은 비즈니스 전문가와 개발 팀이 협력하여 유비쿼터스 언어를 정제하고, 제한된 컨텍스트의 경계를 식별하며, 도메인 모델을 반복적으로 구축하고 개선하는 것을 포함한다.
모델링은 일반적으로 도메인 전문가와의 협업 세션을 통해 시작된다. 팀은 비즈니스 프로세스, 규칙, 핵심 개념을 탐구하며 대화를 기록하고, 초기 유비쿼터스 언어의 단어와 문구를 도출한다. 이 단계에서는 화이트보드 스케치, 이벤트 스토밍[7], 간단한 다이어그램 등이 활용되어 추상적인 개념을 시각적으로 표현한다. 목표는 소프트웨어 구조가 아닌, 비즈니스의 본질을 반영하는 개념 모델을 만드는 것이다.
단계 | 주요 활동 | 산출물 예시 |
|---|---|---|
탐색 및 분석 | 도메인 전문가 인터뷰, 비즈니스 프로세스 분석, 용어 정리 | 이벤트 스토밍 결과물, 용어집 초안, 간단한 개념 다이어그램 |
개념 모델링 | 유비쿼터스 언어 정제본, 초기 도메인 모델 다이어그램 | |
모델 구현 및 검증 | 제한된 컨텍스트 내에서 코드로 모델 구현, 시나리오 기반 테스트 | |
정제 및 반복 | 구현 경험을 바탕으로 모델과 언어 재검토, 리팩토링 | 개선된 모델, 명확해진 유비쿼터스 언어, 경계 조정 |
이러한 탐색과 분석을 바탕으로 구체적인 개념 모델링이 이루어진다. 팀은 식별된 개념들을 엔티티, 값 객체, 도메인 서비스 등으로 분류하고, 이들 간의 관계와 불변 조건을 정의한다. 특히 데이터의 일관성과 생명주기를 관리하는 단위인 애그리게이트의 경계를 찾는 것이 중요하다. 이 모델은 즉시 코드로 구현되어 검증되며, 구현 중 발견된 모호함이나 새로운 통찰은 다시 대화와 모델 정제로 이어진다. 따라서 도메인 모델링은 일회성 설계가 아니라, 비즈니스 이해가 깊어짐에 따라 모델도 함께 진화하는 지속적인 학습과 발견의 순환 과정이다.
도메인 주도 설계는 완벽한 설계를 처음부터 완성하기보다는, 지속적인 학습과 발견을 통해 모델을 진화시키는 점진적 접근법을 강조한다. 이 과정에서 리팩토링은 핵심적인 실천법이다. 리팩토링은 코드의 외부 동작을 변경하지 않으면서 내부 구조를 개선하는 작업으로, 유비쿼터스 언어가 더 명확하게 코드에 반영되도록 하고, 제한된 컨텍스트의 경계를 선명하게 다듬는 데 기여한다. 예를 들어, 중복된 로직을 하나의 값 객체로 추출하거나, 명확하지 않은 엔티티의 책임을 도메인 서비스로 분리하는 작업이 여기에 해당한다.
도메인 모델링은 한 번에 끝나는 활동이 아니다. 비즈니스 요구사항에 대한 이해가 깊어지고, 유비쿼터스 언어가 정제됨에 따라 기존 모델은 불완전하거나 부적절해질 수 있다. 따라서 개발 팀은 구현과 피드백 사이의 빠른 사이클을 유지하며 모델을 지속적으로 개선해야 한다. 이 점진적 개선 과정은 설계의 복잡성을 관리하고, 모델이 실제 도메인을 정확하게 반영하도록 보장한다.
리팩토링과 점진적 개선을 효과적으로 수행하기 위해서는 자동화된 테스트 코드가 필수적이다. 특히 도메인 모델의 핵심 로직을 검증하는 단위 테스트는 리팩토링의 안전망 역할을 한다. 테스트가 충분히覆盖되어 있지 않다면, 구조 변경 시 예기치 않은 부작용을 감지하기 어려워 진화보다는 퇴보가 발생할 위험이 있다.
접근법 | 주요 활동 | 목표 |
|---|---|---|
리팩토링 | 코드/모델 구조 개선, 중복 제거, 명칭 변경 | 유비쿼터스 언어와 코드의 일치성 향상, 설계의 명확성 증대 |
점진적 개선 | 지속적인 학습, 모델 검증, 작은 단위의 반복적 변경 | 도메인에 대한 이해도 심화, 모델의 정확성과 표현력 강화 |
이러한 반복적인 개선 작업은 단순한 코드 정리가 아니라, 도메인에 대한 새로운 통찰을 설계에 즉시 반영하는 도메인 주도 설계의 생명력이다. 최종적으로는 소프트웨어의 유지보수성을 높이고, 변화하는 비즈니스 요구에 더 민첩하게 대응할 수 있는 탄력적인 아키텍처를 구축하는 데 기여한다.
도메인 주도 설계는 복잡한 소프트웨어 프로젝트에서 비즈니스 요구사항을 효과적으로 구현하는 데 강력한 이점을 제공한다. 가장 큰 장점은 개발자와 도메인 전문가가 공유하는 유비쿼터스 언어를 통해 의사소통의 간극을 줄이고, 소프트웨어 모델이 실제 비즈니스 개념을 정확히 반영하도록 한다는 점이다. 이는 요구사항 오해에서 비롯되는 재작업을 줄이고, 장기적으로 유지보수성을 크게 향상시킨다. 또한, 제한된 컨텍스트와 애그리게이트 같은 개념을 통해 시스템을 명확하게 모듈화함으로써, 대규모 팀이 독립적으로 작업하고 시스템의 특정 부분을 이해하기 쉬워진다.
반면, 도메인 주도 설계는 일정한 학습 곡선과 복잡성을 요구한다는 단점이 있다. 개념과 패턴을 숙지하고 적용하는 데 시간이 필요하며, 상대적으로 간단한 CRUD 중심의 애플리케이션에는 과도한 설계가 될 수 있다. 설계 과정에서 도메인 전문가의 지속적이고 적극적인 참여가 필수적이지만, 현실에서는 이를 보장하기 어려운 경우가 많다. 초기 모델링에 상당한 노력이 투입되어야 하며, 설계가 틀어졌을 때 리팩토링 비용이 클 수 있다.
다음 표는 주요 장단점을 요약한 것이다.
장점 | 단점 |
|---|---|
비즈니스 복잡성에 대한 효과적 대처 | 높은 학습 비용과 초기 진입 장벽 |
개발팀과 도메인 전문가 간 의사소통 개선 | 도메인 전문가의 지속적 협력 필요 |
유지보수성과 시스템 이해도 향상 | 간단한 애플리케이션에는 과도할 수 있음 |
명확한 모듈 경계를 통한 팀 협업 용이 | 잘못된 모델 설계 시 수정 비용 큼 |
결론적으로, 도메인 주도 설계는 복잡한 핵심 비즈니스 로직을 가진 프로젝트에서 그 진가를 발휘한다. 비즈니스 규칙이 빠르게 변화하거나 도메인이 본질적으로 복잡한 시스템에 적합하다. 그러나 프로젝트의 규모와 복잡도를 정확히 평가한 후, 그에 상응하는 설계 노력을 기울일 수 있을 때 채택하는 것이 바람직하다.