C++ Template Method 패턴

알고리즘의 골격을 기반 클래스에 정의하고 세부 단계를 서브클래스에서 재정의하는 패턴입니다. 데이터 처리 파이프라인을 예시로 훅 메서드와 final 키워드를 활용합니다.

Gist
#include <iostream>
#include <string>
#include <vector>
#include <sstream>
#include <algorithm>
#include <chrono>
#include <stdexcept>

// ── 데이터 레코드 타입 ────────────────────────────────────────────────────────
struct Record {
    std::string id;
    std::string payload;
    bool        valid{true};
};

// ── 기반 클래스: 데이터 처리 파이프라인 골격 ─────────────────────────────────
class DataPipeline {
public:
    virtual ~DataPipeline() = default;

    // 템플릿 메서드 — 알고리즘 골격 고정 (서브클래스 재정의 금지)
    void process(const std::string& rawInput) final {
        auto start = std::chrono::steady_clock::now();

        std::cout << "[" << pipelineName() << "] 파이프라인 시작\n";

        // 1단계: 파싱
        auto records = parse(rawInput);
        std::cout << "  파싱 완료: " << records.size() << "개 레코드\n";

        // 2단계: 검증 (훅 — 선택적 재정의)
        if (shouldValidate()) {
            validate(records);
            long ok = std::count_if(records.begin(), records.end(),
                                    [](const Record& r) { return r.valid; });
            std::cout << "  검증 완료: " << ok << "/" << records.size() << "개 유효\n";
        }

        // 3단계: 변환
        transform(records);
        std::cout << "  변환 완료\n";

        // 4단계: 적재
        load(records);

        // 5단계: 완료 후 훅 (선택적)
        onComplete(records);

        auto elapsed = std::chrono::duration_cast<std::chrono::microseconds>(
            std::chrono::steady_clock::now() - start).count();
        std::cout << "[" << pipelineName() << "] 완료 (" << elapsed << "μs)\n\n";
    }

protected:
    // ── 서브클래스가 반드시 구현해야 하는 추상 단계 ─────────────────────────
    virtual std::string            pipelineName() const = 0;
    virtual std::vector<Record>    parse(const std::string& raw) = 0;
    virtual void                   transform(std::vector<Record>& records) = 0;
    virtual void                   load(const std::vector<Record>& records) = 0;

    // ── 훅 메서드 — 기본 구현 제공, 선택적 재정의 ────────────────────────────
    virtual bool shouldValidate() const { return true; }

    virtual void validate(std::vector<Record>& records) {
        for (auto& r : records)
            r.valid = !r.payload.empty();
    }

    virtual void onComplete(const std::vector<Record>&) {
        // 기본: 아무 작업 없음
    }
};

// ── 구체 파이프라인 1: CSV → 데이터베이스 적재 ───────────────────────────────
class CsvToDbPipeline final : public DataPipeline {
protected:
    std::string pipelineName() const override { return "CSV→DB"; }

    // CSV 행을 Record 로 파싱
    std::vector<Record> parse(const std::string& raw) override {
        std::vector<Record> result;
        std::istringstream ss(raw);
        std::string line;
        int lineNo = 0;
        while (std::getline(ss, line)) {
            ++lineNo;
            if (line.empty() || line[0] == '#') continue; // 주석/빈 줄 건너뜀
            auto comma = line.find(',');
            if (comma == std::string::npos) continue;
            result.push_back({line.substr(0, comma), line.substr(comma + 1)});
        }
        return result;
    }

    // 페이로드 앞뒤 공백 제거 + 소문자 변환
    void transform(std::vector<Record>& records) override {
        for (auto& r : records) {
            // 앞뒤 공백 제거
            auto lt = r.payload.find_first_not_of(" \t");
            auto rt = r.payload.find_last_not_of(" \t");
            if (lt != std::string::npos)
                r.payload = r.payload.substr(lt, rt - lt + 1);
            // 소문자 변환
            std::transform(r.payload.begin(), r.payload.end(),
                           r.payload.begin(), ::tolower);
        }
    }

    void load(const std::vector<Record>& records) override {
        std::cout << "  [DB 적재] INSERT " << records.size() << "개 행:\n";
        for (const auto& r : records)
            if (r.valid)
                std::cout << "    INSERT INTO table VALUES('"
                          << r.id << "', '" << r.payload << "')\n";
    }

    // 완료 후 통계 출력
    void onComplete(const std::vector<Record>& records) override {
        long skipped = std::count_if(records.begin(), records.end(),
                                     [](const Record& r) { return !r.valid; });
        if (skipped > 0)
            std::cout << "  [경고] " << skipped << "개 레코드 건너뜀\n";
    }
};

// ── 구체 파이프라인 2: JSON 로그 → 파일 아카이브 (검증 생략) ──────────────────
class LogArchivePipeline final : public DataPipeline {
protected:
    std::string pipelineName() const override { return "LOG→Archive"; }

    // 간단한 키=값 로그 파싱
    std::vector<Record> parse(const std::string& raw) override {
        std::vector<Record> result;
        std::istringstream ss(raw);
        std::string line;
        int idx = 0;
        while (std::getline(ss, line)) {
            if (!line.empty())
                result.push_back({std::to_string(++idx), line});
        }
        return result;
    }

    // 로그 레벨 정규화 (ERROR/WARN → 대문자 강조)
    void transform(std::vector<Record>& records) override {
        for (auto& r : records) {
            if (r.payload.find("error") != std::string::npos ||
                r.payload.find("ERROR") != std::string::npos)
                r.payload = "🔴 " + r.payload;
            else if (r.payload.find("warn") != std::string::npos)
                r.payload = "🟡 " + r.payload;
            else
                r.payload = "🟢 " + r.payload;
        }
    }

    void load(const std::vector<Record>& records) override {
        std::cout << "  [파일 아카이브] archive.log 에 " << records.size() << "줄 기록:\n";
        for (const auto& r : records)
            std::cout << "    " << r.id << ": " << r.payload << '\n';
    }

    // 로그 파이프라인은 검증 단계 불필요
    bool shouldValidate() const override { return false; }
};

// ── 사용 예시 ─────────────────────────────────────────────────────────────────
int main() {
    // CSV → DB 파이프라인
    CsvToDbPipeline csvPipeline;
    csvPipeline.process(
        "# 사용자 데이터\n"
        "U001,  Alice  \n"
        "U002,  Bob\n"
        "U003,\n"           // 빈 페이로드 → 검증 실패
        "U004,  Charlie\n"
    );

    // 로그 아카이브 파이프라인
    LogArchivePipeline logPipeline;
    logPipeline.process(
        "INFO  서버 시작됨\n"
        "warn  디스크 사용량 85%\n"
        "ERROR 데이터베이스 연결 실패\n"
        "INFO  재연결 성공\n"
    );
}