앞 장에서 우리는 Cart, Order, Payment 같은 핵심 애그리거트와 그 안의 엔티티, 값 객체를 정리했습니다. 이제 남은 질문은 이것입니다. 실제 규칙은 어디에서 실행되어야 하는가. 더 구체적으로 말하면, 어떤 로직은 애그리거트 내부에 있어야 하고, 어떤 로직은 여러 애그리거트를 묶는 서비스 계층에 있어야 합니다.
이 경계를 잘못 잡으면 백엔드는 아주 빠르게 무너집니다. 처음에는 service.py 하나에 다 넣어도 되는 것처럼 보이지만, 시간이 지나면 라우터는 오케스트레이션과 검증과 상태 변경을 모두 갖게 되고, 서비스는 DB 호출과 규칙 판정과 응답 조립을 전부 떠안게 됩니다. 결국 코드가 많아지는 것이 아니라 책임이 섞이는 것이 문제입니다.
이 페이지의 목적은 그 섞임을 막는 것입니다. 어떤 규칙은 도메인 안에 남겨야 하고, 어떤 흐름은 애플리케이션 서비스가 조합해야 하며, 무엇은 단순한 인프라 호출로 남겨야 하는지를 분명히 정합니다. 이 기준이 있어야 뒤의 백엔드 구현이 라우터 중심 CRUD가 아니라 도메인 중심 구조로 나옵니다.
DDD를 공부할 때 흔히 도메인 서비스와 애플리케이션 서비스를 추상적인 패턴처럼 외우려 합니다. 하지만 실제로 중요한 것은 이름이 아니라 책임입니다.
예를 들어 주문 생성 흐름을 생각해 봅시다.
이 중 무엇이 주문 도메인 규칙이고, 무엇이 여러 컴포넌트를 연결하는 흐름인가를 구분하지 않으면 모든 것이 create_order() 한 함수 안으로 몰립니다.
이 구분이 필요한 이유는 세 가지입니다.
이 페이지를 읽고 나면 아래 항목이 정리되어 있어야 합니다.
이번 교재에서는 최소한 아래 세 층을 구분해서 생각합니다.
애그리거트, 엔티티, 값 객체가 있는 곳입니다.
여기에는 다음이 들어갑니다.
예를 들어:
하나의 애그리거트 안에 넣기에는 어색하지만, 여전히 도메인 규칙에 속하는 로직이 들어갑니다.
즉 "비즈니스 의미는 강한데, 특정 한 객체만의 책임으로 보기 어려운 것"이 이 층의 후보입니다.
유스케이스를 실행하기 위해 여러 도메인 객체와 저장소와 외부 시스템을 연결하는 층입니다.
여기에는 다음이 들어갑니다.
가장 먼저 기억해야 할 원칙은 이것입니다.
비즈니스 규칙은 가능한 한 도메인 모델 안에 둔다.
예를 들어:
이런 로직은 단순한 request/response 처리가 아니라 도메인 의미를 가진 규칙입니다. 따라서 라우터나 단순 서비스 함수에 흩뿌리면 안 됩니다.
모든 규칙을 애그리거트 메서드 안에 넣을 수는 없습니다. 아래 같은 경우에는 도메인 서비스가 더 자연스럽습니다.
예를 들어 주문 생성 직전 총액 계산과 결제 가능 조건을 함께 다루는 판단은 하나의 엔티티 메서드로 넣기 어색할 수 있습니다.
예를 들어 "장바구니에서 주문 생성용 스냅샷을 만든다" 같은 로직은 Cart에 넣을 수도 있지만, 복잡해지면 별도 도메인 서비스로 빼는 편이 더 명확할 수 있습니다.
예를 들어 할인까지는 이번 교재에서 안 다루지만, 만약 복잡한 가격 정책이 있다면 이런 것은 도메인 서비스 후보가 됩니다.
중요한 점은 도메인 서비스가 "넣을 데 없어서 만든 유틸"이 되어선 안 된다는 것입니다.
애플리케이션 서비스는 도메인을 대신 판단하는 곳이 아닙니다. 유스케이스를 실행하는 조정자입니다.
예를 들어 place_order 유스케이스를 보면 애플리케이션 서비스는 아래 역할을 합니다.
Cart를 읽는다Order를 생성한다Payment 요청을 연결한다즉 애플리케이션 서비스는 순서를 조합하지만, "무엇이 유효한가"의 핵심 규칙은 도메인에 위임해야 합니다.
실전 코드에서 가장 흔한 문제는 라우터가 너무 똑똑해지는 것입니다.
라우터는 아래 정도만 해야 합니다.
반대로 아래는 라우터가 가져가면 안 됩니다.
이 로직이 라우터에 들어가기 시작하면, 곧 같은 규칙이 다른 엔드포인트에서 복제됩니다.
이제 실제 유스케이스 기준으로 어디에 무엇을 둘지 보겠습니다.
즉 상품 조회는 쓰기 규칙보다 읽기 조합이 중요합니다.
Cart에 상품 추가를 요청한다핵심 규칙은 Cart 쪽에 있어야 합니다.
Cart를 읽는다Order를 만든다Payment 요청 흐름을 호출한다Payment를 갱신한다Order 상태 반영을 조정한다핵심은 결제 결과를 해석해서 주문 상태를 바꾸는 흐름을 애플리케이션 서비스가 조정하되, 상태 전이 자체의 허용 여부는 도메인이 가져야 한다는 점입니다.
이번 교재에서는 챗봇도 읽기 조합과 정책 분리가 중요합니다.
이번 MVP에서 명확하게 생각해 볼 수 있는 도메인 서비스 후보는 아래와 같습니다.
OrderCreationPolicyCartToOrderConverterPaymentResultPolicy물론 프로젝트 규모가 작다면 이들을 모두 클래스화할 필요는 없습니다. 중요한 것은 "이 로직이 도메인 규칙인가, 유스케이스 조정인가"를 구분하는 사고입니다.
이번 교재에서는 대략 아래와 같은 구조를 떠올리면 됩니다.
| 서비스 | 책임 |
|---|---|
CatalogAppService |
상품 조회 유스케이스 |
CartAppService |
장바구니 조회/추가/수정 유스케이스 |
OrderAppService |
주문 생성/조회 유스케이스 |
PaymentAppService |
결제 확인/결과 반영 유스케이스 |
AdminAppService |
운영자 대시보드 조회 유스케이스 |
SupportAppService |
챗봇 응답 유스케이스 |
이 구조는 절대 규칙은 아니지만, 최소한 유스케이스 중심으로 책임을 나누는 데 도움을 줍니다.
서비스 경계를 잘 나눴는지 보려면 아래 질문에 답해 보면 됩니다.
이 질문에 대부분 예라고 답할 수 있다면 경계가 꽤 괜찮은 편입니다.
이 다섯 가지 실수는 뒤의 구현 단계에서 빠르게 코드 비대화로 이어집니다.
이번 교재를 따라 하는 학습자는 최소한 아래 정도는 메모로 남겨 두는 것이 좋습니다.
이 페이지에서 우리는 규칙을 어디에 둘지 정리했습니다. 도메인 모델은 상태 전이와 불변 조건을 지키고, 도메인 서비스는 애매한 규칙 경계를 보완하며, 애플리케이션 서비스는 유스케이스를 조정합니다. 라우터는 얇게 남습니다. 이 기준이 있어야 뒤의 백엔드 구현이 계층 구조를 유지한 채 커질 수 있습니다.
다음 단계에서는 이제 이 도메인 모델을 실제 화면과 사용자 경험 흐름으로 번역하면서, 프론트와 백엔드가 만나는 접점을 설계하기 시작합니다.