사전 도메인 주도 설계
패턴

도메인 주도 설계

gabury1고친 사람 github-actions[bot]

도메인 주도 설계는 소프트웨어가 다루는 업무를 코드 설계의 중심에 두는 방법입니다. 개발자는 업무를 아는 사람과 같은 낱말을 씁니다. 클래스와 메서드 이름도 그 낱말로 짓습니다. 큰 시스템은 한 낱말이 한 뜻으로 통하는 구역으로 나눠 설계합니다.

쉽고 빠른 이해

도메인 주도 설계는 업무 규칙을 코드 한가운데 두는 설계 방법입니다. 「발송된 주문은 취소할 수 없다」는 규칙을 주문 객체가 스스로 지키게 만듭니다.

이렇게 하지 않으면 규칙이 서비스 코드 여기저기에 흩어집니다. 규칙 하나가 바뀔 때 고칠 곳을 다 찾기 어렵습니다. 개발자와 업무 담당자가 같은 것을 다른 말로 불러 요구가 잘못 옮겨지기도 합니다.

도는 순서는 이렇습니다.

  1. 업무를 아는 사람과 함께 쓸 낱말을 정합니다. 코드 이름도 그 낱말로 짓습니다
  2. 같은 낱말이 같은 뜻으로 통하는 구역마다 시스템을 나눕니다
  3. 구역 안에서는 규칙을 그 규칙이 다루는 데이터와 한 객체에 담습니다

대가도 있습니다. 업무 담당자와 계속 이야기할 시간이 듭니다. 클래스 수도 늘어납니다.

이 대가를 치를 만한 곳은 업무 규칙이 많고 자주 바뀌는 업무입니다. 화면이 테이블을 그대로 보여주는 단순한 기능에는 품만 듭니다.

상세

이 절은 먼저 도메인이라는 낱말과 이 설계 방법이 풀려는 곤란을 봅니다. 그다음 두 층위로 나눠 봅니다. 시스템을 구역으로 나누는 큰 설계와, 구역 안의 코드를 짜는 작은 설계입니다. 예로는 주문을 받고 배송하는 쇼핑몰 하나를 끝까지 씁니다.

도메인과 도메인 모델

도메인은 소프트웨어가 다루는 업무 영역입니다. 쇼핑몰 시스템이라면 온라인으로 물건을 팔고 보내는 업무 전체가 도메인입니다. 보험사 시스템이라면 가입부터 보상 청구까지의 보험 업무가 도메인입니다.

도메인은 다시 작은 업무로 나뉩니다. 쇼핑몰의 주문, 결제, 배송, 정산이 그렇습니다. 이렇게 나눈 업무 하나하나가 하위 도메인입니다.

도메인에는 그 업무만의 규칙이 있습니다. 「발송된 주문은 취소할 수 없다」, 「쿠폰은 두 장까지 겹쳐 쓸 수 있다」 같은 것입니다. 이 규칙을 가장 잘 아는 사람을 도메인 전문가라고 부릅니다. 쇼핑몰의 상품 기획자나 고객센터 담당자가 도메인 전문가입니다.

도메인 모델은 이 업무를 코드로 옮겨 놓은 것입니다. 업무에 나오는 대상과 규칙을 객체로 만듭니다. 주문이라는 대상은 Order 클래스가 됩니다. 취소 규칙은 그 클래스의 cancel() 메서드가 됩니다.

이름이 가리키는 것

도메인 주도 설계는 영어 Domain-Driven Design 을 옮긴 말입니다. 줄여서 DDD 라고 부릅니다. 이 이름은 에릭 에반스(Eric Evans)가 2003년에 낸 같은 제목의 책에서 왔습니다. 이 방법은 여러 패턴을 모아 놓은 꼴입니다.

DDD 는 특정 프레임워크나 폴더 구조가 아닙니다. 설계를 정할 때 무엇을 기준으로 삼을지를 정하는 방법입니다. 기준은 데이터베이스 테이블이나 화면이 아니라 업무입니다.

없으면 무엇이 곤란한가

업무 규칙이 서비스 클래스에 흩어진 코드를 먼저 보겠습니다. 주문 취소를 처리하는 서비스 하나입니다.

Java
class OrderService {
    void cancel(long id) {
        OrderRow row = dao.find(id);
        if (row.status == 3)   // 3 = 발송됨
            throw new IllegalStateException();
        row.status = 9;        // 9 = 취소됨
        dao.update(row);
    }
}

