이전 글에서는 팩토리 메서드(Factory Method) 패턴을 살펴보며, 가상 함수 기반의 고전적 구현을 어떻게 모던 C++20 이상의 기능(Concepts, std::expected, std::variant, std::optional, std::format)으로 개선할 수 있는지 논했습니다. 이번에는 추상 팩토리(Abstract Factory) 패턴으로 한 걸음 더 나아갑니다.
추상 팩토리는 "연관되거나 의존적인 객체 패밀리를 생성하는 인터페이스"를 제공하는 패턴입니다. 즉, 서로 관련 있는 여러 종류의 객체를 일관성 있게 생성할 수 있는 팩토리를 추상화하는 것입니다. 전통적인 C++ 구현에서는 인터페이스 클래스를 상속받아 구체 팩토리를 제공했고, 다양한 제품 계열(class family)을 다룰 때 복잡한 상속 계층이 필연적으로 생겼습니다.
모던 C++에서는 이러한 복잡성을 Concepts와 조합 가능한 팩토리(컴포지션), std::variant, std::expected 등의 기능을 통해 줄이고, 더 선언적이며 타입 안전한 방식으로 추상 팩토리를 구현할 수 있습니다.
패턴 소개: 추상 팩토리(Abstract Factory)
의도:
- 연관된 제품 객체(예: GUI 툴킷에서 Window, ScrollBar, Button)가 한 패밀리를 이루며, 이들의 생성 방식을 하나의 추상 팩토리 인터페이스로 캡슐화.
- 클라이언트는 구체적인 제품 클래스에 의존하지 않고, 추상 팩토리를 통해 해당 패밀리의 객체를 얻을 수 있음.
- 패밀리를 교체하려면 팩토리만 변경하면 되므로, 서로 다른 실행 환경(예: Windows용, Linux용 GUI)이 존재할 때 유용.
고전적 구현 문제점:
- 상속 기반 인터페이스 클래스(예: GUIFactory)와 이를 상속하는 구체 팩토리(WinFactory, LinuxFactory)를 만들어야 함.
- 새로운 제품 계열 추가 시 계층 구조 수정, 다수의 가상 함수 오버라이드 필요.
- 객체 생성 실패나 다양한 변형 옵션 처리 시 예외나 nullptr 반환 등 단순한 수단에 의존.
기존 C++ 스타일 구현 (C++11/14/17 정도)
예를 들어, GUI를 구성하는 Window, Button, ScrollBar를 한 패밀리로 묶어보겠습니다.
#include <memory>
#include <iostream>
// 추상 제품 인터페이스
struct Window {
virtual ~Window() = default;
virtual void render() const = 0;
};
struct Button {
virtual ~Button() = default;
virtual void click() = 0;
};
struct ScrollBar {
virtual ~ScrollBar() = default;
virtual void scroll(int delta) = 0;
};
// 구체 제품들 (Windows 스타일)
struct WinWindow : Window {
void render() const override {
std::cout << "Render WinWindow\n";
}
};
struct WinButton : Button {
void click() override { std::cout << "WinButton clicked\n"; }
};
struct WinScrollBar : ScrollBar {
void scroll(int delta) override { std::cout << "WinScrollBar scroll: " << delta << "\n"; }
};
// 추상 팩토리 인터페이스
struct GUIFactory {
virtual ~GUIFactory() = default;
virtual std::unique_ptr<Window> createWindow() const = 0;
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<ScrollBar> createScrollBar() const = 0;
};
// 구체 팩토리
struct WinFactory : GUIFactory {
std::unique_ptr<Window> createWindow() const override {
return std::make_unique<WinWindow>();
}
std::unique_ptr<Button> createButton() const override {
return std::make_unique<WinButton>();
}
std::unique_ptr<ScrollBar> createScrollBar() const override {
return std::make_unique<WinScrollBar>();
}
};
int main() {
WinFactory factory;
auto w = factory.createWindow();
auto b = factory.createButton();
auto s = factory.createScrollBar();
w->render();
b->click();
s->scroll(10);
}
고찰:
- 상속 계층 명확하나, 제품 유형 추가나 변형 시 인터페이스 수정 필요.
- 구체 팩토리마다 모든 제품을 전부 구현해야 하는 부담.
- 런타임 다형성(vtable)을 사용, 컴파일 타임 체크 제한.
모던 C++20 이상의 개선: Concepts, 조합 가능한 팩토리, Variant
1. Concepts로 추상 팩토리 요건 정의
Concepts를 사용해 추상 팩토리 요구사항을 타입 레벨에서 정의할 수 있습니다. 예를 들어, 팩토리가 createWindow(), createButton(), createScrollBar()를 제공해야 한다는 것을 Concept로 표현할 수 있습니다.
#include <concepts>
#include <memory>
#include <optional>
// 제품 인터페이스를 최소화하거나 값 기반 다형성으로 바꿀 수도 있지만,
// 여기서는 기존처럼 가상 함수 기반 제품 인터페이스를 그대로 둡니다.
template<typename F>
concept GUIFactoryConcept = requires(const F& f) {
{ f.createWindow() } -> std::convertible_to<std::unique_ptr<Window>>;
{ f.createButton() } -> std::convertible_to<std::unique_ptr<Button>>;
{ f.createScrollBar() } -> std::convertible_to<std::unique_ptr<ScrollBar>>;
};
고찰:
- 별도 추상 클래스 필요 없이, 팩토리 타입이 특정 메서드를 제공하는지 템플릿으로 검사 가능.
- 컴파일 타임에 조건 미충족 시 명확한 에러 메시지.
2. 조합 가능한 팩토리: 컨셉 충족 팩토리들을 합성
각 제품 타입별로 별도의 "미니 팩토리"를 만들고, 이를 조합해 전체를 구성할 수도 있습니다.
struct WindowFactory {
std::unique_ptr<Window> createWindow() const {
return std::make_unique<WinWindow>(); // 쉽게 WinWindow 고정
}
};
struct ButtonFactory {
std::unique_ptr<Button> createButton() const {
return std::make_unique<WinButton>();
}
};
struct ScrollBarFactory {
std::unique_ptr<ScrollBar> createScrollBar() const {
return std::make_unique<WinScrollBar>();
}
};
// 조합 팩토리: 하나의 클래스에서 세 메서드를 구현하기 위해 구조체들을 상속/조합
struct ComposedWinFactory : WindowFactory, ButtonFactory, ScrollBarFactory {
// 이 경우 WindowFactory, ButtonFactory, ScrollBarFactory가
// 각자 필요한 메서드를 제공하므로
// 별도 코드 없이 세 메서드를 모두 갖춘 팩토리 완성
// 단, 다중 상속에 주의. 또는 팩토리 객체를 멤버로 갖는 식도 가능.
};
// 또 다른 접근: 템플릿 매개변수로 팩토리 객체를 조합
template<typename WF, typename BF, typename SF>
struct CombinedFactory {
WF wf;
BF bf;
SF sf;
std::unique_ptr<Window> createWindow() const { return wf.createWindow(); }
std::unique_ptr<Button> createButton() const { return bf.createButton(); }
std::unique_ptr<ScrollBar> createScrollBar() const { return sf.createScrollBar(); }
};
static_assert(GUIFactoryConcept<ComposedWinFactory>);
static_assert(GUIFactoryConcept<CombinedFactory<WindowFactory,ButtonFactory,ScrollBarFactory>>);
// 이제 CombinedFactory를 통해 런타임에 다른 팩토리 조합 가능
고찰:
- 모던 C++은 상속 없이도 Concepts로 요구사항을 정의하고, 여러 팩토리를 합성(컴포지션)해 추상 팩토리 역할을 하는 객체를 만들 수 있음.
- 필요하다면 std::variant를 사용해 런타임에 다른 팩토리로 전환하는 것도 가능.
3. std::expected로 생성 실패 처리
연관된 제품 중 하나를 생성할 때 실패 가능성이 있다면 std::expected를 사용해 실패를 명확히 처리할 수 있습니다.
#include <expected>
struct RobustWindowFactory {
std::expected<std::unique_ptr<Window>,std::string> createWindow() const {
bool success = loadWindowResources();
if(!success) return std::unexpected("Failed to load Window resources");
return std::make_unique<WinWindow>();
}
bool loadWindowResources() const { return false; /* simulate fail */ }
};
struct RobustComposedFactory {
RobustWindowFactory wf;
ButtonFactory bf;
ScrollBarFactory sf;
std::expected<std::unique_ptr<Window>,std::string> createWindow() const { return wf.createWindow(); }
std::unique_ptr<Button> createButton() const { return bf.createButton(); }
std::unique_ptr<ScrollBar> createScrollBar() const { return sf.createScrollBar(); }
};
// now the user can handle failure gracefully
고찰:
- 특정 제품 생성이 실패하면 std::unexpected로 에러 메시지 반환.
- 호출자 코드에서 이를 체크해 대안 시나리오를 구현 가능.
4. 값 기반 다형성(std::variant)으로 다양한 제품 계열 지원
여러 GUI 테마(Win, Linux, Mac)를 지원하는 추상 팩토리를 생각해보자. 전통적으로는 if/else나 팩토리 클래스 상속을 통해 처리했을 텐데, std::variant와 std::visit를 활용하면 런타임에 다양한 팩토리를 하나의 변이에 담을 수 있다.
#include <variant>
// Linux 제품 예제
struct LinuxWindow : Window { void render() const override { std::cout << "Linux Window\n"; } };
struct LinuxButton : Button { void click() override { std::cout << "Linux Button click\n"; } };
struct LinuxScrollBar : ScrollBar { void scroll(int delta) override { std::cout << "Linux Scroll\n"; } };
struct LinuxFactory {
std::unique_ptr<Window> createWindow() const { return std::make_unique<LinuxWindow>(); }
std::unique_ptr<Button> createButton() const { return std::make_unique<LinuxButton>(); }
std::unique_ptr<ScrollBar> createScrollBar() const { return std::make_unique<LinuxScrollBar>(); }
};
using AnyGUIFactory = std::variant<WinFactory, LinuxFactory>;
static_assert(GUIFactoryConcept<WinFactory>);
static_assert(GUIFactoryConcept<LinuxFactory>);
void renderWindowFromAnyFactory(const AnyGUIFactory& factory) {
std::visit([](auto& f) {
auto w = f.createWindow();
w->render();
}, factory);
}
고찰:
- std::variant를 사용해 런타임에 다양한 팩토리를 선택할 수 있음.
- 상속이나 공통 추상 클래스를 강제하지 않아도 Concepts로 타입 제약을 검사할 수 있으나, std::visit 시 구체 타입별 처리가 필요.
- 이를 통해 코드를 유연하고 확장 가능하게 유지하면서 상속 계층과 vtable 의존을 최소화.
5. std::format을 통한 로깅
디버그나 로깅 상황에서 std::format을 사용해 제품 생성 과정을 명확히 기록할 수 있습니다.
#include <format>
struct LoggingButtonFactory {
std::unique_ptr<Button> createButton() const {
std::cout << std::format("[DEBUG]: Creating a WinButton\n");
return std::make_unique<WinButton>();
}
};
고찰:
- 단순한 개선점이지만, 코드 가독성과 유지보수성에 기여.
비교 및 분석
- 전통적 구현(C++11 전후):
- 추상 팩토리 인터페이스(가상 함수) + 구체 팩토리 상속 -> 상속 계층 복잡
- 제품 패밀리 추가 시 인터페이스 클래스 수정, vtable 기반 다형성
- 에러 처리 시 예외나 nullptr 반환 등 단순한 수단
- 모던 C++(C++20 이상):
- Concepts로 팩토리 요건 정적 검사: 상속 없이도 타입 요구사항 명시
- 개별 팩토리를 합성(조합)해 추상 팩토리 구성 -> 유연한 코드 구조
- std::expected로 실패를 명확히 처리, std::variant로 런타임에 다양한 팩토리를 선택
- 상속/다형성 의존도 감소, 템플릿 기반 접근으로 성능 및 유지보수성 증대
결국, 모던 C++ 환경에서는 추상 팩토리도 "상속 기반" 패턴에서 "콘셉트 기반" 패턴으로 진화할 수 있습니다. 다양한 언어 기능을 결합해 상속 없이, 또는 상속 최소화로도 패턴 구현이 가능해지며, 타입 안전성과 확장 용이성을 모두 챙길 수 있습니다.
마무리
추상 팩토리 패턴은 연관된 객체들의 일관적 생성 인터페이스를 제공하는 중요한 패턴입니다. 모던 C++20 이상에서 Concepts, std::expected, std::variant, std::format 등을 활용하면, 상속 계층에 의존하지 않고도 이 패턴을 구현할 수 있고, 에러 처리나 다양한 제품 계열 관리가 훨씬 단순하고 타입 안전해집니다.
다음 글에서는 구조적 패턴 중 하나인 브리지(Bridge) 패턴을 살펴보며, 구현과 인터페이스를 분리하는 이 전통적 패턴이 모던 C++에서 어떻게 간결하고 확장성 있게 재구성될 수 있는지 탐구해보겠습니다.