우리 코딩 표준은 코드를 두 군데에 나눠 넣는다. 애그리거트의 메서드에는 「주문 수량은 1 이상이어야 한다」처럼 그 애그리거트 하나만 보면 판단되는 규칙을 넣는다(애그리거트 경계 표준 원칙 3). application 패키지의 XxxService에는 DB에서 꺼내고, 저장하고, @Transactional로 묶는 작업 순서를 넣는다(패키지 구조 표준 원칙 2).
그런데 이 두 곳 어디에도 딱 맞지 않는 규칙이 있다. 계좌 이체가 그렇다.
A 계좌에서 B 계좌로 1만 원을 보낸다.
이 일에는 계좌가 둘 나온다. A의 메서드에 넣자니 B가 걸리고, AccountService에 넣자니 업무 규칙이 DB 코드 사이에 섞인다. DDD는 이런 규칙을 담는 자리를 도메인 서비스(domain service)라고 부른다.
이름에 「서비스」가 똑같이 붙어서 헷갈린다. 이 글이 답하는 질문은 하나다 — 도메인 서비스는 애그리거트의 메서드나 application Service와 무엇이 다른가. 어느 패키지에 어떤 이름으로 둘지는 이 글에서 정하지 않는다. 그건 도메인 서비스 표준에서 정했다.
이체 규칙을 한쪽 계좌의 메서드에 넣으면 그 계좌가 남의 계좌의 새 상태까지 만들어 내게 된다
가장 먼저 떠오르는 것은 이체를 계좌의 메서드로 만드는 것이다. 우리 코드에서 애그리거트의 대표 객체(애그리거트 루트)는 XxxDomain 클래스다. 계좌라면 AccountDomain이다.
// 안 A — 보내는 계좌의 메서드로 만든다
public class AccountDomain {
public TransferResult transferTo(AccountDomain target, long amount) {
// 잔액을 뺀 새 내 계좌와, 잔액을 더한 새 target 계좌를 함께 돌려준다
}
}
a.transferTo(b, 10000) 한 줄로 읽혀서 쓰기 편하다. 「이체」라는 말이 코드에 그대로 남는 것도 장점이다.
버린 이유는 이것이다. 이체는 A의 일도 B의 일도 아니다. 둘 사이에서 일어나는 행위다. 이 메서드를 A에 두면 「왜 보내는 계좌가 받는 계좌의 새 잔액까지 만들어 내지?」라는 물음이 남는다. B에 receiveFrom(a, amount)을 두어도 똑같이 어색하다. 주인이 없는 일을 억지로 한쪽에 맡긴 것이다.
DDD라는 말을 처음 만든 에릭 에반스가 바로 이 점을 들어 도메인 서비스를 꺼냈다. 에반스가 직접 요약해 공개한 『Domain-Driven Design Reference』(2015) 14쪽의 Services 절은 이렇게 시작한다.
Sometimes, it just isn’t a thing. … Forcing the required domain functionality to be the responsibility of an entity or value either distorts the definition of a model-based object or adds meaningless artificial objects.
「때로는 그건 ‘물건’이 아니다. 그 일을 엔티티나 값 객체에 억지로 맡기면 그 객체의 정의가 비틀리거나, 아무 뜻 없는 인공 객체를 더 만들게 된다」는 말이다. 엔티티와 값 객체는 여기서 「애그리거트를 이루는 객체」 정도로 읽으면 된다.
처음에는 둘째 이유로 우리 애그리거트 경계 표준 원칙 4(다른 애그리거트는 ID로 참조한다)와도 부딪친다고 봤다. 다시 열어 보니 원칙 4는 다른 애그리거트를 필드로 객체째 들고 있지 말라는 규칙이다. 메서드가 매개변수로 잠깐 받는 것까지 막지는 않는다. 그래서 안 A를 버린 이유는 「주인이 없다」 하나다.
이체 규칙을 application Service에 넣으면 규칙이 작업 순서 사이에 묻힌다
두 번째 안은 AccountService에 다 넣는 것이다. 지금 우리 표준에는 규칙을 둘 곳이 Domain과 XxxService 둘뿐이라, Domain 하나에 안 맞으면 자연스럽게 이쪽으로 온다.
// 안 B — application Service 에 규칙까지 넣는다
@Service
class AccountService implements TransferUseCase {
@Transactional
public void transfer(TransferCommand command) {
AccountDomain from = accountRepository.findById(command.getFromId());
AccountDomain to = accountRepository.findById(command.getToId());
if (from.getId().equals(to.getId())) { // 규칙
throw new ErrorCodeException(AccountErrorCode.SAME_ACCOUNT);
}
if (!from.getCurrency().equals(to.getCurrency())) { // 규칙
throw new ErrorCodeException(AccountErrorCode.CURRENCY_MISMATCH);
}
accountRepository.save(from.withdraw(command.getAmount()));
accountRepository.save(to.deposit(command.getAmount()));
}
}
- 앞의 두 줄은 Repository로 계좌 둘을 DB에서 꺼낸다
- 가운데 두
if가 이체 규칙이다. 같은 계좌끼리는 보낼 수 없고, 통화가 다르면 보낼 수 없다 - 끝의 두 줄은 A에서 빼고 B에 더한 새 계좌를 저장한다. 우리
Domain은 한번 만들면 값을 바꾸지 않는 불변 객체라서withdraw·deposit이 값을 고치지 않고 새 객체를 돌려준다 withdraw안에는 「잔액보다 많이 뺄 수 없다」가 들어 있다. 이 규칙은 A 하나만 보면 판단되니 애그리거트 메서드의 몫이다
동작은 한다. 파일이 하나 덜 생기는 것도 장점이다.
버린 이유는 규칙과 작업 순서가 한 메서드에 섞인다는 것이다. 지금은 if가 둘이라 잘 보이지만, 「하루 이체 한도」 「휴면 계좌는 받을 수 없다」 같은 규칙이 늘어나면 그 사이사이에 꺼내기 · 저장 · 알림 코드가 끼어든다. 그때 「이체 규칙이 몇 개고 무엇인가」를 알려면 DB 코드까지 다 읽어야 한다. 규칙만 따로 단위 테스트로 시험해 보려 해도, 이 메서드는 첫 줄에서 Repository로 계좌를 꺼낸다. 그래서 계좌를 돌려주는 가짜 Repository부터 만들어 끼워야 한다. if 두 줄을 보려고 DB 쪽 준비부터 하는 셈이다.
도메인 서비스는 둘에 걸친 규칙만 담고 DB와 Spring을 모른다
세 번째 안은 이체 규칙만 따로 떼어 클래스 하나에 두는 것이다. 이 클래스가 도메인 서비스다. 이름은 AccountTransfer라고 붙인다. 하는 일 그대로 짓고 Service를 붙이지 않는다는 것은 도메인 서비스 표준에서 정했다.
// 안 C — 둘에 걸친 규칙만 도메인 서비스로 뗀다
public class AccountTransfer { // Spring 애노테이션도 Repository 도 없다
public TransferResult transfer(AccountDomain from, AccountDomain to, long amount) {
if (from.getId().equals(to.getId())) {
throw new ErrorCodeException(AccountErrorCode.SAME_ACCOUNT);
}
if (!from.getCurrency().equals(to.getCurrency())) {
throw new ErrorCodeException(AccountErrorCode.CURRENCY_MISMATCH);
}
return new TransferResult(from.withdraw(amount), to.deposit(amount));
}
}
- 계좌 둘과 금액을 받는다. DB에서 꺼내 오지 않고, 이미 꺼낸 것을 받기만 한다
- 계좌 둘을 같이 봐야 판단되는 규칙 두 개를 검사한다
- 빼고 더한 결과 두 개를 돌려준다. 저장은 하지 않는다
그러면 AccountService에는 작업 순서만 남는다.
@Transactional
public void transfer(TransferCommand command) {
AccountDomain from = accountRepository.findById(command.getFromId());
AccountDomain to = accountRepository.findById(command.getToId());
TransferResult result = accountTransfer.transfer(from, to, command.getAmount());
accountRepository.save(result.getFrom());
accountRepository.save(result.getTo());
}
꺼내고 → 규칙을 부르고 → 저장한다. 이체 규칙이 무엇인지 알고 싶으면 AccountTransfer 하나만 열면 된다. 규칙을 시험할 때도 DB 없이 new AccountTransfer()에 계좌 둘을 넣어 보면 된다. AccountService는 accountTransfer를 직접 new로 만들어 필드에 둔다. 도메인 서비스에는 Spring 애노테이션이 없어서 Spring이 넣어 주지 않는다.
안 A는 주인 없는 일을 한쪽 계좌에 맡기고, 안 B는 규칙을 작업 순서 사이에 묻는다. 안 C는 규칙을 어느 계좌에도 맡기지 않으면서 작업 순서에서도 떼어 낸다. 그래서 안 C를 도메인 서비스의 모양으로 잡았다.
대가는 클래스 AccountTransfer가 하나 더 생기고, 결과 두 개를 함께 돌려줄 TransferResult 같은 상자도 필요해진다는 것이다. 규칙이 if 하나뿐이면 그 하나를 보려고 AccountService와 AccountTransfer 두 파일을 오가야 한다. DB 없이 시험할 수 있다는 이득은 그때도 남지만, 읽는 쪽이 치르는 비용이 그보다 클 수 있다. 언제부터 떼는지는 아직 정하지 않았다.
여기서 하나 오해하기 쉬운 것이 있다. 처음에 나는 「도메인 코드는 아무것에도 의존하면 안 되는데, 둘을 같이 다뤄야 하니 어쩔 수 없이 등장한 것」이라고 이해했다. 반은 맞다. 둘을 같이 다뤄야 하는 업무 규칙 때문에 나온 것은 맞다. 하지만 「도메인은 의존하지 않는다」를 깨고 나온 것은 아니다. 여기서 「의존하지 않는다」는 DB · Spring 같은 바깥 기술을 모른다는 뜻이다(패키지 구조 표준 원칙 6이 검사한다). 같은 도메인의 AccountDomain을 쓰는 것은 괜찮다. AccountTransfer도 POJO이고, DB도 Spring도 모른다. 도메인 서비스는 그 규칙을 지키면서 규칙을 둘 자리를 하나 더 만든 것이다.
도메인 서비스와 application Service는 DB와 트랜잭션을 지워 보면 갈린다
안 C를 보면 AccountTransfer와 AccountService가 둘 다 「이체」를 다룬다. 게다가 앞의 것을 부르는 이름이 「도메인 서비스」라서, 새 코드를 짤 때 그 코드가 어느 쪽인지 헷갈리는 순간이 온다.
그때 쓰는 판별법이 하나 있다. 그 코드에서 DB에서 꺼내기, 저장하기, 트랜잭션을 전부 지워 본다.
- 지우고 나서도 「같은 계좌끼리는 안 된다」처럼 은행 직원이 코드를 몰라도 ‘그건 규칙’이라고 알아듣는 판단이 남으면 그 부분은 도메인 서비스(또는 애그리거트 메서드)의 것이다
- 지우고 나니 아무것도 안 남거나, 남은 것이 규칙을 부르기만 하는 줄뿐이면 그 코드는 처음부터 작업 순서였다. application Service의 것이다
안 C의 AccountService.transfer에서 꺼내기와 저장을 지우면 accountTransfer.transfer(...) 호출 한 줄만 남는다. 이 줄은 규칙을 부를 뿐 스스로 무엇을 판단하지 않는다. 그래서 application Service다. AccountTransfer.transfer에는 지울 것이 처음부터 없다. if 둘이 그대로 남고, withdraw · deposit 호출도 DB와 상관없는 계산이라 남는다. 그래서 도메인 서비스다.
우리 코드에는 그만둔 관리사를 계약에 배정하지 못하게 막는 규칙이 두 도메인에 걸쳐 있다
계좌 이체는 설명을 위해 만든 장면이다. 그래서 우리가 실제로 만드는 산후조리 관리 서비스의 코드에도 애그리거트 둘에 걸친 규칙이 있는지 찾아봤다(2026-11-04 코드 기준). 찾은 범위 안에서 둘이 있었다. 먼저 상태를 바꿀 때의 규칙 하나를 자세히 본다.
관리자가 산모의 돌봄 계약에 관리사를 배정하려 한다. 그런데 그 관리사가 이미 그만둔 사람이면 배정을 막아야 한다.
여기 나오는 두 애그리거트는 돌봄 계약(CareContractDomain)과 관리사(CaregiverDomain)다. 계좌 이체에서는 같은 종류의 애그리거트 둘(계좌 A와 계좌 B)이었는데, 여기서는 종류가 다른 애그리거트 둘이다. 그리고 둘은 서로 다른 도메인 패키지(carecontract · caregiver)에 산다. 우리 구조는 계약 · 관리사 · 산모처럼 업무 단위마다 패키지를 하나씩 둔다(패키지 구조 표준 원칙 1). 이 두 패키지에는 애그리거트 루트가 하나씩만 있어서, 애그리거트의 종류가 다르면 패키지도 다르다. 이 점이 뒤에서 중요해진다.
「배정」은 계약 쪽의 일이고, 「그만뒀나」는 관리사 쪽이 아는 정보다. 그래서 이 규칙을 판단하려면 두 애그리거트를 같이 봐야 한다. 이 규칙은 지금 계약 쪽의 application Service에 있다.
// CareContractService — 계약 도메인의 application Service
/** 그만둔 사람인지를 계약 애그리거트가 아니라 여기서 봅니다. 관리사 명단의 상태라 계약이 모르는 값입니다. */
private CaregiverSnapshot findWorkingCaregiver(long caregiverNo) {
CaregiverSnapshot caregiver = caregiverProvider.findByCaregiverNo(caregiverNo)
.orElseThrow(() -> ErrorCodeException.of(ErrorCode.CAREGIVER_NOT_FOUND));
if (!caregiver.isWorking()) {
throw ErrorCodeException.of(ErrorCode.CARE_CONTRACT_CAREGIVER_RESIGNED);
}
return caregiver;
}
- 주석은 원래 코드에 있던 것을 줄여 옮겼다. 이 자리에 둔 이유를 코드를 쓴 사람이 직접 적어 두었다
- 첫 줄은 관리사 도메인이 공개한 창구(
caregiverProvider)에 관리사 한 명의 정보를 묻는다. 관리사가 없으면 예외를 던진다. 돌아오는CaregiverSnapshot은 관리사 애그리거트의 그 순간 상태를 베껴 둔 읽기 전용 상자다. 상자이긴 해도 판단 재료는 관리사 애그리거트의 상태다 if가 규칙이다.isWorking()은 일하는 중이면true, 그만둔 관리사면false를 돌려준다. 그래서false면 「그만둔 관리사」 예외를 던진다
이것을 앞 섹션의 판별법에 넣어 본다. 판별법에서 DB에서 꺼내기를 지운 것은, 그 줄이 무엇을 판단하는 것이 아니라 판단할 재료를 가져오는 일이라서였다. 관리사 도메인에 묻는 첫 줄도 같은 일이다. 그 창구 뒤에서 결국 관리사를 DB에서 꺼내 온다. 그러니 이 줄을 「재료는 이미 손에 있다」고 치고 지워도, 가져온 관리사로 판단하는 if는 남는다. 판단하는 규칙이 application Service 안에 있는 것이다. 이 글이 버린 안 B의 자리다. 반대로 계약 하나만 보면 되는 규칙(배정 횟수 상한, 같은 관리사를 다시 배정하지 않기, 계약 기간 안의 날짜인가)은 계약 도메인 객체의 assign 메서드 안에 있다. 앞에서 세운 기준과 맞다.
그런데 이 규칙은 안 C의 모양 그대로, 곧 domain 패키지에 두고 애그리거트 둘을 받는 클래스로는 떼어 낼 수 없다. 패키지 구조 표준 원칙 4는 다른 도메인을 application 층에서만, 그 도메인이 공개한 Provider와 Snapshot으로만 만나게 한다. 그래서 계약의 domain 패키지는 관리사 타입을 아예 볼 수 없다. 막힌 것은 domain 패키지라는 자리다. 도메인 서비스를 다른 자리에 두는 길까지 막힌 것인지는 아직 모른다. 도메인 서비스 표준은 이런 규칙을 받는 쪽 애그리거트의 메서드에 두기로 했다. 관리사 쪽 사실은 boolean 같은 값으로만 받는다.
두 번째 사례는 조회할 때 거르는 규칙이다. 관리자 화면의 사이드바에는 로그인한 관리자가 볼 수 있는 메뉴만 나와야 한다. 이것을 판단하려면 메뉴(adminmenu)의 「사이드바에 노출하나」와, 관리자 역할(adminrole)의 「최고 관리자인가 · 이 메뉴를 볼 수 있는 역할인가」를 같이 봐야 한다. 이 규칙도 메뉴 쪽 application Service(AdminMenuService)에 있고, 역할 도메인의 Snapshot을 받아 if로 거른다. 관리사 배정과 모양이 같다.
두 사례 모두 「애그리거트 둘」이 곧 「도메인 둘」이었다. 계좌 이체처럼 같은 도메인 안의 애그리거트 둘에 걸친 규칙은 이번에 찾지 못했다. 사례가 둘뿐이라 우리 코드 전체가 늘 그렇다고까지 말할 수는 없다. 도메인 서비스 표준은 도메인 경계를 넘는 이 두 사례를 기준으로 자리를 정했다.
찾은 범위도 적어 둔다. 애그리거트 둘에 걸친 규칙이라면 application Service가 두 애그리거트의 재료를 쥐고 있어야 한다. 그래서 그런 Service를 먼저 골라 냈다.
- 다른 도메인의
Provider를 받아 쓰는 Service가 18개였다. 다른 도메인의 재료를 쥔 Service다 - Repository를 둘 이상 쥔 Service가 8개였다. 같은 도메인 안의 애그리거트 둘을 쥘 수 있는 Service다. 쥔 Repository가 어느 애그리거트 것인지 봤더니, 8개 모두 한 애그리거트의 저장용과 검색용 짝이었다. 우리 표준은 저장 · 단건 조회와 조건 검색을 Repository 둘로 나눈다(애그리거트 경계 표준 원칙 2). 그래서 같은 도메인 안에서 애그리거트 둘을 쥔 Service는 없었다
그다음 application 전체에서 if (!x.isXxx())처럼 is나 has로 시작하는 메서드로 판단하는 if 줄을 모두 뽑았다. 그 줄들을 열어서, 판단 재료가 다른 도메인의 Snapshot인 것을 골랐다. 위 두 사례가 여기서 나왔다. 18개 Service를 한 줄씩 다 읽은 것은 아니다. 그래서 숫자를 비교하는 if처럼 이 꼴이 아닌 규칙은 놓쳤을 수 있다.
판단 기준 정리
| 질문 | 답 | 넣는 곳 |
|---|---|---|
| 이 규칙을 판단하려면 무엇을 봐야 하나 | 애그리거트 하나의 상태만 | 그 애그리거트의 메서드 (from.withdraw 안의 「잔액보다 많이 뺄 수 없다」) |
| 애그리거트 둘 이상을 같이 보고, 둘 다 바뀐다 | 도메인 서비스 (AccountTransfer의 「같은 계좌끼리는 안 된다」) |
|
| 둘 이상을 같이 보지만 바뀌는 것은 하나 | 바뀌는 쪽의 메서드. 다른 쪽은 사실만 넘긴다 (도메인 서비스 표준 원칙 1) | |
| 규칙이 아니라 꺼내고 · 저장하고 · 묶는 순서 | application Service (AccountService) |
|
| 헷갈리면 | DB · 저장 · 트랜잭션을 지워 보고 판단하는 규칙이 남는지 본다 | 남으면 도메인 쪽, 규칙을 부르는 줄만 남으면 application Service |
이 개념을 정리하기까지
시작은 질문 큐의 첫 질문이었다. 「애그리거트 하나에 안 들어가는 규칙을 어디에 어떤 이름으로 두나」를 세 질문으로 나눴고, 그 첫째가 「도메인 서비스는 무엇이 다른가」였다. 어디에 둘지 정하기 전에 그것이 무엇인지부터 정해야 했다.
처음 설명은 너무 앞서 나가서 통하지 않았다. 나는 처음에 세 칸짜리 표와 에반스의 정의부터 내밀었다. 읽는 쪽에서 「지금 뭘 공부하려는 거고, 이해가 안 됐다」는 답이 왔다. 그래서 「규칙 코드를 어느 클래스에 넣나」라는 질문으로 다시 시작했다. 지금 우리가 코드를 두 군데에 나눠 넣는다는 데서 출발해, 계좌 이체라는 장면 하나로 둘 다 안 맞는 경우를 먼저 보였다. 이 글의 도입이 그 순서를 따른다.
결과를 어디에 남길지 두 번 바꿨다. 처음 계획은 용어 페이지 한 장이었다. 그런데 용어 페이지는 한두 줄짜리 뜻풀이라서 「왜 이렇게 갈랐나」를 담을 자리가 없다. 공부해서 나온 결과는 글로 가야 한다고 정했다. 그다음에는 #200에서 쓸 코딩 표준 글을 오늘 미리 만들려 했다. 하지만 규칙 파일이 없는 표준 글은 원칙 없이 배포된 채 #200까지 남는다. 그래서 「짧은 뜻으로 안 되는 개념은 concept 글로 쓴다」는 블로그 규칙대로 이 글을 개념 글로 쓰고, 표준은 #200에서 따로 쓰기로 했다. 용어 페이지 /term/domain-service에는 한 줄 뜻과 이 글로 가는 링크만 둔다.
「어쩔 수 없이 의존을 깨고 나온 것」이라는 이해를 반만 받았다. 이 대목은 앞 섹션 끝에 그대로 적었다. 이때 나는 안 A를 버릴 이유를 둘로 갈랐다. 「이체는 주인이 없다」는 에반스의 이유이고, 「다른 애그리거트는 ID로만 가리킨다」는 우리 표준에서 생기는 이유라고 했다.
둘째 이유는 글을 검토하다가 뺐다. 원칙 4를 다시 열어 보니 다른 애그리거트를 필드로 들고 있지 말라는 규칙이었다. 매개변수로 받는 것은 막지 않는다. 이 이유가 맞다면 계좌 둘을 받는 안 C도 똑같이 어기게 된다. 그래서 안 A를 버린 이유는 「주인이 없다」 하나만 남았다.
예를 쓰다가 내 설명 하나를 고쳤다. 대화에서는 「A 잔액이 모자라면 안 된다」를 계좌 둘에 걸친 규칙으로 들었다. 코드로 옮겨 보니 그 규칙은 A 하나만 보면 판단된다. A의 withdraw 안에 두면 되는 애그리거트 메서드의 규칙이다. 그래서 글에서는 정말로 둘을 같이 봐야 하는 규칙(같은 계좌끼리는 안 된다, 통화가 같아야 한다)으로 예를 바꿨다.
우리 코드에서 찾은 사례는 애그리거트 둘이 곧 도메인 둘이었다. 계좌 이체는 같은 도메인 안의 이야기였다. 실제로 찾은 두 규칙(관리사 배정, 사이드바 메뉴 거르기)은 모두 다른 도메인에 걸쳐 있었다. 처음에는 하나만 찾았다고 했는데, 검토 중에 if 목록을 다시 열어 보고 메뉴 쪽을 더 찾았다. 우리 표준은 domain 패키지끼리 서로 못 보게 막으므로, 안 C의 모양을 그대로 쓸 수 없다. 이 발견은 #200의 출발점으로 넘긴다.
근거는 에반스의 원문으로 확인했고, 확인하지 못한 것은 뺐다. 『Domain-Driven Design Reference』(2015)의 Services 절은 열어서 읽었다. 반면 2003년 책 『Domain-Driven Design』 5장에 있다고 기억하는 내용, 곧 도메인 서비스의 세 특징(그중 하나가 「상태를 들고 있지 않다」)은 책을 열어 보지 못했다. 그래서 이 글에서는 그 부분을 에반스의 말로 쓰지 않았다. 계좌 이체도 에반스 책의 예로 기억하지만, 이 글에서는 내가 든 예로 썼다.
도메인 서비스에서 아직 안 본 것
- 두 도메인에 걸친 규칙의 자리와 도메인 서비스의 자리 · 이름. 도메인 서비스 표준에서 정했다. 다른 도메인의 사실은 받는 쪽 애그리거트가 값으로 받아 판단하고, 도메인 서비스는 같은 도메인 안의 애그리거트 둘에 걸친 규칙에만 쓴다. 규칙이 몇 개부터 떼어 낼지는 아직 정하지 않았다
- 도메인 서비스가 Repository를 받아도 되나. 이 글은 「이미 꺼낸 것을 받기만 한다」로 썼지만, 그게 언제나 맞는지는 따져 보지 않았다
- Reference의 Services 절이 말하는 「서비스 계약(service contract)」. 에반스는 서비스마다 상호작용에 대한 단언(assertion)을 적으라고 한다. 우리 Service 계층 assert 표준과 같은 이야기인지는 보지 않았다
정리
- 규칙 하나를 판단하려고 무엇을 봐야 하는지로 가른다. 애그리거트 하나면 그 메서드에, 둘 이상이 함께 바뀌면 도메인 서비스에, 규칙이 아니라 순서면 application Service에 둔다. 둘 이상을 봐도 바뀌는 것이 하나면 그 쪽 메서드다
- 도메인 서비스는 주인 없는 업무 규칙의 자리다. 한쪽 애그리거트에 억지로 넣으면 그 객체가 남의 새 상태까지 만들어 내게 된다
- 도메인 서비스도 도메인 코드다. DB · Spring · 트랜잭션을 모른다. 이 글의 안 C는 이미 꺼낸 애그리거트를 받아 규칙만 판단한다
- 헷갈리면 DB · 저장 · 트랜잭션을 지워 본다. 판단하는 규칙이 남으면 도메인 쪽, 규칙을 부르는 줄만 남으면 application Service다
- 우리 코드에도 애그리거트 둘에 걸친 규칙이 있다. 관리사 배정과 사이드바 메뉴 거르기 둘이고, 둘 다 다른 도메인에 걸쳐 있어 지금은 application Service에 있다
자신만의 철학을 만들어가는 중입니다.
댓글남기기