research

O-R Behavioral Patterns — 실전 예시와 템플릿

O-R Behavioral Patterns — 실전 예시와 템플릿

이론: 06-or-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;
    }
};