research

Organizing Domain Logic — 실전 예시와 템플릿

Organizing Domain Logic — 실전 예시와 템플릿

이론: 02-organizing-domain-logic

예시: "과목 추천" 기능을 세 가지 패턴으로 구현하면

같은 기능이 패턴에 따라 어떻게 달라지는지 비교한다.

Transaction Script 버전

// 하나의 프로시저가 모든 것을 처리
function recommendCourses(studentId, semester):
    // 1. DB에서 학생 정보 조회
    student = db.query("SELECT * FROM students WHERE id = ?", studentId)
    
    // 2. DB에서 이수 과목 조회
    completed = db.query("SELECT course_id FROM enrollments WHERE student_id = ?", studentId)
    
    // 3. DB에서 커리큘럼 요구사항 조회
    requirements = db.query("SELECT * FROM curriculum WHERE dept = ?", student.dept)
    
    // 4. 추천 로직 (여기에 비즈니스 규칙이 직접 들어감)
    recommendations = []
    for req in requirements:
        if req.course_id not in completed:
            if req.semester <= semester:
                if checkPrerequisites(completed, req.course_id):
                    recommendations.add(req)
    
    // 5. 정렬하고 반환
    return sortByPriority(recommendations)

장점: 읽기 쉽고, 흐름이 한눈에 보인다. 단점: "졸업요건 확인" 기능에도 비슷한 조회+필터링 로직이 필요하면? 중복이 시작된다.

Domain Model 버전

// 각 객체가 자기 책임을 담당
class Student:
    completedCourses: List<Enrollment>
    curriculum: Curriculum
    
    getRecommendations(semester) -> List<Course>:
        return curriculum.getUnfulfilledRequirements(this, semester)

class Curriculum:
    requirements: List<Requirement>
    
    getUnfulfilledRequirements(student, semester) -> List<Course>:
        return requirements
            .filter(r -> !student.hasCompleted(r.course))
            .filter(r -> r.isAvailableIn(semester))
            .filter(r -> r.prerequisitesMet(student))
            .sortBy(r -> r.priority)

class Requirement:
    course: Course
    semester: int
    prerequisites: List<Course>
    
    prerequisitesMet(student) -> bool:
        return prerequisites.all(p -> student.hasCompleted(p))

장점: "졸업요건 확인"도 curriculum.getUnfulfilledRequirements()를 재사용. 새 추천 전략(예: LLM 기반)은 Strategy 패턴으로 추가. 단점: 객체 그래프를 DB에서 어떻게 로드할지가 별도 문제.

Table Module 버전

// 테이블당 하나의 클래스, 인스턴스도 하나
class StudentModule:
    dataSet: RecordSet  // students 테이블 전체

    getCompletedCourseIds(studentId) -> List<String>:
        return dataSet.enrollments
            .filter(row -> row["student_id"] == studentId)
            .map(row -> row["course_id"])

class CurriculumModule:
    dataSet: RecordSet  // curriculum 테이블 전체
    
    getUnfulfilled(dept, completedIds, semester) -> RecordSet:
        return dataSet
            .filter(row -> row["dept"] == dept)
            .filter(row -> row["course_id"] not in completedIds)
            .filter(row -> row["semester"] <= semester)

장점: Transaction Script보다 구조적, Domain Model보다 DB 매핑이 단순. 단점: Record Set 프레임워크 없으면 의미 없음. C++/Drogon에서는 해당 없음.


템플릿

도메인 로직 패턴 선택 워크시트

## [프로젝트명] 도메인 로직 패턴 선택

### 1. 복잡도 평가
- 핵심 비즈니스 규칙 수: ___개
- 규칙 간 조건 분기: 단순 / 중간 / 복잡
- 같은 데이터를 다른 맥락에서 재사용하는가?: Yes / No
- 향후 규칙 추가/변경 빈도 예상: 낮음 / 중간 / 높음

### 2. 환경 평가
- Record Set 프레임워크 사용 여부: Yes / No
- 팀의 OOP 숙련도: 초급 / 중급 / 고급
- 프로젝트 수명 예상: 단기(~6개월) / 중기(1~2년) / 장기(2년+)

### 3. 결정
- [ ] Transaction Script — 규칙이 단순하고 재사용 필요 없음
- [ ] Table Module — Record Set 프레임워크가 있고 중간 복잡도
- [ ] Domain Model — 복잡한 규칙, 재사용, 변경 빈도 높음

선택: __________
근거: __________

Transaction Script 구조 템플릿

// ============================================
// [기능명] Transaction Script
// Use Case: [대응하는 유스케이스명]
// ============================================

