O-R Behavioral Patterns — 실전 예시와 템플릿
예시: Academic Life System에서 Unit of Work + Identity Map + Lazy Load
시나리오: "학생이 수강 신청을 하고 추천을 받는다"
한 요청 안에서 Student 로드 → Enrollment 추가 → 추천 계산 → 저장이 일어난다.
Unit of Work 예시
class UnitOfWork:
newObjects: List<DomainObject> = []
dirtyObjects: List<DomainObject> = []
removedObjects: List<DomainObject> = []
registerNew(obj):
assert obj.id != null
assert obj not in dirtyObjects
assert obj not in removedObjects
newObjects.add(obj)
registerDirty(obj):
assert obj.id != null
assert obj not in removedObjects
if obj not in dirtyObjects and obj not in newObjects:
dirtyObjects.add(obj)
registerRemoved(obj):
assert obj.id != null
if obj in newObjects:
newObjects.remove(obj)
return
dirtyObjects.remove(obj)
if obj not in removedObjects:
removedObjects.add(obj)
registerClean(obj):
// Identity Map에 등록 (있다면)
identityMap.put(obj.id, obj)
commit():
tx = db.beginTransaction()
try:
insertNew()
updateDirty()
deleteRemoved()
tx.commit()
catch:
tx.rollback()
throw
insertNew():
for obj in newObjects:
MapperRegistry.getMapper(obj.class).insert(obj)
updateDirty():
for obj in dirtyObjects:
MapperRegistry.getMapper(obj.class).update(obj)
deleteRemoved():
for obj in removedObjects:
MapperRegistry.getMapper(obj.class).delete(obj)
사용 예시:
// 수강 신청 + 추천 받기
function enrollAndRecommend(studentId, courseId, semester):
uow = new UnitOfWork()
student = studentMapper.find(studentId) // registerClean 자동 호출
course = courseMapper.find(courseId)
enrollment = new Enrollment(student, course, semester)
uow.registerNew(enrollment) // 새 객체 등록
student.addEnrollment(enrollment)
uow.registerDirty(student) // 변경된 객체 등록
recommendations = student.getRecommendations(semester)
uow.commit() // INSERT enrollment + UPDATE student 한 트랜잭션으로
return recommendations
Identity Map 예시
class IdentityMap:
maps: Map<Class, Map<ID, DomainObject>>
get(cls, id) -> DomainObject or null:
classMap = maps.get(cls)
if classMap == null:
return null
return classMap.get(id)
put(cls, id, obj):
if cls not in maps:
maps[cls] = {}
maps[cls][id] = obj
remove(cls, id):
if cls in maps:
maps[cls].remove(id)
왜 필요한가 — 없으면 생기는 문제:
// Identity Map 없을 때
student1 = studentMapper.find("2024001") // DB에서 로드 → 객체 A
student2 = studentMapper.find("2024001") // DB에서 또 로드 → 객체 B (다른 인스턴스!)
student1.setName("김철수")
// student2.getName()은 여전히 "김영희"
// student2.save()하면 student1의 변경이 덮어써진다!
// Identity Map 있을 때
student1 = studentMapper.find("2024001") // DB에서 로드 → 객체 A, Map에 등록
student2 = studentMapper.find("2024001") // Map에서 찾음 → 객체 A (같은 인스턴스!)
// student1 === student2, 문제 없음
Lazy Load 예시: 네 가지 방식
1. Lazy Initialization (가장 단순)
class Student:
_enrollments: List<Enrollment> = null
getEnrollments() -> List<Enrollment>:
if _enrollments == null:
_enrollments = enrollmentMapper.findByStudent(this.id)
return _enrollments
2. Virtual Proxy
class EnrollmentListProxy implements List<Enrollment>:
studentId: String
_real: List<Enrollment> = null
_load():
if _real == null:
_real = enrollmentMapper.findByStudent(studentId)
size() -> int:
_load()
return _real.size()
get(index) -> Enrollment:
_load()
return _real.get(index)
3. Ghost (부분 로드)
class Student:
id: String // 항상 로드
_loaded: bool = false
_name: String
_department: String
getName() -> String:
if not _loaded:
_loadFull()
return _name
_loadFull():
row = db.query("SELECT * FROM students WHERE id = ?", id)
_name = row["name"]
_department = row["department"]
_loaded = true
N+1 문제 예시:
// 나쁜 예: N+1 쿼리
students = studentMapper.findByDepartment("CS") // 1번 쿼리
for student in students:
gpas.add(student.getEnrollments()) // 학생마다 1번씩 = N번 쿼리!
// 총 N+1번 쿼리
// 좋은 예: Eager Loading
students = studentMapper.findByDepartmentWithEnrollments("CS")
// JOIN으로 1번에 다 가져옴
// SELECT s.*, e.* FROM students s LEFT JOIN enrollments e ON ... WHERE s.dept = 'CS'
템플릿
Unit of Work 템플릿
class UnitOfWork:
newObjects: List<DomainObject> = []
dirtyObjects: List<DomainObject> = []
removedObjects: List<DomainObject> = []
identityMap: IdentityMap = new IdentityMap()
// --- 등록 ---
registerNew(obj):
assert obj.id != null
newObjects.add(obj)
registerDirty(obj):
if obj not in dirtyObjects and obj not in newObjects:
dirtyObjects.add(obj)
registerRemoved(obj):
if obj in newObjects: newObjects.remove(obj); return
dirtyObjects.remove(obj)
removedObjects.add(obj)
registerClean(obj):
identityMap.put(obj.class, obj.id, obj)
// --- 커밋 ---
commit():
tx = db.beginTransaction()
try:
for obj in newObjects:
MapperRegistry.getMapper(obj.class).insert(obj)
for obj in dirtyObjects:
MapperRegistry.getMapper(obj.class).update(obj)
for obj in removedObjects:
MapperRegistry.getMapper(obj.class).delete(obj)
tx.commit()
catch:
tx.rollback()
throw
finally:
clear()
clear():
newObjects.clear()
dirtyObjects.clear()
removedObjects.clear()
Identity Map 통합 Mapper 템플릿
class [엔티티]Mapper:
uow: UnitOfWork // UnitOfWork가 Identity Map을 포함
find([pk]) -> [엔티티]:
// 1. Identity Map 확인
existing = uow.identityMap.get([엔티티].class, [pk])
if existing != null:
return existing
// 2. DB 조회
row = db.query("SELECT * FROM [테이블] WHERE [pk_col] = ?", [pk])
// 3. 객체 생성
obj = load(row)
// 4. Identity Map + Unit of Work 등록
uow.registerClean(obj)
return obj
load(row) -> [엔티티]:
obj = new [엔티티]()
obj.[field1] = row["[col1]"]
obj.[field2] = row["[col2]"]
// Lazy Load 설정
obj._[연관] = new LazyLoader(() -> [연관Mapper].findBy[FK](obj.id))
return obj
Lazy Load 선택 가이드
## [프로젝트명] Lazy Load 결정
### 연관 관계별 로딩 전략
| 도메인 클래스 | 연관 관계 | 로딩 전략 | 근거 |
|-------------|----------|----------|------|
| __________ | __→__ (1:N) | Lazy / Eager | |
| __________ | __→__ (N:1) | Lazy / Eager | |
| __________ | __→__ (M:N) | Lazy / Eager | |
### 판단 기준
- 거의 항상 함께 쓰는 데이터 → **Eager** (JOIN)
- 가끔만 쓰는 데이터 → **Lazy**
- 리스트 화면에서 요약만 보여주는 경우 → **Lazy**
- 상세 화면에서 전부 보여주는 경우 → **Eager**
### N+1 위험 지점
| 위치 | 루프 안에서 Lazy Load 발생? | 해결 |
|------|-------------------------|------|
| __________ | Yes / No | Eager로 전환 / 그대로 유지 |
세 패턴 통합 시퀀스 다이어그램 템플릿
C++ 구현 레퍼런스
Unit of Work — 클래스 구조
Layer Supertype + Object Registration 방식
// 모든 도메인 객체의 부모 — setter에서 자동으로 dirty 등록
class DomainObject {
protected:
string id;
void markNew() {
UnitOfWork::getCurrent().registerNew(this);
}
void markDirty() {
UnitOfWork::getCurrent().registerDirty(this);
}
void markRemoved() {
UnitOfWork::getCurrent().registerRemoved(this);
}
};
// Student에서의 사용
class Student : public DomainObject {
void setName(string name) {
this->name = name;
markDirty(); // setter에서 자동 등록!
}
};
Identity Map — C++ 템플릿 클래스
template<typename T>
class IdentityMap {
unordered_map<string, shared_ptr<T>> map;
public:
shared_ptr<T> get(string id) {
auto it = map.find(id);
if (it != map.end()) return it->second;
return nullptr;
}
void put(string id, shared_ptr<T> obj) {
map[id] = obj;
}
void remove(string id) {
map.erase(id);
}
};
Lazy Load — C++ 구현 4가지
1. Lazy Initialization:
class Student {
List<Enrollment>* enrollments = nullptr;
List<Enrollment>& getEnrollments() {
if (enrollments == nullptr) {
enrollments = enrollmentMapper.findByStudent(this->id);
}
return *enrollments;
}
};
2. Virtual Proxy:
class EnrollmentListProxy : public List<Enrollment> {
string studentId;
List<Enrollment>* realList = nullptr;
List<Enrollment>& getRealList() {
if (!realList) {
realList = enrollmentMapper.findByStudent(studentId);
}
return *realList;
}
int size() override { return getRealList().size(); }
Enrollment& get(int i) override { return getRealList().get(i); }
};
3. Value Holder (Lambda 활용):
template<typename T>
class ValueHolder {
function<T()> loader;
optional<T> value;
public:
ValueHolder(function<T()> loader) : loader(loader) {}
T& get() {
if (!value.has_value()) {
value = loader();
}
return value.value();
}
};
class Student {
ValueHolder<List<Enrollment>> enrollments;
Student(string id)
: enrollments([id]() {
return enrollmentMapper.findByStudent(id);
}) {}
};
4. Ghost (부분 로드):
class Student {
enum LoadState { GHOST, PARTIAL, FULL };
LoadState state = GHOST;
string id; // GHOST 상태에서도 있음
string name; // PARTIAL에서 로드
List<Enrollment> enrollments; // FULL에서 로드
string getName() {
if (state == GHOST) loadBasicFields();
return name;
}
List<Enrollment>& getEnrollments() {
if (state != FULL) loadAllFields();
return enrollments;
}
void loadBasicFields() {
auto rs = studentMapper.loadBasic(id);
name = rs.getString("name");
state = PARTIAL;
}
void loadAllFields() {
loadBasicFields();
enrollments = enrollmentMapper.findByStudent(id);
state = FULL;
}
};