목차
도메인 주도 설계: 업무의 언어를 코드의 경계로 옮기기
결제 팀은 카드 승인을 받으면 주문이 완료됐다고 말하고, 물류 팀은 출고할 상품이 확정돼야 주문이 완료됐다고 말한다. 이 두 의미를 하나의 completed 필드에 넣으면 어느 팀이 값을 바꿔야 하는지, 다음 처리가 무엇인지 모호해진다. DDD를 시작하는 지점은 이런 업무 의미의 차이를 모델에 드러내는 일이다.
도메인 주도 설계(Domain-Driven Design)는 전략적 설계와 전술적 설계로 나눠 볼 수 있다. 전략적 설계는 업무 영역·용어·모델의 경계와 관계를 정리한다. 전술적 설계는 경계 안에서 엔티티, 값 객체, 애그리거트 등으로 규칙을 코드에 표현한다. 두 작업은 한 번의 문서 작성으로 끝나는 순서라기보다 구현과 대화 과정에서 서로를 수정한다.
같은 단어를 같은 뜻으로 사용하기
회의에서는 배송지라고 부르고 코드에는 address1, DB에는 customer_location, 테스트에는 수령지라고 적혀 있으면 같은 대상을 말하는지 확인하는 데 시간이 든다. 유비쿼터스 언어는 도메인 전문가와 개발자가 모델을 설명할 때 함께 사용하는 언어다. 문서의 용어집뿐 아니라 클래스, 메서드, 테스트 사례에도 나타나야 한다. Ubiquitous Language
예를 들어 “주문을 취소한다”라는 말을 들었을 때 다음 질문을 해 볼 수 있다.
- 결제 전 주문과 결제 후 주문의 취소 조건이 같은가?
- 출고가 시작된 주문도 취소라고 부르는가, 반품이라고 부르는가?
- 결제 취소가 실패했을 때 주문 상태는 무엇인가?
이 질문의 답이 setStatus("CANCELLED") 한 줄에 숨겨져 있다면 코드만으로 업무 의미를 이해하기 어렵다. requestCancellation, confirmRefund처럼 실제 행위와 사건을 구분하면 예외 경로도 더 잘 보인다. 이름을 길게 바꾸는 것이 목적은 아니다. 구분해야 할 업무 의미가 있는지 먼저 확인한다.
모델이 유효한 범위를 정하기
바운디드 컨텍스트는 특정 모델과 언어가 일관된 의미를 갖는 경계다. 모든 부서의 Product를 하나의 거대한 클래스로 통일할 필요는 없다. Bounded Context
| 컨텍스트 | 상품에서 필요한 정보 | 중요한 규칙 |
|---|---|---|
| 카탈로그 | 이름, 설명, 노출 상태 | 판매 화면에 무엇을 공개할 것인가 |
| 주문 | 구매 당시 이름·가격, 주문 수량 | 확정한 주문 금액을 어떻게 보존할 것인가 |
| 배송 | 무게, 포장 단위, 배송 제한 | 어떤 방식으로 운송할 수 있는가 |
카탈로그 가격이 바뀌었다고 이미 결제한 주문 금액이 자동으로 달라지면 안 된다. 따라서 주문은 현재 카탈로그 객체를 무조건 다시 읽는 대신 구매 당시 가격을 기록할 수 있다. 같은 상품 ID를 주고받더라도 각 모델의 속성과 변경 규칙은 다르다.
경계가 곧 마이크로서비스 하나를 뜻하는 것은 아니다. 먼저 같은 애플리케이션 안에서 모듈을 구분할 수도 있다. 네트워크를 나누면 독립 배포가 가능해지는 대신 실패·재시도·일관성 문제가 추가된다. 모델의 경계와 배포 단위는 관련돼 있지만 같은 결정은 아니다.
전술적 설계로 규칙을 놓을 위치 찾기
주문 컨텍스트 안에서 주문 ID가 같은 주문은 배송지나 상태가 달라져도 같은 주문으로 추적된다. 이런 식별의 연속성이 중요한 객체가 엔티티다. 금액이나 배송지처럼 속성의 값으로 비교할 대상을 값 객체로 표현할 수 있다. 무조건 모든 DTO를 엔티티라고 부르면 이 구분을 잃는다.
애그리거트는 함께 일관성을 지킬 객체의 묶음이며 루트를 통해 변경을 제어한다. “주문 항목 수량은 양수이고, 확정한 주문에는 최소 한 개의 항목이 있다”는 규칙을 지킬 책임을 Order에 모으는 식이다. DDD Aggregate
Order.confirm()
├─ 주문 항목이 비어 있는가? → 거절
├─ 변경 가능한 상태인가? → 아니면 거절
├─ 각 항목의 금액 검증
└─ 확정 상태를 가진 새 주문 반환
이 흐름은 언어나 ORM에 독립적인 모델 예시다. 화면에서만 수량을 검사하고 엔티티의 public setter는 아무 값이나 받는다면 다른 진입점에서 규칙이 깨질 수 있다. 규칙을 소유한 객체가 정상 상태를 보장하도록 생성과 변경 경로를 설계한다. 조회용 화면 모델은 사용하기 편한 모양을 별도로 가질 수 있다.
컨텍스트 사이에는 어떤 사실을 전달할 것인가
sequenceDiagram
participant O as 주문
participant P as 결제
participant D as 배송
O->>P: 결제 요청 ID와 금액
P-->>O: 결제 승인 사실
O->>O: 주문 상태 전이 검증
O-->>D: 배송 요청 사실
D->>D: 포장과 출고 준비
화살표는 의미 있는 업무 정보를 전달하는 경로다. 실제로 동기 API를 쓸지 메시지를 쓸지는 처리 시간과 실패 요구에 따라 선택한다. 메시지를 두 번 받거나 순서가 바뀌는 상황에서도 같은 주문을 두 번 출고하지 않아야 하므로 이벤트 ID, 상태 전이, 중복 처리를 함께 설계한다.
외부 결제 서비스의 상태 이름을 내부 주문 상태로 그대로 복사하면 외부 API 변경이 도메인 모델 전체에 번질 수 있다. 경계에서 외부 응답을 내부 의미로 번역하는 계층을 둘 수 있다. 이 계층이 필요하다는 사실은 모든 값을 변환하는 거대한 프레임워크가 필요하다는 뜻은 아니다. 실제로 다른 의미와 변경 속도를 가진 부분에 적용한다.
처음 적용할 때의 작업 순서
먼저 핵심 사용자 흐름 하나를 골라 발생한 사건, 수행하는 행위, 실패 조건을 도메인 전문가와 적는다. 모호한 단어를 찾아 정의하고, 예외가 생기면 용어와 경계를 다시 확인한다. 그다음 반드시 함께 지켜야 할 규칙을 기준으로 애그리거트 후보를 만든다.
구현 후에는 “결제 승인은 받았지만 주문 확정은 실패한 경우”, “배송 요청을 중복 수신한 경우”처럼 업무가 구분하는 사례로 모델을 검증한다. 클래스 수나 패턴 수가 늘었다는 사실로 DDD 적용 성과를 판단하지 않는다. 변경 요청을 받았을 때 어느 규칙과 경계를 고쳐야 하는지 팀이 같은 말로 설명할 수 있는지 확인한다.
업무 규칙이 단순한 조회·관리 도구라면 복잡한 모델보다 명료한 CRUD 구조가 충분할 수 있다. DDD의 전술 패턴을 모두 적용하기 전에 도메인의 복잡성과 변경 빈도를 살펴본다. 다음 글에서는 전략적 설계의 출발점인 비즈니스 도메인 분석을 구체적으로 다룬다.