research

Data Source Architectural Patterns

Data Source Architectural Patterns

도메인 로직을 어떻게 구조화할지 결정했으면, 다음 질문은 "그 객체들을 DB에 어떻게 연결할 것인가"다. 이 선택은 02-organizing-domain-logic|도메인 로직 패턴의 선택에 종속된다.

Table Data Gateway

DB 테이블에 대한 Gateway. 인스턴스 하나가 테이블 전체를 담당한다.

하나의 클래스가 특정 테이블에 대한 모든 SQL을 캡슐화한다: find(), findByName(), insert(), update(), delete(). 나머지 코드는 SQL을 전혀 몰라도 된다.

PersonGateway
  + findAll() : RecordSet
  + findWithLastName(name) : RecordSet
  + update(id, lastname, firstname, ...)
  + insert(lastname, firstname, ...) : long
  + delete(id)

반환 방식 선택지:

  • Record Set 반환 (가장 흔함, .NET 환경에서 특히 강력)
  • Data Transfer Object 반환 (더 명시적이지만 작성할 코드가 늘어남)
  • Map 반환 (비추 — 컴파일 타임 검증 불가)

궁합: Table Module과 천생연분. Transaction Script와도 잘 맞는다. Domain Model과는 덜 어울린다.

Row Data Gateway

DB 행 하나에 대한 Gateway. 쿼리 결과의 각 행마다 인스턴스가 하나 생긴다.

각 객체가 정확히 하나의 DB 행에 대응하며, 그 행의 컬럼에 대한 getter/setter를 제공한다. 도메인 로직은 포함하지 않는다 — 순수하게 데이터 접근만.

PersonGateway
  - lastName: String
  - firstName: String
  - numberOfDependents: int
  + getLastName() : String
  + setLastName(name)
  + insert()
  + update()
  // 도메인 로직 없음

finder 메서드 위치: 행 기반이라 인스턴스 메서드로는 못 만든다. static 메서드로 하면 테스트 시 대체가 어려우니, 별도의 Finder 클래스를 만드는 것이 좋다.

궁합: Transaction Script와 잘 맞는다. Domain Model에는 Active Record나 Data Mapper가 더 낫다.

Active Record

Row Data Gateway + 도메인 로직. 행 하나에 대한 데이터 접근과 비즈니스 로직을 모두 담는다.

Row Data Gateway에서 출발해서 비즈니스 로직을 추가한 것이다. Person 객체가 find(), insert(), update()도 하면서 getExemption() 같은 비즈니스 계산도 한다.

Person
  - lastName: String
  - firstName: String  
  - numberOfDependents: int
  + find(id) : Person          // DB 접근
  + insert()                    // DB 접근
  + update()                    // DB 접근
  + getExemption() : Money      // 도메인 로직!

Active Record vs Row Data Gateway: 유일한 차이는 도메인 로직의 유무. 경계가 항상 선명하지는 않다.

한계: 클래스와 테이블이 1:1로 대응해야 한다. 상속, Strategy 패턴, 복잡한 연관 관계를 쓰기 시작하면 Active Record가 감당하기 어려워진다. 이 시점이 Data Mapper로 전환해야 할 신호.

진화 경로: Transaction Script에서 코드 중복이 보이기 시작하면, 테이블을 Gateway로 감싸고 → 공통 로직을 Gateway에 옮기면 → 자연스럽게 Active Record가 된다.

Data Mapper

도메인 객체와 DB 사이에 독립적인 매핑 계층을 둔다. 양쪽이 서로의 존재를 전혀 모른다.

가장 복잡하지만 가장 강력한 분리를 제공한다.

PersonMapper
  + find(id) : Person
  + insert(person)
  + update(person)
  // 내부에서 Person ↔ DB 변환 로직

Person
  - lastName: String
  - firstName: String
  + getExemption() : Money
  // SQL 코드 전혀 없음, DB 존재를 모름

동작 흐름:

  1. 클라이언트가 mapper.find(id) 호출
  2. Mapper가 Identity Map 확인 → 없으면 SQL 실행
  3. 결과를 도메인 객체로 변환하여 반환
  4. 업데이트 시, Mapper가 도메인 객체에서 데이터를 꺼내 SQL로 변환

왜 써야 하는가:

  • 도메인 객체를 DB 없이 테스트할 수 있다 (Mapper를 Mock으로 대체)
  • 도메인 스키마와 DB 스키마가 독립적으로 진화할 수 있다
  • 매핑 계층 전체를 교체할 수 있다 (다른 DB, 파일, 테스트 스텁)

Fowler의 강력 권고: Data Mapper를 직접 구현하지 말고 O/R 매핑 도구를 사라. 벤더들이 수년간 이 문제를 연구해왔다. 도구 비용은 직접 구현·유지보수 비용보다 싸다.

선택 매트릭스

도메인 로직 패턴추천 데이터 소스이유
Transaction ScriptTable Data Gateway 또는 Row Data Gateway단순, SQL 분리만 하면 충분
Table ModuleTable Data GatewayRecord Set 기반이라 필연적 조합
Domain Model (단순)Active Record1:1 매핑이면 가장 간단
Domain Model (복잡)Data Mapper독립적 진화 필수

이 패턴들은 상호 배타적이지 않다. Data Mapper를 쓰면서도, 외부 서비스 테이블은 Table Data Gateway로 감쌀 수 있다.