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 존재를 모름
동작 흐름:
- 클라이언트가
mapper.find(id)호출 - Mapper가 Identity Map 확인 → 없으면 SQL 실행
- 결과를 도메인 객체로 변환하여 반환
- 업데이트 시, 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로 감쌀 수 있다.