research

Distribution & Base Patterns — 실전 예시와 템플릿

Distribution & Base Patterns — 실전 예시와 템플릿

이론: 09-distribution-and-base-patterns

예시: LLM 연동을 Gateway + DTO로 설계

Academic Life System에서 외부 LLM API와의 통신이 가장 분산 패턴에 가까운 부분이다.

Gateway 패턴 — 외부 시스템 격리

// Gateway — 외부 시스템 접근을 캡슐화
class OpenAIGateway implements LLMGateway:
    
    sendPrompt(context: CounselingContext) -> LLMResponse:
        // 1. DTO를 API 요청 형식으로 변환
        requestBody = {
            "model": "gpt-4",
            "messages": [
                {"role": "system", "content": buildSystemPrompt()},
                {"role": "user", "content": context.toPromptString()}
            ]
        }
        
        // 2. HTTP 호출
        httpResponse = http.post(endpoint, requestBody, headers={"Authorization": apiKey})
        
        // 3. API 응답을 DTO로 변환
        return LLMResponse(
            recommendation = httpResponse.choices[0].message.content,
            confidence = extractConfidence(httpResponse),
            reasoning = extractReasoning(httpResponse)
        )

// 테스트용 Gateway
class MockLLMGateway implements LLMGateway:
    
    sendPrompt(context) -> LLMResponse:
        return LLMResponse(
            recommendation = "CS101을 먼저 수강하세요.",
            confidence = 0.9,
            reasoning = "선수과목 분석 결과"
        )

Remote Facade + DTO — API 서빙 시

만약 프론트엔드가 SPA(React 등)라서 REST API로 소통한다면:

// Remote Facade — 여러 도메인 객체의 데이터를 하나의 DTO로 조합
class RecommendationFacade:
    
    getRecommendations(studentId, semester) -> RecommendationDTO:
        // 여러 도메인 객체에서 데이터 수집
        student = Student.find(studentId)
        recommendations = student.getRecommendations(semester)
        
        // 하나의 DTO로 조합하여 반환 (네트워크 호출 1회로 끝)
        return RecommendationDTO(
            studentName = student.name,
            semester = semester,
            courses = recommendations.map(c -> CourseDTO(
                courseId = c.courseId,
                name = c.name,
                credits = c.credits,
                reason = c.recommendationReason,
                priority = c.priority
            )),
            totalCredits = recommendations.sum(c -> c.credits)
        )

Registry 패턴 — Mapper/Gateway 찾기

class MapperRegistry:
    // Singleton
    static instance: MapperRegistry
    
    mappers: Map<Class, Mapper>
    gateways: Map<String, Gateway>
    
    static getMapper(cls) -> Mapper:
        return instance.mappers.get(cls)
    
    static getGateway(name) -> Gateway:
        return instance.gateways.get(name)
    
    // 초기화 (앱 시작 시)
    static init():
        instance = new MapperRegistry()
        instance.mappers[Student] = new StudentMapper()
        instance.mappers[Course] = new CourseMapper()
        instance.gateways["llm"] = new OpenAIGateway(config.apiKey)

Plugin 패턴 — 런타임 교체

// 설정 파일로 구현체 결정
config.yaml:
    llm_gateway: "openai"    # 또는 "local" 또는 "mock"
    db_type: "sqlite"         # 또는 "mysql"

// Plugin 로더
class PluginLoader:
    static loadLLMGateway(config) -> LLMGateway:
        switch config.llm_gateway:
            case "openai": return new OpenAIGateway(config.api_key)
            case "local":  return new LocalLLMGateway(config.model_path)
            case "mock":   return new MockLLMGateway()

템플릿

Gateway 설계 템플릿

// Gateway 인터페이스
interface [외부시스템]Gateway:
    [동작]([입력DTO]) -> [출력DTO]
    isAvailable() -> bool

// 실제 구현
class [구체구현]Gateway implements [외부시스템]Gateway:
    [동작]([입력DTO]) -> [출력DTO]:
        // 1. DTO → 외부 시스템 요청 형식 변환
        request = convertToExternalFormat([입력DTO])
        // 2. 외부 시스템 호출
        rawResponse = [외부호출](request)
        // 3. 응답 → DTO 변환
        return convertToDTO(rawResponse)

// 테스트용
class Mock[외부시스템]Gateway implements [외부시스템]Gateway:
    [동작]([입력DTO]) -> [출력DTO]:
        return [고정된 응답]

DTO 설계 템플릿

// DTO 설계 원칙:
// 1. 비즈니스 로직 없음 — 순수 데이터 컨테이너
// 2. 직렬화 가능 (JSON/XML)
// 3. 여러 도메인 객체의 데이터를 조합 가능

class [기능명]DTO:
    // 기본 정보
    [field1]: [Type]
    [field2]: [Type]
    
    // 관련 객체 정보 (중첩 DTO)
    [related]: [관련DTO]
    [relatedList]: List<[관련DTO]>
    
    // 파생 값 (클라이언트 편의)
    [computed]: [Type]
    
    // DTO ↔ JSON 변환
    toJSON() -> String
    static fromJSON(json) -> [기능명]DTO