function [동사][명사](입력파라미터들):
    // --- 1. 입력 검증 ---
    validate(입력파라미터들)
    
    // --- 2. 데이터 조회 ---
    [엔티티1] = [Gateway].find[조건](파라미터)
    [엔티티2] = [Gateway].find[조건](파라미터)
    
    // --- 3. 비즈니스 로직 ---
    [결과] = [계산/판단 로직]
    
    // --- 4. 상태 변경 ---
    [Gateway].update/insert([변경데이터])
    
    // --- 5. 결과 반환 ---
    return [결과]

Domain Model 클래스 설계 템플릿

Service Layer 설계 템플릿

// Service Layer — Thin Facade 스타일
class [유스케이스]Service:
    [mapper1]: [엔티티1]Mapper
    [mapper2]: [엔티티2]Mapper

    [유스케이스동사](입력파라미터들) -> 결과:
        // 1. 트랜잭션 시작
        tx = beginTransaction()
        try:
            // 2. 도메인 객체 조회
            [obj1] = [mapper1].find(id)
            [obj2] = [mapper2].find(id)
            
            // 3. 도메인 객체에게 비즈니스 로직 위임
            result = [obj1].비즈니스메서드([obj2])
            
            // 4. 변경사항 저장
            [mapper1].update([obj1])
            
            // 5. 트랜잭션 커밋
            tx.commit()
            return result
        catch:
            tx.rollback()
            throw

예시: "수강 가능 여부 판별"을 세 패턴으로

선수과목 이수, 학점 상한, 학과 제한을 체크해야 하는 기능.

Transaction Script 방식

class EnrollmentScripts {
    bool checkEligibility(string studentId, string courseId) {
        auto studentRow = studentGateway.find(studentId);
        auto courseRow = courseGateway.find(courseId);
        auto completedRows = enrollmentGateway.findByStudent(studentId);
        
        // 선수과목 체크
        auto prereqs = prereqGateway.findByCourse(courseId);
        for (auto& prereq : prereqs) {
            bool found = false;
            for (auto& completed : completedRows) {
                if (completed.courseId == prereq.requiredCourseId 
                    && completed.grade != "F") {
                    found = true; break;
                }
            }
            if (!found) return false;
        }
        
        // 학점 상한 체크
        int currentCredits = getCurrentSemesterCredits(studentId);
        if (currentCredits + courseRow.credits > 21) return false;
        
        // 학과 제한 체크
        if (courseRow.departmentRestriction != ""
            && courseRow.departmentRestriction != studentRow.department)
            return false;
        
        return true;
    }
};

Domain Model + Strategy 방식

class Student {
    string department;
    List<Enrollment> enrollments;
    
    bool hasCompleted(Course course) {
        return enrollments.any(e => e.course == course && e.grade != "F");
    }
    int currentSemesterCredits() {
        return enrollments.filter(e => e.semester == currentSemester()).sum(e => e.course.credits);
    }
    bool canTakeMoreCredits(int additional) {
        return currentSemesterCredits() + additional <= 21;
    }
};

class Course {
    int credits;
    string departmentRestriction;
    List<Course> prerequisites;
    
    bool isEligibleFor(Student student) {
        return meetsPrerequisites(student)
            && meetsCreditsLimit(student)
            && meetsDepartmentRestriction(student);
    }
    bool meetsPrerequisites(Student student) {
        return prerequisites.all(prereq => student.hasCompleted(prereq));
    }
    bool meetsDepartmentRestriction(Student student) {
        if (departmentRestriction == "") return true;
        return student.department == departmentRestriction;
    }
    bool meetsCreditsLimit(Student student) {
        return student.canTakeMoreCredits(credits);
    }
};

// 호출부: 한 줄
bool eligible = course.isEligibleFor(student);

비교 요약

| 관점 | Transaction Script | Domain Model | |------|-------------------|--------------| | 새 규칙 추가 | if-else 추가 | 새 메서드 또는 Strategy | | 코드 중복 | 다른 Script에서 같은 체크 반복 | 각 객체가 자기 로직 소유 | | 테스트 | DB 의존적 | 객체 단위 테스트 가능 |

Domain Model 설계 체크리스트

### 책임 분배
- [ ] 각 클래스가 자기 데이터에 관한 로직을 직접 수행하는가? (Information Expert)
- [ ] if-else로 타입을 분기하는 곳이 있는가? → Strategy 패턴 후보
- [ ] 외부 시스템 호출이 도메인 클래스 안에 있는가? → Gateway로 분리

### 복잡도 관리
- [ ] 메서드가 10줄 이상인 곳이 있는가? → 협력 객체에게 위임 검토
- [ ] 한 클래스의 public 메서드가 7개 이상인가? → 역할 분리 검토
- [ ] 순환 의존이 있는가? → 중간 객체 도입 또는 인터페이스 분리

### 테스트 용이성
- [ ] 외부 의존 없이 도메인 로직을 테스트할 수 있는가?
- [ ] Strategy를 Mock으로 교체할 수 있는가?