research

Data Source Patterns — 실전 예시와 템플릿

Data Source Patterns — 실전 예시와 템플릿

이론: 05-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 전환이 예상되는가? __________