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으로 교체할 수 있는가?