Domain Logic Patterns — 실전 예시와 템플릿
예시: Revenue Recognition을 통한 패턴 비교
Fowler가 책 전체에서 반복 사용하는 예제. "소프트웨어 제품을 팔면, 매출 인식을 언제 어떻게 하는가?"
- Word Processor: 계약 체결일에 전액 인식
- Spreadsheet: 1/3 즉시, 1/3 60일 후, 1/3 90일 후
- Database: 1/3 즉시, 1/3 30일 후, 1/3 60일 후
Transaction Script로 구현
class RecognitionService:
gateway: ContractGateway
calculateRevenueRecognitions(contractId):
contract = gateway.findContract(contractId)
totalRevenue = contract.revenue
recognitionDate = contract.dateSigned
productType = contract.product.type
if productType == "W": // Word Processor
gateway.insertRecognition(contractId, totalRevenue, recognitionDate)
elif productType == "S": // Spreadsheet
amount = totalRevenue / 3
gateway.insertRecognition(contractId, amount, recognitionDate)
gateway.insertRecognition(contractId, amount, recognitionDate + 60days)
gateway.insertRecognition(contractId, amount, recognitionDate + 90days)
elif productType == "D": // Database
amount = totalRevenue / 3
gateway.insertRecognition(contractId, amount, recognitionDate)
gateway.insertRecognition(contractId, amount, recognitionDate + 30days)
gateway.insertRecognition(contractId, amount, recognitionDate + 60days)
새 제품 유형 추가 시: if-elif 분기 추가. 5개가 되면 읽기 어렵고, 20개가 되면 악몽.
Domain Model + Strategy로 구현
class Contract:
calculateRecognitions():
product.calculateRecognitions(this) // Product에게 위임
class Product:
calculateRecognitions(contract):
recognitionStrategy.calculateRecognitions(contract) // Strategy에게 위임
class ThreeWayRecognitionStrategy:
firstGap: int // 30 or 60
secondGap: int // 60 or 90
calculateRecognitions(contract):
amount = contract.revenue / 3
contract.addRecognition(amount, contract.dateSigned)
contract.addRecognition(amount, contract.dateSigned + firstGap)
contract.addRecognition(amount, contract.dateSigned + secondGap)
새 제품 유형 추가 시: 새 Strategy 클래스 추가. 기존 코드 수정 없음.
// Spreadsheet: ThreeWayRecognitionStrategy(60, 90)
// Database: ThreeWayRecognitionStrategy(30, 60)
// 새 제품: 새 Strategy 클래스 또는 기존 Strategy에 다른 파라미터
Service Layer 추가
Service Layer는 트랜잭션 제어만 하고, 비즈니스 로직은 도메인 객체에 위임한다.
템플릿
Transaction Script 템플릿
// ============================================
// [기능명] Transaction Script
// UC: [유스케이스명]
// SSD Event: [시스템 이벤트명]
// ============================================
class [기능영역]Service:
[entity1]Gateway: [Entity1]Gateway
[entity2]Gateway: [Entity2]Gateway
// --- Main Script ---
[동사][명사]([파라미터1], [파라미터2]):
// 1. 조회
[data1] = [entity1]Gateway.findBy[조건]([파라미터1])
[data2] = [entity2]Gateway.findBy[조건]([파라미터2])
// 2. 검증
if not [유효성조건]:
throw [예외]
// 3. 비즈니스 로직
[결과] = [계산]
// 4. 저장
[entity1]Gateway.update([변경사항])
// 또는
[entity2]Gateway.insert([새데이터])
// 5. 반환
return [결과]
Domain Model 설계 템플릿
Domain Model 설계 체크리스트
## [프로젝트명] Domain Model 점검
### 책임 분배
- [ ] 각 클래스가 자기 데이터에 관한 로직을 직접 수행하는가? (Information Expert)
- [ ] if-else로 타입을 분기하는 곳이 있는가? → Strategy 패턴 후보
- [ ] 외부 시스템 호출이 도메인 클래스 안에 있는가? → Gateway로 분리
### 복잡도 관리
- [ ] 메서드가 10줄 이상인 곳이 있는가? → 협력 객체에게 위임 검토
- [ ] 한 클래스의 public 메서드가 7개 이상인가? → 역할 분리 검토
- [ ] 순환 의존이 있는가? → 중간 객체 도입 또는 인터페이스 분리
### 테스트 용이성
- [ ] 외부 의존 없이 도메인 로직을 테스트할 수 있는가?
- [ ] Strategy를 Mock으로 교체할 수 있는가?
Service Layer 템플릿
// ============================================
// [유스케이스명] Service (Thin Facade)
// ============================================
class [유스케이스]Service:
[mapper/repository 필드들]
[유스케이스동사]([입력]) -> [출력]:
// 1. 트랜잭션 시작
tx = beginTransaction()
try:
// 2. 도메인 객체 획득
[obj] = [mapper].find([id])
// 3. 도메인 로직 실행 (여기서 로직을 직접 쓰지 않는다!)
result = [obj].[비즈니스메서드]([파라미터])
// 4. 변경 저장
[mapper].update([obj])
// 5. 커밋
tx.commit()
return result
catch Exception as e:
tx.rollback()
throw