research

Domain Logic Patterns

Domain Logic Patterns

02-organizing-domain-logic|Ch.2에서 큰 그림을 봤다면, 여기서는 각 패턴의 구체적인 동작 방식과 코드 수준의 차이를 다룬다.

Transaction Script

각 비즈니스 트랜잭션을 하나의 프로시저로 구성한다.

호텔 예약이면 bookHotelRoom(), 장바구니 추가면 addToCart(). 각 프로시저가 입력 받기 → 검증 → 계산 → DB 저장 → 응답의 전 과정을 담당한다.

조직 방식 두 가지:

  1. 관련 Transaction Script들을 하나의 클래스에 모은다 (주제 영역별). 가장 흔하고 단순하다.
  2. 각 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과 도메인의 경계를 명확히 한다.

세 가지 두께:

  1. Thin Facade (Fowler 추천): Service Layer는 위임만 한다. 트랜잭션 제어, 보안 체크, Use Case 단위의 API 제공이 역할의 전부.
  2. Controller-Entity: Use Case 고유 로직은 Service에, 공통 로직은 도메인 객체에.
  3. Rich Service: 대부분의 로직이 Service에. 도메인 객체는 거의 데이터 홀더.

Fowler의 조언: "가능한 한 얇게 유지하라. 처음에는 없이 시작하고, 필요하면 추가하라." 하지만 Service Layer를 항상 두텁게 쓰는 훌륭한 설계자도 있으니 절대적 규칙은 아니다.

패턴 간 관계 정리