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 Script | Table Data Gateway 또는 Row Data Gateway | 단순, SQL 분리만 하면 충분 | | Table Module | Table Data Gateway | Record Set 기반이라 필연적 조합 | | Domain Model (단순) | Active Record | 1:1 매핑이면 가장 간단 | | Domain Model (복잡) | Data Mapper | 독립적 진화 필수 |

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