이전 글에서는 메멘토(Memento) 패턴을 모던 C++ 관점에서 재해석하며, 상속 없이 값 기반 상태 스냅샷과 std::expected, coroutine, Ranges 등을 활용해 Undo/Redo, 비동기 복원, 로깅 등의 요구사항에 대응할 수 있음을 확인했습니다. 이번에는 행동(Behavioral) 패턴 중 템플릿 메서드(Template Method) 패턴을 다룹니다.
템플릿 메서드 패턴은 상위 클래스에서 알고리즘의 골격(템플릿)을 정의하고, 하위 클래스에서 일부 단계를 오버라이드하여 구체화하는 패턴입니다. 그러나 전통적 접근은 상속 기반 구조로, 알고리즘 변형 시 하위 클래스 증가, 유지보수 어려움 등이 발생합니다. C++20 이상에서는 Concepts, 람다, std::expected, std::format, coroutine 등을 활용해 상속 없이도 알고리즘 골격을 선언적으로 정의하고, 구체 단계를 함수나 람다로 주입할 수 있습니다. 이를 통해 타입 안전성과 확장성을 모두 개선할 수 있습니다.
패턴 소개: 템플릿 메서드(Template Method)
의도:
- 상위 클래스에서 알고리즘 골격(템플릿)을 정의하고, 일부 단계를 하위 클래스에서 오버라이드하여 알고리즘 변형 가능.
- 예: 문서 처리 알고리즘(로딩, 파싱, 렌더링), 특정 단계(파싱, 렌더링)만 하위 클래스가 결정.
전통적 구현 문제점:
- 추상 클래스에 templateMethod() 정의, 알고리즘 단계용 가상 함수 필요.
- 하위 클래스 오버라이드 증가 → 클래스 수 증가, 유지보수 어려움.
- 에러 처리나 비동기 처리, 조건부 단계 스킵, 로깅 등 고급 기능 대응 제한.
기존 C++ 스타일 구현 (전통적 방식)
예를 들어, 문서 처리 알고리즘 템플릿:
#include <iostream>
struct DocumentProcessor {
virtual ~DocumentProcessor() = default;
void process() {
load();
parse();
render();
}
virtual void load() = 0;
virtual void parse() = 0;
virtual void render() = 0;
};
struct XMLProcessor : DocumentProcessor {
void load() override {
std::cout << "Loading XML\n";
}
void parse() override {
std::cout << "Parsing XML\n";
}
void render() override {
std::cout << "Rendering XML\n";
}
};
int main() {
XMLProcessor processor;
processor.process(); // Loading XML -> Parsing XML -> Rendering XML
}
문제점:
- 상속 기반, 하위 클래스 증가
- 알고리즘 변형 시 클래스 추가 필요
- 에러 처리나 로깅, 비동기 단계 구현 어려움
모던 C++20 이상의 개선: Concepts, 람다로 알고리즘 단계 주입
1. Concepts로 알고리즘 단계 요구사항 정의
알고리즘 골격: load(), parse(), render() 단계 필요하다고 가정하고, 각 단계가 std::expected<void,std::string> 반환. Concepts로 단계 정의:
template<typename L, typename P, typename R>
concept TemplateMethodSteps = requires(L& loadFunc, P& parseFunc, R& renderFunc) {
{ loadFunc() } -> std::convertible_to<std::expected<void,std::string>>;
{ parseFunc() } -> std::convertible_to<std::expected<void,std::string>>;
{ renderFunc() } -> std::convertible_to<std::expected<void,std::string>>;
};
2. 템플릿 메서드 골격을 함수 합성으로 구현
알고리즘 골격(템플릿 메서드): load → parse → render 순서 실행.
auto templateMethod = [](TemplateMethodSteps auto loadFunc, auto parseFunc, auto renderFunc) {
return [=]() -> std::expected<void,std::string> {
// load
auto lr = loadFunc();
if(!lr) return std::unexpected(lr.error());
// parse
auto pr = parseFunc();
if(!pr) return std::unexpected(pr.error());
// render
auto rr = renderFunc();
if(!rr) return std::unexpected(rr.error());
return {};
};
};
비교:
- 전통적: 상위 클래스에서 templateMethod(), 가상 함수 단계 호출
- C++11/14/17: 람다 가능하지만 Concepts로 단계 제약 불가, 에러 처리나 비동기 대응 제한
- C++20: Concepts로 단계 요구사항 정의, 람다 합성으로 상속 없이 알고리즘 골격 구현
3. 구체 단계를 람다로 정의
auto loadXML = []() -> std::expected<void,std::string> {
std::cout << "Loading XML\n";
return {};
};
auto parseXML = []() -> std::expected<void,std::string> {
std::cout << "Parsing XML\n";
return {};
};
auto renderXML = []() -> std::expected<void,std::string> {
std::cout << "Rendering XML\n";
return {};
};
static_assert(TemplateMethodSteps<decltype(loadXML),decltype(parseXML),decltype(renderXML)>);
사용 예
int main() {
auto xmlProcess = templateMethod(loadXML, parseXML, renderXML);
auto res = xmlProcess();
if(!res) std::cerr << "Error: " << res.error() << "\n";
else std::cout << "Processing succeeded.\n";
}
출력:
Loading XML
Parsing XML
Rendering XML
Processing succeeded.
비교:
- 전통적: XML 처리 위해 XMLProcessor 하위 클래스 필요
- C++20: 람다 세 개와 templateMethod로 구성, 상속 없이 구현
Undo/Redo, 조건부 단계 스킵, 파이프라인 적용
특정 단계 건너뛰거나 추가 로직 넣을 때 람다 장식:
auto conditionalParse = [&](auto baseParse) {
return [=]() -> std::expected<void,std::string> {
// 특정 조건에서 parse 생략 가능
bool skipParse = false;
if (skipParse) return {}; // parse 건너뛰기
return baseParse();
};
};
auto xmlProcessWithSkip = templateMethod(loadXML, conditionalParse(parseXML), renderXML);
비교:
- 전통적: 특정 단계 조건부 생략 시 하위 클래스 추가 필요
- C++20: 함수 합성으로 조건부 단계 쉽게 추가
비동기 단계: coroutine
각 단계가 coroutine일 수도 있음. 예: load 단계에서 네트워크 co_await, parse 단계에서 비동기 I/O:
// 가상의 예: co_load()에서 co_await 비동기 I/O, 완성 후 std::expected 반환
비교:
- 전통적: 비동기 처리 어렵고 콜백/쓰레드 관리 필요
- C++20: coroutine으로 자연스럽게 비동기 단계 구현, Concepts로 비동기 가능성 제약
로깅 추가: std::format
auto loggingStage = [&](auto stageFunc, std::string stageName) {
return [=]() -> std::expected<void,std::string> {
std::cout << std::format("[LOG] {} stage start\n", stageName);
auto res = stageFunc();
if(!res) std::cout << std::format("[LOG] {} stage error: {}\n", stageName, res.error());
else std::cout << std::format("[LOG] {} stage success\n", stageName);
return res;
};
};
auto xmlProcessLogged = templateMethod(
loggingStage(loadXML, "Load"),
loggingStage(parseXML, "Parse"),
loggingStage(renderXML, "Render")
);
비교:
- 전통적: 로깅 추가 시 템플릿 메서드 상속 + Decorator 필요
- C++20: 람다 장식으로 로깅 추가, 상속 없음
Ranges로 다양한 알고리즘 단계 조합
여러 단계 함수를 벡터나 컨테이너에 담아 Ranges로 처리 가능:
std::vector<std::function<std::expected<void,std::string>()>> steps = {
loadXML, parseXML, renderXML
};
auto pipeline = [&]() -> std::expected<void,std::string> {
for (auto& step : steps) {
auto r = step();
if(!r) return r;
}
return {};
};
// pipeline 호출로 templateMethod 유사 효과
비교:
- 전통적: templateMethod 고정된 알고리즘 구조, 변경 시 상속 필요
- C++20: steps 벡터와 Ranges로 동적 알고리즘 구성 가능
전통적 구현 vs C++11/14/17 vs C++20 이상 비교
전통적 (C++98/03):
- 상위 클래스에 templateMethod(), 하위 클래스에서 단계 오버라이드
- 클래스 증가, 알고리즘 변형 시 하위 클래스 추가 필요
- 조건부 단계, 비동기 처리, 로깅, Undo/Redo 등 고급 요구사항 구현 번거로움
C++11/14/17:
- 람다, std::function 일부 활용 가능
- Concepts, coroutine, std::expected, Ranges 부재로 타입 안전한 인터페이스 제약, 비동기/에러 처리 선언적 표현 한계
C++20 이상(모던 C++):
- Concepts로 단계 요구사항 정의, 상속 없이 알고리즘 골격 표현
- 람다/함수 합성으로 단계 주입, std::expected로 에러 처리 명확, std::format으로 로깅 용이
- coroutine으로 비동기 단계, Ranges로 동적 파이프라인 구성, Undo/Redo 시 상태 관리 가능
- 유지보수성, 확장성, 타입 안전성, 선언적 표현력 모두 향상
- 전통적 구현 대비 훨씬 유연하고 강력한 template method 구현 가능
결론
템플릿 메서드(Template Method) 패턴은 상위 클래스에서 알고리즘 골격을 정의하고 하위 클래스가 일부 단계를 오버라이드하는 패턴이었으나, 모던 C++20 이상에서는 상속 없이 람다, Concepts, std::expected, std::format, coroutine, Ranges 등을 활용해 알고리즘 골격을 선언적이고 타입 안전하게 표현할 수 있습니다. 이를 통해 전통적 구현 대비 훨씬 적은 코드와 더 나은 유지보수성으로 Undo/Redo, 비동기 처리, 로깅, 조건부 단계 실행 등 다양한 요구사항을 처리할 수 있습니다.