지난 편에서 엔티티와 값 객체의 차이를 살펴봤어요. 이번 편에서는 하나의 객체에 속하지 않는 비즈니스 로직을 다루는 도메인 서비스(Domain Service)가 무엇인지, 좋은 도메인 서비스의 특성은 무엇인지, 그리고 애플리케이션 서비스·인프라스트럭처 서비스와 어떻게 다른지를 알아볼게요.
엔티티와 값 객체가 자리를 잡으면, 팀은 곧 실무적인 질문에 부딪혀요.
“이 로직이 하나의 객체에 명확히 속하지 않을 때, 어디에 두어야 하는가?”
여기서 서비스(Service)가 등장해요.
서비스는 자주 오해받는 개념이에요. 도메인 주도 설계에서 서비스는 “또 다른 클래스”가 아니고, 기술적 마이크로서비스와도 다른 개념이에요.
1. 도메인 서비스가 존재하는 이유: 여러 개념에 걸친 비즈니스 결정
서비스가 존재하는 이유는 일부 비즈니스 행동이 단일 객체에 속하지 않기 때문이에요.
엔티티와 값 객체는 특정한 것(Thing)을 모델링하는 데 뛰어나요. 하지만 제품에는 개념적 소유가 아니라 결정과 조율에 관한 행동들도 있어요.
서비스가 보통 적절한 경우는 이래요.
- 연산이 여러 도메인 개념에 걸쳐 있다
- 로직이 하나의 엔티티 안에 있으면 부자연스럽게 느껴진다
- 그 액션이 비즈니스 내러티브의 핵심이다
- 연산이 상태 소유가 아닌 순수 로직이다
로직이 하나의 엔티티 내부 상태만 조작한다면, 먼저 그 엔티티에서 시작하세요.
전형적인 예시로는 여러 검증 후 대출 승인, 가용성 기반 면접관 배정, 계정 간 크레딧 이전, 여러 규칙에 걸친 자격 판단 같은 것이 있어요.
이 액션들은 공통 패턴이 있어요. 하나 이상의 도메인 개념이 관여하고, 비즈니스 결정을 나타내고, 하나의 엔티티에 자연스럽게 속하지 않는다는 거예요.
이런 종류의 행동을 억지로 엔티티에 넣으면, 모델이 휘기 시작해요. 엔티티가 다른 엔티티에 손을 뻗고, 책임이 흐려지고, 도메인 개념의 명확성이 떨어지죠. 서비스는 엔티티를 왜곡하지 않고 이런 로직을 담을 곳을 제공해요.
도메인 서비스를 비유하면, 공증인의 역할과 같아요. 부동산 거래에서 매도인(엔티티 A)과 매수인(엔티티 B)은 각각 자기 재산과 자금을 관리해요. 하지만 “소유권 이전”이라는 행위는 어느 한쪽에 속하지 않죠. 공증인(도메인 서비스)이 양쪽의 조건을 확인하고, 규칙에 따라 거래를 성립시키는 거예요.
2. 좋은 도메인 서비스의 특성: 무상태, 의도 중심, 올바른 계층
모든 함수가 도메인 서비스는 아니에요. 좋은 도메인 서비스는 세 가지 특성을 공유해요.
| 특성 | 실무적 의미 | 왜 중요한가 |
|---|---|---|
| 1. 무상태(Stateless) | 서비스가 오래 지속되는 데이터를 소유하지 않음, 실행 시점에 전달되는 엔티티, 값 객체, 입력에 대해서만 작동 | 책임을 명확히 하고, 서비스가 숨겨진 상태 보유자가 되는 것을 방지 |
| 2. 도메인 의도 표현 | 서비스 이름이 기술적 단계가 아닌 비즈니스 액션을 반영. 좋은 예: scheduleInterview, approveApplication약한 예: processData, handleLogic |
팀원 모두가 도메인 행동을 읽기 쉬워짐, 이름이 의미를 전달 |
| 3. 올바른 계층 | 서비스의 책임이 그것이 사는 계층에 부합 | 비즈니스 규칙이 오케스트레이션이나 인프라스트럭처 코드로 누출되는 것을 방지 |
좋은 도메인 서비스는 구조보다 모델에서의 역할로 정의돼요.
첫째, 도메인 서비스는 의도적으로 무상태예요. 오래 지속되는 데이터를 보유하거나 비즈니스 객체 자체를 나타내지 않아요. 대신 기존 엔티티와 값 객체에 대해 작동하죠.
둘째, 도메인 서비스는 명확히 도메인 의도를 표현해야 해요. 이름과 목적이 내부 기술 단계가 아니라, 제품 이해관계자에게 의미가 있는 비즈니스 액션을 반영해야 해요.
마지막으로, 도메인 서비스는 올바른 계층에 살아야 해요. 서비스는 여러 계층에 존재하지만, 도메인 서비스는 구체적으로 비즈니스 규칙을 강제하는 책임을 져요. 오케스트레이션과 통합 로직을 도메인 계층 밖에 두면 명확한 경계가 유지되고 모델 침식이 줄어들어요.
이 특성들이 함께 작용하면, 서비스가 잘못 배치된 로직의 투기장이 되는 대신에 도메인 모델을 강화하는 역할을 해요.
3. 애플리케이션 서비스 vs 도메인 서비스 vs 인프라스트럭처 서비스
같은 사용자 액션을 각 계층의 서비스가 어떻게 지원하는지 비교해 볼게요.
| 계층 | 서비스의 책임 | 의도적으로 피하는 것 |
|---|---|---|
| 애플리케이션 서비스 | – 유스 케이스를 엔드투엔드로 조율 – 요청을 받고, 도메인 로직을 호출하고, 트랜잭션을 관리하고, 결과를 반환 |
비즈니스 결정을 내리거나 도메인 규칙을 내장하는 것 |
| 도메인 서비스 | 가용성, 자격, 충돌 같이 여러 도메인 개념에 걸친 비즈니스 규칙을 강제 | 플로우 관리, 트랜잭션, 외부 부수 효과 |
| 인프라스트럭처 서비스 | 캘린더, 알림, 서드파티 API 같은 외부 시스템과의 상호작용을 처리 | 비즈니스 규칙을 알거나 강제하는 것 |
4. 예시: 크레딧 이전을 하는 도메인 서비스
팀이 월간 사용 크레딧을 받고, 프로젝트 간에 이전할 수 있는 플랫폼을 생각해 보세요.
┌────────────────────┐ ┌────────────────────┐
│ Project A (엔티티) │ │ Project B (엔티티) │
│--------------------│ │--------------------│
│ credits: 100 │ │ credits: 40 │
└─────────┬──────────┘ └─────────┬──────────┘
│ 30 크레딧 이동 │
└──────────────▶──────────────┘
┌────────────────────┐ ┌────────────────────┐
│ credits: 70 │ │ credits: 70 │
└────────────────────┘ └────────────────────┘
크레딧 이전은 단일 프로젝트의 속성이 아니라 비즈니스 액션이에요. 몇 가지 필수 규칙이 따라오죠.
- 크레딧은 음수가 될 수 없다
- 이전은 전체가 성공하거나 전체가 실패해야 한다
- 양쪽 프로젝트 모두 존재해야 한다
어느 한 프로젝트만으로는 이 규칙을 강제할 수 없어요. 연산은 양쪽이 함께 고려될 때만 의미가 있죠.
크레딧 이전 (하나의 사용자 액션)
─────────────────────────────────
┌──────────────────────────┐
│ 프레젠테이션 계층 │ UI / API
│ "30 크레딧 이전" │
└─────────────┬────────────┘
│ 요청
▼
┌──────────────────────────┐
│ 애플리케이션 계층 │ 유스케이스 조율
│ TransferCreditsUseCase │
│ - 트랜잭션 시작 │
│ - 도메인 서비스 호출 │
└─────────────┬────────────┘
│ 도메인 연산
▼
┌──────────────────────────┐
│ 도메인 계층 │ 비즈니스 규칙 적용
│ CreditTransferService │
│ - 금액 > 0 검증 │
│ - 음수 방지 확인 │
│ - A, B에 변경 적용 │
└─────────────┬────────────┘
│ 영속화 + 부수 효과
▼
┌──────────────────────────┐
│ 인프라스트럭처 계층 │ 기술적 구현
│ ProjectRepository │
│ - A, B 불러오기 │
│ - 업데이트된 잔액 저장 │
│ EventPublisher/Notifier │
└──────────────────────────┘
이것이 크레딧 이전을 도메인 서비스의 자연스러운 후보로 만드는 거예요. 연산이 여러 엔티티에 관여하고, 규칙이 그 엔티티들에 걸쳐 적용되고, 액션이 어느 한 프로젝트에만 속하지 않으니까요.
다음 편에서는 관련 개념을 의미 있게 묶는 모듈(Module)이 무엇이고, 모듈 설계가 제품 조직과 확장에 어떤 영향을 미치는지를 살펴볼게요.
도메인 주도 설계 시리즈