Remote Facade 템플릿

// Remote Facade — 도메인 객체의 fine-grained 인터페이스를 
// coarse-grained 인터페이스로 감싼다

class [기능영역]Facade:
    
    // 하나의 메서드 = 하나의 API 호출로 완결
    [유스케이스]([최소한의 입력]) -> [포괄적DTO]:
        // 1. 여러 도메인 객체 조회
        [obj1] = [mapper1].find([id1])
        [obj2] = [mapper2].findBy[조건]([조건])
        
        // 2. 도메인 로직 실행
        [result] = [obj1].[비즈니스메서드]([obj2])
        
        // 3. 결과를 DTO로 조합
        return [포괄적DTO](
            [obj1에서 가져온 필드들],
            [obj2에서 가져온 필드들],
            [result에서 가져온 필드들]
        )

외부 시스템 연동 체크리스트

## [프로젝트명] 외부 시스템 연동

### 연동 대상
| 외부 시스템 | Gateway 이름 | 구현체 | Mock 여부 |
|-----------|------------|--------|----------|
| _________ | _________Gateway | _________ | [ ] Mock 준비됨 |
| _________ | _________Gateway | _________ | [ ] Mock 준비됨 |

### 격리 확인
- [ ] 도메인 클래스에서 외부 시스템을 직접 호출하지 않는가?
- [ ] Gateway 인터페이스로 추상화되어 있는가?
- [ ] Mock Gateway로 교체하여 테스트할 수 있는가?
- [ ] 외부 시스템 장애 시 graceful degradation이 가능한가?

Remote Facade — Fine vs Coarse-Grained 비교

DTO Assembler 패턴

DTO 조립 로직을 별도 클래스로 분리:

Separated Interface — 의존성 역전의 핵심

인터페이스는 Domain에, 구현은 Infrastructure에. Domain은 외부를 모른다.

Layer Supertype — C++ 구현

// 모든 도메인 객체의 부모
class DomainObject {
protected:
    string id;
    int version;        // Optimistic Lock용
    Timestamp modified;
    string modifiedBy;
public:
    string getId() { return id; }
    int getVersion() { return version; }
    bool isNew() { return id == ""; }
};

// 모든 Mapper의 부모
class AbstractMapper {
protected:
    string tableName;
    vector<string> columns;
    
    string buildSelectSQL(string whereClause) {
        return "SELECT " + join(columns, ",") + " FROM " + tableName 
               + " WHERE " + whereClause;
    }
    string buildUpdateSQL() {
        auto sets = columns.map(c -> c + "=?");
        return "UPDATE " + tableName + " SET " + join(sets, ",") 
               + ", version=version+1 WHERE id=? AND version=?";
    }
    void checkConcurrency(int rowCount, string id) {
        if (rowCount == 0)
            throw ConcurrencyException("Conflict on " + tableName + " id=" + id);
    }
};

Service Stub — 테스트용 외부 서비스 대체

class LLMServiceStub : public LLMPort {
    map<string, string> cannedResponses;
    
    string complete(string prompt) override {
        for (auto& [keyword, response] : cannedResponses) {
            if (prompt.contains(keyword)) return response;
        }
        return "Default stub response";
    }
};

// 테스트에서 사용
void testRecommendation() {
    LLMServiceStub stub;
    stub.cannedResponses["recommend"] = "CS101, CS201, CS301";
    
    RecommendationService service(stub);
    auto result = service.recommend(testStudent);
    assert(result.size() == 3);
}

Value Object — 불변 값 객체

class Money {
    double amount;
    string currency;
    
    bool operator==(const Money& other) const {
        return amount == other.amount && currency == other.currency;
    }
    Money add(const Money& other) const {
        assert(currency == other.currency);
        return Money(amount + other.amount, currency);
    }
};

class Semester {
    int year;
    string term; // "spring", "fall"
    
    bool operator==(const Semester& other) const {
        return year == other.year && term == other.term;
    }
    Semester next() {
        if (term == "spring") return Semester(year, "fall");
        return Semester(year + 1, "spring");
    }
};

Base Pattern 적용 체크리스트

## [프로젝트명] Base Pattern 적용 현황

| 패턴 | 적용 여부 | 구현 위치 | 비고 |
|------|----------|----------|------|
| Gateway | | | 외부 시스템(LLM, 크롤러 등) |
| Mapper | | | Data Mapper를 쓰면 자동 |
| Layer Supertype | | | DomainObject, AbstractMapper |
| Separated Interface | | | 인터페이스 위치 = Domain |
| Registry | | | Mapper 조회용 |
| Value Object | | | Money, Semester 등 |
| Plugin | | | 설정으로 구현체 결정 |
| Service Stub | | | 테스트용 |
| Record Set | | | Table Module 사용 시 |