이전 글에서는 빌더(Builder) 패턴을 모던 C++ 관점에서 재해석하며, 상속 없이 람다, Concepts, std::expected, coroutine, Ranges, std::format 등을 활용해 단계별 객체 생성 과정을 간결히 표현하고, 조건부 단계, 비동기 처리, 로깅, 에러 처리 등 다양한 요구사항을 손쉽게 처리할 수 있음을 확인했습니다. 이제는 생성(Creational) 패턴 중 팩토리 메서드(Factory Method) 패턴을 다룹니다.
팩토리 메서드 패턴은 객체 생성 로직을 서브클래스(또는 별도의 로직)로 위임하여, 상위 클래스가 어떤 객체를 생성할지 모르지만, 하위 클래스나 제공된 로직에 따라 객체를 생성하는 패턴입니다. 전통적으로는 Factory Method를 오버라이드하는 하위 클래스를 정의했지만, 이는 클래스 증가와 유지보수 어려움을 초래했습니다.
C++20 이상에서는 Concepts와 람다, std::function, std::expected, coroutine, Ranges, std::format 등을 활용해 상속 없이도 객체 생성 로직을 간결하고 타입 안전하게 표현할 수 있습니다. 이를 통해 조건부 객체 생성, 비동기 생성, 로깅, 에러 처리 등 다양한 요구사항에도 쉽게 대응 가능하며, 전통적 구현 대비 유지보수성과 확장성이 크게 향상됩니다.
패턴 소개: 팩토리 메서드(Factory Method)
의도:
- 객체 생성 로직을 별도 메서드(팩토리 메서드)에 위임, 상위 클래스는 생성 시점을 제어하지만 구체 생성 로직은 하위 로직이나 별도 구현체에 맡김.
- 예: Document 클래스가 createPage() 메서드를 통해 문서 페이지를 생성하지만, 구체 페이지 타입은 하위 구현에 따라 결정.
전통적 구현 문제점:
- 팩토리 메서드를 오버라이드하는 하위 클래스 필요
- 새로운 제품 타입 추가 시 클래스 증가
- 비동기 생성, 에러 처리, 로깅, 조건부 생성 등 기능 추가 시 복잡성 증가
기존 C++ 스타일 구현 (전통적 방식)
예를 들어, Document 클래스를 상속한 TextDocument, PDFDocument 등 하위 클래스가 createPage()를 오버라이드:
#include <iostream>
#include <memory>
#include <string>
struct Page {
virtual ~Page()=default;
virtual void render()=0;
};
struct TextPage : Page {
void render() override { std::cout<<"TextPage\n"; }
};
struct PDFPage : Page {
void render() override { std::cout<<"PDFPage\n"; }
};
struct Document {
virtual ~Document()=default;
virtual std::unique_ptr<Page> createPage()=0;
void printPage() {
auto p = createPage();
p->render();
}
};
struct TextDocument : Document {
std::unique_ptr<Page> createPage() override {
return std::make_unique<TextPage>();
}
};
struct PDFDocument : Document {
std::unique_ptr<Page> createPage() override {
return std::make_unique<PDFPage>();
}
};
int main() {
std::unique_ptr<Document> doc = std::make_unique<TextDocument>();
doc->printPage(); // TextPage
}
문제점:
- Document 인터페이스, TextDocument, PDFDocument 하위 클래스 필요
- 새로운 페이지 타입 추가 시 클래스 증가
- 비동기 생성, 조건부 생성, 로깅, 에러 처리 시 복잡성 증가
모던 C++20 이상 개선: Concepts, 람다, std::expected
1. Concepts로 팩토리 메서드 인터페이스 정의
팩토리 메서드: ( ) -> std::expected<Page,std::string> 가정, 에러 발생 가능성도 반영.
struct Page2 {
std::string type;
};
template<typename F>
concept PageFactory = requires(F& f) {
{ f() } -> std::convertible_to<std::expected<Page2,std::string>>;
};
2. 람다로 페이지 생성 로직 정의
auto createTextPage = []()->std::expected<Page2,std::string> {
std::cout<<"Creating TextPage\n";
return Page2{"TextPage"};
};
auto createPDFPage = []()->std::expected<Page2,std::string> {
std::cout<<"Creating PDFPage\n";
return Page2{"PDFPage"};
};
static_assert(PageFactory<decltype(createTextPage)>);
3. Document 없이도 상위 로직 합성
Document가 아닌 단순 함수로 printPage() 구현 가능. Factory 메서드를 함수로 주입:
auto printPage = [](PageFactory auto factory) -> std::expected<void,std::string> {
auto pageRes = factory();
if(!pageRes) return std::unexpected(pageRes.error());
std::cout << pageRes->type << " rendered.\n";
return {};
};
int main() {
auto res = printPage(createTextPage);
if(!res) std::cerr<<"Error:"<<res.error()<<"\n";
printPage(createPDFPage);
}
출력:
Creating TextPage
TextPage rendered.
Creating PDFPage
PDFPage rendered.
비교:
- 전통적: 상위 클래스, 하위 클래스 상속 필요
- C++20: 람다로 생성 로직 정의, 상속 없이 타입 안전한 팩토리 메서드 구현
조건부 생성, 비동기 처리, 로깅, Ranges 적용
조건부 생성(예: 특정 조건에서 다른 페이지 생성):
auto conditionalPageFactory = [&](auto baseFactory1, auto baseFactory2) {
return [=]() -> std::expected<Page2,std::string> {
bool usePDF = false; // 조건 가정
return usePDF ? baseFactory2() : baseFactory1();
};
};
auto hybridFactory = conditionalPageFactory(createTextPage, createPDFPage);
printPage(hybridFactory);
비동기 생성: coroutine으로 페이지 생성 비동기화 가능.
로깅(std::format):
auto loggingFactory = [&](auto baseFactory) {
return [=]() -> std::expected<Page2,std::string> {
std::cout << "[LOG] Creating page...\n";
auto res = baseFactory();
if(!res) std::cout << "[LOG] Error: " << res.error() << "\n";
else std::cout << "[LOG] " << res->type << " created.\n";
return res;
};
};
auto loggedFactory = loggingFactory(createTextPage);
printPage(loggedFactory);
Ranges: 여러 팩토리 리스트를 순회하며 페이지 생성:
#include <vector>
#include <ranges>
std::vector<decltype(createTextPage)> factories = {createTextPage, createPDFPage};
for (auto res : factories | std::views::transform(printPage)) {
if(!res) std::cerr<<"One factory failed:"<<res.error()<<"\n";
}
비교:
- 전통적: 조건부 생성, 비동기 처리, 로깅, Ranges 파이프라인 등 추가 시 클래스나 코드 복잡 증가
- C++20: 람다 합성으로 기능 추가, coroutine으로 비동기, Ranges로 파이프라인 처리, std::expected로 에러 처리 등 다양한 요구사항 쉽게 처리
전통적 구현 vs C++11/14/17 vs C++20 이상 비교
전통적(C++98/03):
- AbstractFactory 인터페이스 + ConcreteFactory 상속 필요
- 제품 생성 메서드 오버라이드, 새로운 로직 추가 시 클래스 증가
- 조건부 생성, 비동기 처리, 로깅, 에러 처리 등 구현 시 복잡성 증가
C++11/14/17:
- 람다, std::function으로 일부 단순화 가능하지만 Concepts, coroutine, std::expected, Ranges 미지원
- 타입 안전한 인터페이스 제약, 비동기/에러 처리 간단 표현 한계
C++20 이상(모던 C++):
- Concepts로 팩토리 메서드 요구사항 정의, 상속 없이 타입 안전한 인터페이스 구현
- 람다 합성과 std::expected로 에러 처리 명확, std::format으로 로깅, coroutine으로 비동기 처리, Ranges로 파이프라인 실행
- 클래스 증가 없이 확장 가능, 유지보수성, 확장성, 타입 안전성 대폭 향상
결론
팩토리 메서드(Factory Method) 패턴은 객체 생성 로직을 하위나 별도 로직에 위임하는 패턴이지만, 전통적 구현은 Factory Method를 오버라이드하는 하위 클래스로 인한 클래스 증가와 유지보수 어려움을 초래했습니다.
C++20 이상에서는 Concepts, 람다, std::function, std::expected, std::format, coroutine, Ranges 등을 활용해 상속 없이도 객체 생성 로직을 함수 합성 형태로 표현할 수 있으며, 조건부 생성, 비동기 생성, 로깅, 에러 처리 등 다양한 요구사항에 적은 코드로 대응 가능합니다. 전통적 구현 대비 유지보수성과 확장성이 크게 개선됩니다.