C++ Template Method 패턴
알고리즘의 골격을 기반 클래스에 정의하고 세부 단계를 서브클래스에서 재정의하는 패턴입니다. 데이터 처리 파이프라인을 예시로 훅 메서드와 final 키워드를 활용합니다.
#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"
);
}