OrderRow 는 테이블의 한 행을 옮겨 담기만 하는 객체입니다. 「발송된 주문은 취소할 수 없다」는 규칙은 서비스 안의 if 문에 있습니다. 관리자 화면의 취소 기능과 밤마다 도는 자동 취소 작업도 같은 if 문을 따로 들고 있을 수 있습니다.

규칙이 바뀌면 이 셋을 다 찾아 고쳐야 합니다. 하나를 놓치면 한쪽 경로로는 발송된 주문이 취소됩니다. 서비스 메서드마다 테이블 행을 읽고 고치는 이런 짜임을 트랜잭션 스크립트라고 부릅니다. 규칙이 몇 개 없을 때는 이 짜임이 가장 짧습니다.

곤란은 코드 밖에도 있습니다. 도메인 전문가는 「주문 확정」이라고 말합니다. 그런데 코드에는 updateStatus(2) 만 있습니다. 그러면 누군가 둘 사이를 머릿속에서 옮겨야 합니다.

옮길 때마다 뜻이 조금씩 샙니다. 요구 사항과 코드가 어긋나는 틈이 여기서 생깁니다.

DDD 로 짜면 규칙이 주문 객체 안으로 들어갑니다.

Java
class Order {
    private OrderStatus status;

    void cancel() {
        if (status == OrderStatus.SHIPPED)
            throw new AlreadyShippedException();
        status = OrderStatus.CANCELED;
    }
}

이제 취소 규칙을 아는 곳은 Order 하나입니다. 어느 경로로 취소하든 order.cancel() 을 부르므로 규칙을 건너뛸 수 없습니다. 상태 값도 숫자 대신 「발송됨」을 뜻하는 SHIPPED 같은 이름으로 드러납니다.

유비쿼터스 언어

유비쿼터스 언어는 개발자와 도메인 전문가가 함께 쓰는 한 벌의 낱말입니다. 유비쿼터스(ubiquitous)는 「어디에나 있는」이라는 뜻입니다. 회의에서 쓰는 말, 문서에 적는 말, 코드의 이름이 모두 같은 낱말이라는 뜻에서 붙은 이름입니다.

이 언어가 필요한 까닭은 앞 소절의 번역 틈을 없애기 위해서입니다. 도메인 전문가가 「주문을 확정한다」고 하면 코드에도 order.confirm() 이 있습니다. 코드를 읽는 개발자와 요구를 말하는 도메인 전문가가 같은 문장을 봅니다.

낱말은 대화하면서 계속 다듬습니다. 회의에서 새 낱말이 나오거나 뜻이 바뀌면 코드의 이름도 함께 바꿉니다. 코드 이름이 어색하게 느껴지면 그것이 업무를 잘못 이해했다는 신호일 수 있습니다.

바운디드 컨텍스트

큰 시스템에서는 한 낱말이 구역마다 다른 뜻을 가집니다. 쇼핑몰의 「상품」이 그렇습니다. 주문 쪽에서 상품은 이름과 판매가를 가집니다. 배송 쪽에서 상품은 무게와 부피를 가집니다. 정산 쪽에서 상품은 판매자와 수수료율을 가집니다.

이 셋을 Product 클래스 하나에 모으면 모든 필드를 다 가진 거대한 클래스가 됩니다. 배송 쪽이 필드를 하나 바꿀 때 주문 쪽 코드까지 흔들립니다. 누구의 「상품」인지 이름만 보고는 알 수 없게 됩니다.

DDD 는 대신 한 낱말이 한 뜻으로 통하는 경계를 긋습니다. 이 경계 안을 바운디드 컨텍스트라고 부릅니다. 경계 안에서는 모델 하나와 언어 하나가 통합니다. 경계를 넘으면 같은 낱말이라도 다른 모델입니다.

flowchart TD
    subgraph O["주문 컨텍스트"]
        OP["상품 · 이름 · 판매가"]
    end
    subgraph S["배송 컨텍스트"]
        SP["상품 · 무게 · 부피"]
    end
    subgraph F["정산 컨텍스트"]
        FP["상품 · 판매자 · 수수료율"]
    end
    O -->|"주문이 확정됐다 · 상품 번호"| S
    O -->|"주문이 확정됐다 · 상품 번호"| F

