research

Domain Logic Patterns — 실전 예시와 템플릿

Domain Logic Patterns — 실전 예시와 템플릿

이론: 04-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