Data Source Patterns — 실전 예시와 템플릿
예시: Student 클래스를 네 가지 패턴으로
같은 Student를 각 데이터 소스 패턴으로 구현하면 어떤 차이가 생기는지 비교한다.
1. Table Data Gateway
class StudentGateway:
findById(id) -> RecordSet:
return db.query("SELECT * FROM students WHERE student_id = ?", id)
findByDepartment(dept) -> RecordSet:
return db.query("SELECT * FROM students WHERE department = ?", dept)
insert(id, name, dept, year):
db.execute("INSERT INTO students VALUES (?, ?, ?, ?)",
id, name, dept, year)
update(id, name, dept, year):
db.execute("UPDATE students SET name=?, department=?, enrollment_year=? WHERE student_id=?",
name, dept, year, id)
// 사용하는 곳 (Transaction Script)
function getStudentInfo(studentId):
row = studentGateway.findById(studentId) // RecordSet 반환
// row["name"], row["department"] 등으로 접근
2. Row Data Gateway
class StudentGateway:
studentId: String
name: String
department: String
enrollmentYear: int
insert():
db.execute("INSERT INTO students VALUES (?, ?, ?, ?)",
studentId, name, department, enrollmentYear)
update():
db.execute("UPDATE students SET name=?, ... WHERE student_id=?",
name, ..., studentId)
class StudentFinder:
find(id) -> StudentGateway:
row = db.query("SELECT * FROM students WHERE student_id = ?", id)
return StudentGateway(row.studentId, row.name, row.department, row.enrollmentYear)
3. Active Record
class Student:
studentId: String
name: String
department: String
enrollmentYear: int
// --- 도메인 로직 ---
getCompletedCourses() -> List<Course>:
rows = db.query("SELECT c.* FROM courses c JOIN enrollments e ON ... WHERE e.student_id = ?", studentId)
return rows.map(row -> Course.load(row))
getGPA() -> float:
enrollments = getEnrollments()
return enrollments.average(e -> e.gradePoint())
isEligibleForGraduation() -> bool:
return getGPA() >= 2.0 and completedAllRequirements()
// --- DB 접근 ---
static find(id) -> Student:
row = db.query("SELECT * FROM students WHERE student_id = ?", id)
return Student(row...)
save():
if isNew:
db.execute("INSERT INTO students ...", ...)
else:
db.execute("UPDATE students SET ... WHERE student_id = ?", ..., studentId)
4. Data Mapper
class Student:
// DB 접근 코드가 전혀 없다!
getCompletedCourses() -> List<Course>:
return enrollments.filter(e -> e.grade != null).map(e -> e.course)
getGPA() -> float:
return enrollments.average(e -> e.gradePoint())
class StudentMapper:
identityMap: Map<String, Student>
find(id) -> Student:
// 1. Identity Map 확인
if id in identityMap:
return identityMap[id]
// 2. DB에서 로드
row = db.query("SELECT * FROM students WHERE student_id = ?", id)
student = Student(row.studentId, row.name, row.department, row.enrollmentYear)
// 3. Identity Map에 등록
identityMap[id] = student
return student
update(student):
db.execute("UPDATE students SET name=?, department=?, enrollment_year=? WHERE student_id=?",
student.name, student.department, student.enrollmentYear, student.studentId)
핵심 차이를 시퀀스 다이어그램으로
Active Record — Student가 직접 DB와 대화:
Data Mapper — Student는 DB를 모름:
템플릿
Table Data Gateway 템플릿
class [엔티티]Gateway:
// --- 조회 ---
findById([pk]) -> RecordSet:
return db.query("SELECT * FROM [테이블] WHERE [pk_col] = ?", [pk])
findBy[조건]([param]) -> RecordSet:
return db.query("SELECT * FROM [테이블] WHERE [col] = ?", [param])
findAll() -> RecordSet:
return db.query("SELECT * FROM [테이블]")
// --- 변경 ---
insert([col1], [col2], ...):
db.execute("INSERT INTO [테이블] ([col1], [col2], ...) VALUES (?, ?, ...)",
[col1], [col2], ...)
update([pk], [col1], [col2], ...):
db.execute("UPDATE [테이블] SET [col1]=?, [col2]=? WHERE [pk_col]=?",
[col1], [col2], ..., [pk])
delete([pk]):
db.execute("DELETE FROM [테이블] WHERE [pk_col] = ?", [pk])
Active Record 템플릿
class [엔티티]:
// --- 필드 (DB 컬럼과 1:1) ---
[field1]: [Type]
[field2]: [Type]
[fk_field]: [Type] // FK는 참조 또는 ID로
// --- Finder (static) ---
static find([pk]) -> [엔티티]:
row = db.query("SELECT * FROM [테이블] WHERE [pk_col] = ?", [pk])
return [엔티티].load(row)
static findBy[조건]([param]) -> List<[엔티티]>:
rows = db.query("SELECT * FROM [테이블] WHERE [col] = ?", [param])
return rows.map(row -> [엔티티].load(row))
// --- Persistence ---
save():
if isNew():
db.execute("INSERT INTO [테이블] VALUES (...)", ...)
else:
db.execute("UPDATE [테이블] SET ... WHERE [pk_col] = ?", ..., [pk])
delete():
db.execute("DELETE FROM [테이블] WHERE [pk_col] = ?", [pk])
// --- 도메인 로직 ---
[비즈니스메서드1]() -> [결과]:
// 이 객체의 데이터를 사용한 계산/판단
[비즈니스메서드2]([파라미터]) -> [결과]:
// 관련 객체 조회 후 로직 수행
Data Mapper 템플릿
// === 도메인 클래스 (DB 코드 없음) ===
class [엔티티]:
[field1]: [Type]
[field2]: [Type]
[연관객체]: [Type] // 객체 참조
[비즈니스메서드]() -> [결과]:
// 순수 도메인 로직만
// === Mapper 클래스 ===
class [엔티티]Mapper:
identityMap: Map<[PKType], [엔티티]>
find([pk]) -> [엔티티]:
if [pk] in identityMap:
return identityMap[[pk]]
row = db.query("SELECT * FROM [테이블] WHERE [pk_col] = ?", [pk])
obj = load(row)
identityMap[[pk]] = obj
return obj
load(row) -> [엔티티]:
obj = [엔티티]()
obj.[field1] = row["[col1]"]
obj.[field2] = row["[col2]"]
// FK → 객체 참조 변환 (Lazy Load 또는 Eager)
// obj.[연관객체] = [연관Mapper].find(row["[fk_col]"])
return obj
insert([obj]):
db.execute("INSERT INTO [테이블] ([col1], [col2]) VALUES (?, ?)",
[obj].[field1], [obj].[field2])
update([obj]):
db.execute("UPDATE [테이블] SET [col1]=?, [col2]=? WHERE [pk_col]=?",
[obj].[field1], [obj].[field2], [obj].[pk])
delete([obj]):
db.execute("DELETE FROM [테이블] WHERE [pk_col] = ?", [obj].[pk])
identityMap.remove([obj].[pk])
패턴 선택 결정 워크시트
## [프로젝트명] Data Source 패턴 선택
### 전제 조건
- 선택한 도메인 로직 패턴: __________
- 클래스-테이블 1:1 비율: ___% (대략)
- 상속 구조 존재 여부: Yes / No
- 다대다 관계 복잡도: 단순 / 복잡
### 패턴 적합성 평가
| 패턴 | 적합? | 근거 |
|------|------|------|
| Table Data Gateway | | Transaction Script에 궁합 좋음 |
| Row Data Gateway | | 객체 지향적 접근, 로직 분리 |
| Active Record | | 1:1 매핑 + 단순 도메인 로직 |
| Data Mapper | | 복잡한 도메인 + DB 독립성 필요 |
### 결정
선택: __________
향후 전환 가능성: Active Record → Data Mapper 전환이 예상되는가? __________