Domain Logic Patterns
02-organizing-domain-logic|Ch.2에서 큰 그림을 봤다면, 여기서는 각 패턴의 구체적인 동작 방식과 코드 수준의 차이를 다룬다.
Transaction Script
각 비즈니스 트랜잭션을 하나의 프로시저로 구성한다.
호텔 예약이면 bookHotelRoom(), 장바구니 추가면 addToCart(). 각 프로시저가 입력 받기 → 검증 → 계산 → DB 저장 → 응답의 전 과정을 담당한다.
조직 방식 두 가지:
- 관련 Transaction Script들을 하나의 클래스에 모은다 (주제 영역별). 가장 흔하고 단순하다.
- 각 Script를 별도 클래스로 만든다 (Command 패턴). 런타임에 스크립트를 객체로 다룰 수 있다.
핵심 규칙: Transaction Script에서 Presentation 로직을 호출하지 말라. 이래야 테스트와 UI 교체가 쉽다.
언제 쓰는가: 도메인 로직이 단순할 때. 쇼핑몰의 기본 가격 체계 정도면 충분하다. 로직이 복잡해지면 Transaction Script 간에 코드 중복이 생기기 시작하고, 그 시점이 Domain Model로 리팩터링해야 할 신호다.
Domain Model
도메인의 명사들을 중심으로 객체 모델을 구축하고, 각 객체가 관련 로직을 담는다.
Revenue Recognition 예제가 잘 보여준다. Transaction Script에서는 하나의 프로시저가 모든 계산을 if-else로 분기하지만, Domain Model에서는 Product → RecognitionStrategy → RevenueRecognition 객체들이 각자의 몫을 처리하며 협력한다.
Transaction Script 방식:
recognitionService.calculateRecognitions(contractId)
→ 내부에서 if(productType == "WP") ... else if ... 분기
Domain Model 방식:
contract.calculateRecognitions()
→ contract가 product에게 위임
→ product가 recognitionStrategy에게 위임
→ strategy가 RevenueRecognition 객체들을 생성
새로운 제품 유형이 추가되면? Transaction Script는 if-else를 추가하고, Domain Model은 새 Strategy 객체를 추가한다. Strategy 패턴 덕분에 기존 코드를 건드리지 않아도 된다(Open-Closed Principle).
Domain Model의 두 가지 스타일:
- Simple Domain Model: 클래스가 DB 테이블과 거의 1:1. Active Record와 잘 맞는다.
- Rich Domain Model: 상속, Strategy, 복잡한 연관 관계 활용. Data Mapper 필수.
언제 쓰는가: 도메인 로직이 복잡할 때. 일단 Domain Model에 익숙해지면 단순한 문제에도 적용하게 된다. 하지만 팀이 익숙하지 않으면 초기 비용이 크다. Fowler: "한번 감염되면 평생이다."
Table Module
테이블당 하나의 클래스, 인스턴스도 하나. Record Set을 받아서 처리한다.
Domain Model과의 핵심 차이: Domain Model에서 Contract 클래스의 인스턴스는 DB의 각 행마다 하나씩 존재한다. Table Module에서는 Contract 인스턴스가 단 하나이고, 모든 contract 행을 Record Set으로 다룬다.
Domain Model: contract = Contract.find(42) // 하나의 계약 객체
Table Module: contracts = new ContractModule(recordSet) // 모든 계약을 다루는 모듈
contracts.calculateRecognitions(42) // ID로 특정 행 지정
언제 쓰는가: Record Set 중심 프레임워크(.NET 등)를 쓸 때. Record Set 도구가 없으면 Table Module을 쓸 이유가 없다. Fowler: ".NET에서는 Transaction Script 대신 무조건 Table Module."
Service Layer
Domain Model이나 Table Module 위에 놓는 API 계층. Presentation과 도메인의 경계를 명확히 한다.
세 가지 두께:
- Thin Facade (Fowler 추천): Service Layer는 위임만 한다. 트랜잭션 제어, 보안 체크, Use Case 단위의 API 제공이 역할의 전부.
- Controller-Entity: Use Case 고유 로직은 Service에, 공통 로직은 도메인 객체에.
- Rich Service: 대부분의 로직이 Service에. 도메인 객체는 거의 데이터 홀더.
Fowler의 조언: "가능한 한 얇게 유지하라. 처음에는 없이 시작하고, 필요하면 추가하라." 하지만 Service Layer를 항상 두텁게 쓰는 훌륭한 설계자도 있으니 절대적 규칙은 아니다.