그림의 세 컨텍스트는 저마다 자기 「상품」을 가집니다. 컨텍스트 사이로는 상품 객체가 통째로 건너가지 않습니다. 「주문이 확정됐다」는 알림과 상품 번호만 건너갑니다. 받는 쪽은 그 번호로 자기 모델의 상품을 찾습니다.

그림의 컨텍스트 이름은 앞에서 본 하위 도메인 이름과 같습니다. 하위 도메인 하나를 컨텍스트 하나가 맡게 나누는 경우가 흔해서입니다. 하지만 하위 도메인은 업무를 나눈 것입니다. 바운디드 컨텍스트는 모델 하나가 통하는 코드의 경계입니다.

이 경계는 마이크로서비스를 나누는 기준으로 자주 쓰입니다. 마이크로서비스는 시스템을 따로 배포하는 작은 서비스 여럿으로 나누는 아키텍처입니다. 바운디드 컨텍스트 하나를 서비스 하나로 삼으면 서비스마다 자기 모델을 가집니다. 컨텍스트를 한 애플리케이션 안의 모듈로만 나눌 수도 있습니다.

전략 설계

시스템을 어떤 컨텍스트로 나눌지 정하는 일을 전략 설계라고 부릅니다. 앞 소절의 바운디드 컨텍스트가 전략 설계의 한가운데에 있습니다.

컨텍스트를 나눈 뒤에는 컨텍스트끼리 어떻게 이어지는지를 정합니다. 이 연결을 한 장에 그린 것이 컨텍스트 맵입니다. 앞 그림에서도 「주문이 확정됐다」는 알림은 경계를 건넜습니다. 연결마다 정할 것은 받는 쪽이 이 알림을 누구의 모델로 읽느냐입니다.

한 가지는 상대 모델을 따르는 것입니다. 배송 컨텍스트가 주문 쪽 알림의 필드 이름과 구조를 손대지 않고 씁니다. 코드는 짧습니다. 대신 주문 쪽이 필드 이름을 바꾸면 배송 코드도 함께 고쳐야 합니다.

다른 한 가지는 번역입니다. 받은 데이터를 경계에서 자기 모델로 바꿔 담는 일입니다. 배송 컨텍스트는 알림을 받자마자 자기 「배송 요청」 객체로 옮겨 담습니다. 주문 쪽 이름이 바뀌어도 옮겨 담는 코드 한 곳만 고치면 됩니다.

번역을 맡는 부품 가운데 하나가 부패 방지 계층입니다. 다른 시스템의 모델을 받아 자기 컨텍스트의 모델로 바꿔 주는 층입니다. 이 층이 있으면 다른 시스템의 이름과 구조가 자기 코드 안으로 번지지 않습니다.

배송 컨텍스트가 택배사 시스템과 이어지는 경우를 보겠습니다. 택배사 시스템이 배송 상태를 03 같은 코드 값으로 돌려준다고 해 봅시다. 부패 방지 계층이 이 값을 배송 컨텍스트의 「배송 중」 상태로 바꿔 줍니다. 택배사가 코드 값을 바꿔도 고칠 곳은 이 층 하나입니다.

모든 하위 도메인에 같은 품을 들이지는 않습니다. 회사가 남과 달라지는 업무를 핵심 도메인이라고 부릅니다. 쇼핑몰이라면 추천이나 가격 정책이 그럴 수 있습니다. 설계 품은 여기에 몰아 씁니다. 회원 가입이나 알림 발송처럼 어디나 비슷한 업무는 단순하게 짜거나 기성 제품을 씁니다.

전술 설계

컨텍스트 안의 코드를 짜는 방법을 전술 설계라고 부릅니다. 전술 설계는 업무의 대상을 몇 가지 역할로 나눠 다룹니다. 아래 표는 여섯 역할을 한눈에 보여 줍니다. 표 아래에서 하나씩 풉니다.

역할 맡는 일 쇼핑몰의 예
엔티티 식별자로 구별합니다. 상태가 바뀌어도 같은 것입니다 주문 번호를 가진 주문
값 객체 값으로만 구별합니다. 고치지 않고 새로 만듭니다 금액 · 배송지 주소
애그리거트 함께 바뀌어야 하는 객체를 한 묶음으로 다룹니다 주문과 그 주문 항목들
[[리포지토리 패턴 리포지토리]] 애그리거트를 저장하고 다시 꺼내 줍니다
도메인 서비스 한 객체에 넣기 어색한 규칙을 맡습니다 여러 쿠폰의 할인 계산
도메인 이벤트 업무에서 일어난 일을 알립니다 주문이 확정됐다

