다크 모드를 지원하는 앱에서 버튼은 다크 테마로, 체크박스는 라이트 테마로 그려지는 버그는 흔하다. 화면 곳곳에서 new DarkButton, new LightCheckbox처럼 구체 클래스를 직접 만들면 “같은 패밀리끼리만 섞여야 한다”는 규칙을 지킬 곳이 사라지기 때문이다. 추상 팩토리(Abstract Factory)는 서로 관련된 객체들의 패밀리를 만드는 인터페이스를 하나로 묶어, 클라이언트가 구체 클래스를 모른 채 일관된 패밀리를 얻게 한다. 이 글에서는 GoF 생성 패턴의 첫 번째인 추상 팩토리를 C++ 가상 함수, 실행 시점 레지스트리, 템플릿과 concept, C 함수 포인터 테이블로 각각 구현해 본다.
예제는 모두 g++/gcc 13.3(Ubuntu 24.04), -std=c++20, -std=c11로 컴파일해 실행한 결과다.
이 시리즈의 다른 글
구체 클래스를 직접 만들 때의 문제
// mixed.cpp — 구체 클래스를 직접 생성하면 테마가 섞이기 쉽다
#include <iostream>
struct DarkButton { void draw() const { std::cout << "[Dark ] Button\n"; } };
struct LightButton { void draw() const { std::cout << "[Light] Button\n"; } };
struct DarkCheckbox { void draw() const { std::cout << "[Dark ] Checkbox\n"; } };
struct LightCheckbox { void draw() const { std::cout << "[Light] Checkbox\n"; } };
int main()
{
DarkButton ok;
LightCheckbox remember; // 실수: 다크 화면에 라이트 체크박스
ok.draw();
remember.draw();
}$ g++ -std=c++20 -Wall -O2 -o mixed mixed.cpp && ./mixed
[Dark ] Button
[Light] Checkbox
컴파일러는 아무 말도 하지 않는다. 테마 일관성은 타입 어디에도 표현되지 않았기 때문이다.
구조
| 참여자 | 역할 | 예제 |
|---|---|---|
| AbstractFactory | 제품 패밀리 전체를 만드는 연산 선언 | WidgetFactory |
| ConcreteFactory | 한 패밀리의 구체 제품을 생성 | LightFactory, DarkFactory |
| AbstractProduct | 제품 종류별 인터페이스 | Button, Checkbox |
| ConcreteProduct | 패밀리에 속한 실제 제품 | DarkButton, LightCheckbox … |
| Client | 추상 팩토리·추상 제품 인터페이스만 사용 | renderLoginForm() |
C++ 기본 구현
추상 제품과 추상 팩토리부터 정의한다. 제품 소유권은 std::unique_ptr로 호출자에게 넘긴다.
// widgets.hpp — 추상 제품과 추상 팩토리
#pragma once
#include <memory>
#include <string>
struct Button {
virtual ~Button() = default;
virtual std::string draw() const = 0;
};
struct Checkbox {
virtual ~Checkbox() = default;
virtual std::string draw() const = 0;
};
struct WidgetFactory {
virtual ~WidgetFactory() = default;
virtual std::unique_ptr<Button> createButton() const = 0;
virtual std::unique_ptr<Checkbox> createCheckbox() const = 0;
};// themes.hpp — 구체 제품과 구체 팩토리 (테마 패밀리 2개)
#pragma once
#include "widgets.hpp"
struct LightButton : Button {
std::string draw() const override { return "[Light] Button"; }
};
struct LightCheckbox : Checkbox {
std::string draw() const override { return "[Light] Checkbox"; }
};
struct DarkButton : Button {
std::string draw() const override { return "[Dark ] Button"; }
};
struct DarkCheckbox : Checkbox {
std::string draw() const override { return "[Dark ] Checkbox"; }
};
struct LightFactory : WidgetFactory {
std::unique_ptr<Button> createButton() const override { return std::make_unique<LightButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<LightCheckbox>(); }
};
struct DarkFactory : WidgetFactory {
std::unique_ptr<Button> createButton() const override { return std::make_unique<DarkButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<DarkCheckbox>(); }
};// basic.cpp — 클라이언트는 추상 팩토리만 안다
#include <iostream>
#include "themes.hpp"
void renderLoginForm(const WidgetFactory &f)
{
auto ok = f.createButton();
auto remember = f.createCheckbox();
std::cout << ok->draw() << '\n' << remember->draw() << '\n';
}
int main()
{
renderLoginForm(DarkFactory{});
std::cout << "--\n";
renderLoginForm(LightFactory{});
}$ g++ -std=c++20 -Wall -O2 -o basic basic.cpp && ./basic
[Dark ] Button
[Dark ] Checkbox
--
[Light] Button
[Light] Checkbox
renderLoginForm()에는 구체 클래스 이름이 하나도 없다. 어떤 팩토리를 넘기느냐가 패밀리 전체를 결정하므로 섞일 방법이 없다.
실행 시점에 패밀리 고르기
설정 파일이나 명령행 인자로 테마를 고르는 일이 많다. 이름 → 팩토리 생성 함수 맵을 두면 구체 팩토리를 아는 곳이 이 한 곳으로 줄어든다.
// registry.cpp — 실행 시점에 테마 이름으로 팩토리를 고른다
#include <functional>
#include <iostream>
#include <map>
#include "themes.hpp"
using FactoryMaker = std::function<std::unique_ptr<WidgetFactory>()>;
const std::map<std::string, FactoryMaker> ®istry()
{
static const std::map<std::string, FactoryMaker> r = {
{"light", [] { return std::make_unique<LightFactory>(); }},
{"dark", [] { return std::make_unique<DarkFactory>(); }},
};
return r;
}
int main(int argc, char **argv)
{
std::string theme = argc > 1 ? argv[1] : "light";
auto it = registry().find(theme);
if (it == registry().end()) {
std::cerr << "unknown theme: " << theme << '\n';
return 1;
}
auto factory = it->second();
std::cout << factory->createButton()->draw() << '\n'
<< factory->createCheckbox()->draw() << '\n';
}$ g++ -std=c++20 -Wall -O2 -o registry registry.cpp && for t in dark light solarized; do ./registry $t; echo "(exit $?)"; done
[Dark ] Button
[Dark ] Checkbox
(exit 0)
[Light] Button
[Light] Checkbox
(exit 0)
unknown theme: solarized
(exit 1)
확장 비용: 패밀리 추가 vs 제품 추가
새 패밀리(고대비 테마)는 클래스만 추가하면 된다. 클라이언트 함수는 basic.cpp와 똑같다.
// contrast.cpp — 새 패밀리(HighContrast)를 추가해도 renderLoginForm은 그대로다
#include <iostream>
#include "themes.hpp"
struct ContrastButton : Button {
std::string draw() const override { return "[HiCon] BUTTON"; }
};
struct ContrastCheckbox : Checkbox {
std::string draw() const override { return "[HiCon] [X] CHECKBOX"; }
};
struct HighContrastFactory : WidgetFactory {
std::unique_ptr<Button> createButton() const override { return std::make_unique<ContrastButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override { return std::make_unique<ContrastCheckbox>(); }
};
void renderLoginForm(const WidgetFactory &f) // basic.cpp와 동일
{
auto ok = f.createButton();
auto remember = f.createCheckbox();
std::cout << ok->draw() << '\n' << remember->draw() << '\n';
}
int main()
{
renderLoginForm(HighContrastFactory{});
}$ g++ -std=c++20 -Wall -O2 -o contrast contrast.cpp && ./contrast
[HiCon] BUTTON
[HiCon] [X] CHECKBOX
반대로 새 제품 종류(Slider)를 추가하면 추상 팩토리 인터페이스가 바뀌고, 모든 구체 팩토리가 이를 구현해야 한다. 하나라도 빠뜨리면 그 팩토리는 추상 클래스로 남는다.
// add_product.cpp — 추상 팩토리에 제품(Slider)을 추가하면 모든 구체 팩토리가 영향을 받는다
#include <memory>
#include <string>
struct Slider { virtual ~Slider() = default; virtual std::string draw() const = 0; };
struct DarkSlider : Slider { std::string draw() const override { return "[Dark ] Slider"; } };
struct WidgetFactory {
virtual ~WidgetFactory() = default;
virtual std::unique_ptr<Slider> createSlider() const = 0; // 새로 추가
};
struct DarkFactory : WidgetFactory {
std::unique_ptr<Slider> createSlider() const override { return std::make_unique<DarkSlider>(); }
};
struct LightFactory : WidgetFactory {
// createSlider()를 아직 구현하지 않음
};
int main()
{
DarkFactory dark;
LightFactory light; // 여기서 컴파일 에러
(void)dark; (void)light;
}$ g++ -std=c++20 -c add_product.cpp 2>&1 | head -10
add_product.cpp: In function ‘int main()’:
add_product.cpp:22:22: error: cannot declare variable ‘light’ to be of abstract type ‘LightFactory’
22 | LightFactory light; // 여기서 컴파일 에러
| ^~~~~
add_product.cpp:15:8: note: because the following virtual functions are pure within ‘LightFactory’:
15 | struct LightFactory : WidgetFactory {
| ^~~~~~~~~~~~
add_product.cpp:10:41: note: ‘virtual std::unique_ptr<Slider> WidgetFactory::createSlider() const’
10 | virtual std::unique_ptr<Slider> createSlider() const = 0; // 새로 추가
| ^~~~~~~~~~~~
| 변경 | 손대는 곳 | 클라이언트 영향 |
|---|---|---|
| 패밀리 추가 (HighContrast) | 구체 팩토리 1개 + 구체 제품 N개 추가 | 없음 |
| 제품 종류 추가 (Slider) | 추상 팩토리 수정 + 모든 구체 팩토리 수정 | 새 제품을 쓰는 곳만 |
테스트 대역 주입
팩토리를 인자로 받기 때문에 테스트에서는 호출을 기록하는 가짜 팩토리를 넣을 수 있다.
// mock.cpp — 테스트에서는 호출을 기록하는 가짜 팩토리를 주입한다
#include <cassert>
#include <iostream>
#include "widgets.hpp"
struct FakeButton : Button { std::string draw() const override { return "btn"; } };
struct FakeCheckbox : Checkbox { std::string draw() const override { return "chk"; } };
struct RecordingFactory : WidgetFactory {
mutable int buttons = 0, checkboxes = 0;
std::unique_ptr<Button> createButton() const override
{ ++buttons; return std::make_unique<FakeButton>(); }
std::unique_ptr<Checkbox> createCheckbox() const override
{ ++checkboxes; return std::make_unique<FakeCheckbox>(); }
};
std::string renderLoginForm(const WidgetFactory &f)
{
return f.createButton()->draw() + "," + f.createCheckbox()->draw();
}
int main()
{
RecordingFactory f;
std::string out = renderLoginForm(f);
assert(out == "btn,chk");
assert(f.buttons == 1 && f.checkboxes == 1);
std::cout << "ok: " << out << " (buttons=" << f.buttons
<< ", checkboxes=" << f.checkboxes << ")\n";
}$ g++ -std=c++20 -Wall -O2 -o mock mock.cpp && ./mock
ok: btn,chk (buttons=1, checkboxes=1)
컴파일 시점 추상 팩토리: 템플릿과 concept
패밀리를 실행 중에 바꿀 일이 없다면 패밀리를 하나의 타입으로 묶고 템플릿 인자로 넘길 수 있다. 가상 호출과 힙 할당이 없고, 패밀리에 빠진 제품은 concept이 잡아낸다.
// static_factory.cpp — 패밀리를 타입으로 묶어 컴파일 시점에 고르는 추상 팩토리
#include <concepts>
#include <iostream>
#include <string>
struct DarkTheme {
struct Button { std::string draw() const { return "[Dark ] Button"; } };
struct Checkbox { std::string draw() const { return "[Dark ] Checkbox"; } };
};
struct LightTheme {
struct Button { std::string draw() const { return "[Light] Button"; } };
struct Checkbox { std::string draw() const { return "[Light] Checkbox"; } };
};
template <class T>
concept Theme = requires {
{ typename T::Button{}.draw() } -> std::convertible_to<std::string>;
{ typename T::Checkbox{}.draw() } -> std::convertible_to<std::string>;
};
template <Theme T>
void renderLoginForm()
{
typename T::Button ok;
typename T::Checkbox remember;
std::cout << ok.draw() << '\n' << remember.draw() << '\n';
}
struct BrokenTheme { struct Button { std::string draw() const { return ""; } }; };
int main()
{
renderLoginForm<DarkTheme>();
renderLoginForm<LightTheme>();
#ifdef SHOW_ERROR
renderLoginForm<BrokenTheme>(); // Checkbox가 없는 패밀리
#endif
}$ g++ -std=c++20 -Wall -O2 -o static_factory static_factory.cpp && ./static_factory
[Dark ] Button
[Dark ] Checkbox
[Light] Button
[Light] Checkbox
$ g++ -std=c++20 -DSHOW_ERROR -c static_factory.cpp 2>&1 | grep -E 'error|constraints not satisfied|required expression|Checkbox' | head -6
static_factory.cpp:36:37: error: no matching function for call to ‘renderLoginForm<BrokenTheme>()’
36 | renderLoginForm<BrokenTheme>(); // Checkbox가 없는 패밀리
static_factory.cpp:22:6: note: constraints not satisfied
static_factory.cpp:18:38: note: the required expression ‘typename T::Checkbox{}.draw()’ is invalid
18 | { typename T::Checkbox{}.draw() } -> std::convertible_to<std::string>;
BrokenTheme에 Checkbox가 없다는 사실이 호출 지점에서 바로 드러난다. 이 방식은 템플릿 인자로 정책을 넘기는 설계 기법 (7) 템플릿 인자로 전달하는 정책과 같은 계열이다.
C 구현: 함수 포인터 테이블
C에서는 생성 함수 포인터를 담은 구조체가 추상 팩토리이고, const 전역 인스턴스가 구체 팩토리다.
/* factory.c — C에서는 함수 포인터 테이블이 추상 팩토리 역할을 한다 */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
struct widget {
const char *(*draw)(const struct widget *w);
void (*destroy)(struct widget *w);
};
struct widget_factory {
const char *name;
struct widget *(*create_button)(void);
struct widget *(*create_checkbox)(void);
};
static void widget_free(struct widget *w) { free(w); }
#define DEFINE_WIDGET(fn, text) \
static const char *fn##_draw(const struct widget *w) \
{ (void)w; return text; } \
static struct widget *fn(void) \
{ \
struct widget *w = malloc(sizeof(*w)); \
if (!w) \
abort(); \
w->draw = fn##_draw; \
w->destroy = widget_free; \
return w; \
}
DEFINE_WIDGET(dark_button, "[Dark ] Button")
DEFINE_WIDGET(dark_checkbox, "[Dark ] Checkbox")
DEFINE_WIDGET(light_button, "[Light] Button")
DEFINE_WIDGET(light_checkbox, "[Light] Checkbox")
static const struct widget_factory factories[] = {
{ "dark", dark_button, dark_checkbox },
{ "light", light_button, light_checkbox },
};
static void render_login_form(const struct widget_factory *f)
{
struct widget *ok = f->create_button();
struct widget *remember = f->create_checkbox();
printf("%s\n%s\n", ok->draw(ok), remember->draw(remember));
ok->destroy(ok);
remember->destroy(remember);
}
int main(int argc, char **argv)
{
const char *theme = argc > 1 ? argv[1] : "light";
for (size_t i = 0; i < sizeof(factories) / sizeof(factories[0]); i++) {
if (strcmp(factories[i].name, theme) == 0) {
render_login_form(&factories[i]);
return 0;
}
}
fprintf(stderr, "unknown theme: %s\n", theme);
return 1;
}$ gcc -std=c11 -Wall -Wextra -O2 -fsanitize=address -o factory factory.c && ./factory dark && ./factory light
[Dark ] Button
[Dark ] Checkbox
[Light] Button
[Light] Checkbox
AddressSanitizer를 켜고 돌려도 누수 보고가 없다. 생성한 쪽과 짝이 맞는 destroy를 제품 자신이 들고 다니는 것이 C 구현의 요점이다.
다른 생성 패턴과 비교
| 패턴 | 초점 | 이 예제에 대입하면 |
|---|---|---|
| 추상 팩토리 | 관련 객체 패밀리의 일관성 | 테마별 Button + Checkbox 세트 |
| 팩토리 메서드 | 하나의 객체 생성을 서브클래스에 위임 | 각 화면 클래스가 createButton()만 재정의 |
| 빌더 | 복잡한 객체를 단계별로 조립 | 버튼 여러 개 + 레이아웃을 가진 폼 하나를 조립 |
| 프로토타입 | 기존 객체를 복제해 생성 | 미리 만든 테마 위젯을 clone() |
실제로는 추상 팩토리의 각 생성 메서드가 팩토리 메서드로 구현되는 경우가 많다. 위 LightFactory::createButton()이 바로 그렇다.
주의사항
- 제품 종류가 자주 늘어나는 곳에는 맞지 않는다: 위 표처럼 제품 하나를 추가할 때마다 모든 구체 팩토리를 고쳐야 한다. 패밀리가 2개뿐이고 제품 종류만 계속 늘어난다면 팩토리 메서드나 단순 함수가 낫다.
- 패밀리가 하나뿐이면 과설계다: 테마나 플랫폼처럼 “교체 가능한 세트”가 실제로 2개 이상 있을 때 의미가 있다.
- 팩토리 자체는 상태가 없게: 예제의 구체 팩토리는 멤버가 없어
const참조로 넘길 수 있다. 팩토리가 캐시나 설정을 들고 있으면 스레드 안전성과 수명 문제가 따라온다. - 반환 타입은 소유권을 드러내게: 원시 포인터를 반환하면 누가
delete할지 모호하다. C++에서는std::unique_ptr, C에서는 제품에 짝이 되는destroy를 붙인다. - 실행 시점 선택이 필요 없으면 템플릿: 가상 함수 기반은 런타임 교체와 플러그인에 유리하고, 템플릿 기반은 오버헤드가 없지만 패밀리마다 코드가 따로 생성된다.
마무리
추상 팩토리의 핵심은 “어떤 것들을 함께 써야 하는가”를 팩토리 타입 하나로 표현하는 것이다. 클라이언트는 팩토리 하나만 받으면 되고, 패밀리를 바꾸는 일은 팩토리를 바꾸는 일 하나로 끝난다. 다음 글에서는 복잡한 객체를 단계별로 조립하는 빌더(Builder) 패턴을 다룬다.
참고
- Erich Gamma 외, 『Design Patterns: Elements of Reusable Object-Oriented Software』(GoF), Abstract Factory
- cppreference — std::unique_ptr
- cppreference — Constraints and concepts
- C++ 예제로 배우는 소프트웨어 설계 기법 (5) — 도형 편집기(Shape) (자기 등록 팩토리)