표의 역할들은 앞 소절의 Order 예로 이어 볼 수 있습니다. Order 는 주문 번호로 구별되는 엔티티입니다. 주문 항목과 함께 애그리거트 하나를 이룹니다. 바깥 코드는 주문 항목을 직접 고치지 않습니다. 언제나 Order 의 메서드를 거칩니다.

이렇게 묶는 까닭은 규칙을 한 곳에서 지키기 위해서입니다. 「주문 총액은 항목 금액의 합이다」라는 규칙은 항목을 고칠 때마다 지켜져야 합니다. 항목을 고치는 길이 Order 하나뿐이면 그 규칙도 Order 하나가 지킵니다.

리포지토리는 이 묶음을 저장소와 이어 줍니다. 도메인 코드는 orders.findById(id) 로 주문을 꺼내고 저장할 뿐입니다. 데이터베이스에 보낼 SQL(Structured Query Language) 문을 짜거나 테이블을 고르는 일은 리포지토리 구현이 맡습니다.

값 객체는 식별자 없이 값으로만 구별됩니다. 10,000원짜리 금액 객체 두 개는 어느 쪽을 써도 같은 것입니다. 그래서 금액을 바꿀 때는 객체를 고치지 않고 새 금액 객체를 만듭니다. 금액을 int 대신 이런 객체로 두면 「금액은 음수가 될 수 없다」 같은 규칙을 금액 객체가 스스로 지킵니다.

도메인 서비스는 어느 한 객체의 것이라고 하기 어려운 규칙을 맡습니다. 쿠폰 여러 장을 겹쳐 쓸 때의 할인 계산은 주문 총액과 쿠폰마다의 조건을 함께 봅니다. 이 계산을 주문에 넣으면 주문이 쿠폰 규칙까지 알게 됩니다. 쿠폰 한 장에 넣으면 그 쿠폰은 다른 쿠폰을 볼 수 없습니다.

그래서 할인 계산은 떼어 내 도메인 서비스 하나에 둡니다. 이 서비스는 업무 규칙만 계산합니다. 주문을 꺼내거나 저장하는 일은 하지 않습니다.

도메인 이벤트는 업무에서 일어난 일을 알리는 객체입니다. 바운디드 컨텍스트 그림에서 주문 컨텍스트가 배송과 정산에 보낸 「주문이 확정됐다」는 알림이 바로 도메인 이벤트입니다. 주문 컨텍스트는 이 알림을 누가 받는지 몰라도 됩니다. 정산 쪽에 새 처리가 생겨도 주문 코드는 고치지 않습니다.

계층 안의 도메인 모델

DDD 는 코드를 네 계층으로 나눕니다. 계층마다 맡는 일이 다릅니다. 업무 규칙은 도메인 계층에만 둡니다.

계층 맡는 일 주문 취소에서 하는 일
표현 계층 요청을 받고 응답을 돌려줍니다 취소 요청을 받아 주문 번호를 꺼냅니다
응용 계층 할 일의 순서를 정하고 트랜잭션을 엽니다 주문을 꺼내 cancel() 을 부르고 저장합니다
도메인 계층 업무 규칙을 지킵니다 발송된 주문이면 취소를 거절합니다
인프라 계층 데이터베이스와 외부 시스템을 다룹니다 주문을 테이블에 저장합니다

표에서 응용 계층은 규칙을 판단하지 않습니다. 「발송됐나」를 묻는 if 문이 응용 계층에 있으면 규칙이 도메인 밖으로 샌 것입니다. 응용 계층의 일은 도메인 객체에 순서대로 일을 시키는 데서 끝납니다.

패턴 이름만 따라 할 때

전술 설계의 이름만 가져다 쓰는 경우가 있습니다. 클래스 이름에 Entity, Repository 를 붙이지만 규칙은 여전히 서비스에 있습니다. 엔티티는 필드와 getter·setter 만 가진 데이터 통입니다.

이런 모델을 빈약한 도메인 모델이라고 부릅니다. 겉모양은 DDD 를 닮았지만 규칙이 흩어진 문제는 그대로 남습니다. DDD 의 무게는 클래스 이름이 아니라 규칙이 어디에 있느냐에 있습니다.

쓰면 나빠지는 것

첫째 대가는 시간입니다. 유비쿼터스 언어와 컨텍스트 경계는 도메인 전문가와 오래 이야기해야 나옵니다. 도메인 전문가가 곁에 없으면 이 과정이 성립하지 않습니다.

둘째 대가는 코드의 양입니다. 값 객체와 리포지토리, 컨텍스트 사이의 번역 코드가 늘어납니다. 테이블 행을 바로 고치면 몇 줄로 끝날 기능에 클래스가 여럿 생깁니다.

셋째 대가는 저장과의 어긋남입니다. 객체 묶음의 모양과 테이블의 모양이 다르면 둘을 잇는 매핑 코드가 복잡해집니다. 객체와 테이블을 자동으로 잇는 도구인 ORM(Object-Relational Mapping, 객체-관계 매핑)을 써도 이 어긋남은 남습니다.

넷째 대가는 경계를 잘못 그었을 때의 비용입니다. 컨텍스트 경계가 서비스나 팀 경계로 굳은 뒤에는 옮기기 어렵습니다. 업무를 덜 이해한 초기에 그은 경계일수록 틀릴 가능성이 큽니다.

쓰는 곳과 안 쓰는 곳

업무 규칙이 많고 계속 바뀌는 업무에 맞습니다. 보험료 계산, 물류 배차, 금융 상품처럼 규칙이 얽힌 업무가 그렇습니다. 규칙이 많을수록 한 곳에 모아 둔 효과가 커집니다.

화면이 테이블을 그대로 보여주는 기능에는 안 맞습니다. 만들기·읽기·고치기·지우기 네 동작으로 끝나는 이런 기능을 CRUD(Create·Read·Update·Delete)라고 부릅니다. 지킬 규칙이 적어 모델을 세운 품이 돌아오지 않습니다.

전략 설계만 가져다 쓰기도 합니다. 전술 설계의 역할 이름은 쓰지 않습니다. 컨텍스트 경계와 유비쿼터스 언어만 씁니다. 서비스를 어디서 나눌지 정할 때 이 방식이 쓰입니다.

한 시스템 안에서도 컨텍스트마다 다르게 고릅니다. 핵심 도메인을 맡은 컨텍스트에는 전술 설계까지 씁니다. 나머지 컨텍스트는 트랜잭션 스크립트로 짭니다. 바운디드 컨텍스트가 나뉘어 있으면 컨텍스트마다 다른 방식을 써도 서로 섞이지 않습니다.

관련 항목

도메인 주도 설계를 이루는 전략 설계 도구

경계 컨텍스트 · 유비쿼터스 언어 · 컨텍스트 맵 · 부패 방지 계층 · 공유 커널 · 핵심 도메인 · 하위 도메인

도메인 주도 설계를 이루는 전술 설계 부품

엔티티 · 값 객체 · 애그리거트 · 리포지토리 패턴 · 도메인 서비스 · 도메인 이벤트 · 팩토리 · 응용 서비스

도메인 주도 설계가 코드를 나누는 계층

계층형 아키텍처 · 표현 계층 · 응용 계층 · 도메인 계층 · 인프라 계층 · 헥사고날 아키텍처 · 클린 아키텍처 · 의존성 역전 원칙

도메인 주도 설계와 짝지어 쓰는 설계 패턴

CQRS · 이벤트 소싱 · 사가 · 트랜잭셔널 아웃박스 · 이벤트 주도 · 이벤트 스토밍

도메인 주도 설계와 맞세워지는 업무 로직 짜임

트랜잭션 스크립트 · 빈약한 도메인 모델 · 액티브 레코드 · 테이블 모듈 · 도메인 모델 · CRUD

도메인 주도 설계가 경계를 긋는 아키텍처

마이크로서비스 · 모놀리스 · 모듈러 모놀리스 · 서비스 지향 아키텍처 · 콘웨이의 법칙 · 소프트웨어 아키텍처

도메인 주도 설계가 기대는 객체 지향 설계 원칙

객체 지향 설계 · 캡슐화 · 응집도 · 결합도 · 관심사 분리 · 단일 책임 원칙 · ORM · 트랜잭션

다른 이름: DDD · Domain-Driven Design · 도메인 드리븐 디자인 · 도메인 주도 설계 방법론