내보내기 기능을 만든다고 하자. “헤더를 쓰고, 행을 하나씩 쓰고, 마무리한다”는 절차는 CSV든 JSON이든 같은데, 어떤 Writer 객체를 쓸지만 다르다. 이 절차를 가진 부모 클래스가 new CsvWriter를 직접 부르면 JSON을 지원할 때마다 부모를 고쳐야 한다. 팩토리 메서드(Factory Method)는 객체를 만드는 연산을 가상 함수로 빼서, 어떤 클래스를 생성할지는 서브클래스가 결정하게 한다. 이 글에서는 기본형, 기본 구현이 있는 팩토리 메서드, 매개변수화된 팩토리 메서드, 생성자에서 부를 때의 함정, CRTP 변형, 그리고 흔히 같은 이름으로 불리는 정적 팩토리 함수와 C 구현까지 다룬다.
이 시리즈의 다른 글
- C/C++ 디자인 패턴 (1) — 추상 팩토리(Abstract Factory)
- C/C++ 디자인 패턴 (2) — 빌더(Builder)
- C/C++ 디자인 패턴 (4) — 프로토타입(Prototype)
- C/C++ 디자인 패턴 (5) — 싱글톤(Singleton)
구조
| 참여자 | 역할 | 예제 |
|---|---|---|
| Product | 팩토리 메서드가 만드는 객체의 인터페이스 | Writer |
| ConcreteProduct | 실제 제품 | CsvWriter, JsonWriter |
| Creator | 팩토리 메서드를 선언하고, 이를 호출하는 절차를 가짐 | Exporter (run(), createWriter()) |
| ConcreteCreator | 팩토리 메서드를 재정의해 ConcreteProduct를 반환 | CsvExporter, JsonExporter |
기본형
// exporter.cpp — 프레임워크(run)는 고정, 무엇을 만들지는 서브클래스가 정한다
#include <iostream>
#include <memory>
#include <string>
#include <vector>
struct Row { std::string name; int score; };
struct Writer {
virtual ~Writer() = default;
virtual void begin() {}
virtual void write(const Row &r) = 0;
virtual void end() {}
};
class Exporter {
public:
virtual ~Exporter() = default;
void run(const std::vector<Row> &rows) // 변하지 않는 절차
{
auto w = createWriter(); // 팩토리 메서드 호출
w->begin();
for (const auto &r : rows)
w->write(r);
w->end();
}
protected:
virtual std::unique_ptr<Writer> createWriter() = 0; // 팩토리 메서드
};
struct CsvWriter : Writer {
void begin() override { std::cout << "name,score\n"; }
void write(const Row &r) override { std::cout << r.name << ',' << r.score << '\n'; }
};
struct JsonWriter : Writer {
bool first = true;
void begin() override { std::cout << '['; }
void write(const Row &r) override
{
std::cout << (first ? "" : ",") << "{\"name\":\"" << r.name << "\",\"score\":" << r.score << '}';
first = false;
}
void end() override { std::cout << "]\n"; }
};
struct CsvExporter : Exporter {
std::unique_ptr<Writer> createWriter() override { return std::make_unique<CsvWriter>(); }
};
struct JsonExporter : Exporter {
std::unique_ptr<Writer> createWriter() override { return std::make_unique<JsonWriter>(); }
};
int main()
{
std::vector<Row> rows = {{"kim", 90}, {"lee", 85}};
CsvExporter{}.run(rows);
JsonExporter{}.run(rows);
}$ g++ -std=c++20 -Wall -Wextra -O2 -o exporter exporter.cpp && ./exporter
name,score
kim,90
lee,85
[{"name":"kim","score":90},{"name":"lee","score":85}]
run()은 어떤 Writer가 만들어지는지 모른다. 새 형식은 ConcreteCreator와 ConcreteProduct 한 쌍을 추가하는 것으로 끝난다.
기본 구현이 있는 팩토리 메서드
팩토리 메서드가 꼭 순수 가상일 필요는 없다. 부모가 합리적인 기본 제품을 만들고, 다른 제품이 필요한 서브클래스만 재정의한다.
// default_fm.cpp — 기본 구현이 있는 팩토리 메서드: 필요한 서브클래스만 재정의
#include <iostream>
#include <memory>
#include <string>
struct Connection {
virtual ~Connection() = default;
virtual std::string describe() const { return "TCP connection"; }
};
struct TlsConnection : Connection {
std::string describe() const override { return "TLS connection"; }
};
class Client {
public:
virtual ~Client() = default;
void request(const std::string &path)
{
auto c = makeConnection();
std::cout << "GET " << path << " over " << c->describe() << '\n';
}
protected:
virtual std::unique_ptr<Connection> makeConnection()
{ return std::make_unique<Connection>(); } // 기본값
};
struct SecureClient : Client {
protected:
std::unique_ptr<Connection> makeConnection() override
{ return std::make_unique<TlsConnection>(); }
};
int main()
{
Client{}.request("/health");
SecureClient{}.request("/login");
}$ g++ -std=c++20 -Wall -Wextra -O2 -o default_fm default_fm.cpp && ./default_fm
GET /health over TCP connection
GET /login over TLS connection
매개변수화된 팩토리 메서드
식별자를 받아 여러 종류의 제품 중 하나를 만드는 형태다. 서브클래스는 새 식별자만 처리하고 나머지는 부모에게 넘긴다.
// param_fm.cpp — 매개변수화된 팩토리 메서드: 식별자로 제품을 고른다
#include <iostream>
#include <memory>
#include <string_view>
struct Codec {
virtual ~Codec() = default;
virtual std::string_view name() const = 0;
};
struct Gzip : Codec { std::string_view name() const override { return "gzip"; } };
struct Zstd : Codec { std::string_view name() const override { return "zstd"; } };
struct Identity : Codec { std::string_view name() const override { return "identity"; } };
class Decoder {
public:
virtual ~Decoder() = default;
virtual std::unique_ptr<Codec> createCodec(std::string_view encoding) const
{
if (encoding == "gzip") return std::make_unique<Gzip>();
if (encoding == "" || encoding == "identity") return std::make_unique<Identity>();
return nullptr; // 모르는 인코딩
}
};
// 서브클래스는 식별자 하나만 추가하고 나머지는 부모에게 넘긴다
class ModernDecoder : public Decoder {
public:
std::unique_ptr<Codec> createCodec(std::string_view encoding) const override
{
if (encoding == "zstd") return std::make_unique<Zstd>();
return Decoder::createCodec(encoding);
}
};
int main()
{
ModernDecoder d;
for (std::string_view e : {"gzip", "zstd", "", "br"}) {
auto c = d.createCodec(e);
std::cout << '"' << e << "\" -> " << (c ? c->name() : "unsupported") << '\n';
}
std::cout << "base zstd -> " << (Decoder{}.createCodec("zstd") ? "ok" : "unsupported") << '\n';
}$ g++ -std=c++20 -Wall -Wextra -O2 -o param_fm param_fm.cpp && ./param_fm
"gzip" -> gzip
"zstd" -> zstd
"" -> identity
"br" -> unsupported
base zstd -> unsupported
같은 "zstd"라도 부모 Decoder는 지원하지 않고, ModernDecoder만 지원한다. 지원 범위를 서브클래스가 넓힌 것이다.
생성자에서 부르면 안 되는 이유
C++에서는 부모 생성자가 실행되는 동안 객체의 동적 타입이 아직 부모다. 그래서 생성자 안에서 부른 팩토리 메서드는 재정의된 버전이 아니라 부모 버전으로 디스패치된다.
// ctor_trap.cpp — 생성자에서 부른 팩토리 메서드는 파생 클래스 버전이 아니다
#include <iostream>
#include <memory>
#include <string>
struct Logger {
virtual ~Logger() = default;
virtual std::string name() const { return "ConsoleLogger"; }
};
struct FileLogger : Logger {
std::string name() const override { return "FileLogger"; }
};
class Service {
public:
Service() : log_(createLogger()) {} // 함정
virtual ~Service() = default;
std::string loggerName() const { return log_->name(); }
protected:
virtual std::unique_ptr<Logger> createLogger() { return std::make_unique<Logger>(); }
private:
std::unique_ptr<Logger> log_;
};
struct FileService : Service {
protected:
std::unique_ptr<Logger> createLogger() override { return std::make_unique<FileLogger>(); }
};
// 해결: 생성이 끝난 뒤 팩토리 메서드를 부르는 2단계 생성
template <class T>
std::unique_ptr<T> make_initialized()
{
auto p = std::make_unique<T>();
p->init();
return p;
}
class Service2 {
public:
virtual ~Service2() = default;
void init() { log_ = createLogger(); }
std::string loggerName() const { return log_->name(); }
protected:
virtual std::unique_ptr<Logger> createLogger() { return std::make_unique<Logger>(); }
private:
std::unique_ptr<Logger> log_;
};
struct FileService2 : Service2 {
protected:
std::unique_ptr<Logger> createLogger() override { return std::make_unique<FileLogger>(); }
};
int main()
{
FileService s;
std::cout << "ctor call : " << s.loggerName() << '\n';
std::cout << "two-phase : " << make_initialized<FileService2>()->loggerName() << '\n';
}$ g++ -std=c++20 -Wall -Wextra -O2 -o ctor_trap ctor_trap.cpp && ./ctor_trap
ctor call : ConsoleLogger
two-phase : FileLogger
FileService인데도 ConsoleLogger가 만들어졌다. 생성이 끝난 뒤 init()에서 부르는 2단계 생성이나, 필요할 때 만드는 지연 생성으로 피한다.
팩토리 메서드가 순수 가상이면 더 나쁘다. 생성자에서 직접 부르면 링크가 안 되고, 다른 멤버 함수를 거쳐 부르면 런타임에 중단된다.
// pure_direct.cpp — 생성자에서 직접 호출
#include <memory>
struct P { virtual ~P() = default; };
struct Base {
Base() { p = create(); }
virtual ~Base() = default;
virtual std::unique_ptr<P> create() = 0;
std::unique_ptr<P> p;
};
struct Derived : Base {
std::unique_ptr<P> create() override { return std::make_unique<P>(); }
};
int main() { Derived d; }$ g++ -std=c++20 -Wall -Wextra -O0 -o pure_direct pure_direct.cpp 2>&1 | grep -E 'warning|undefined|error'
pure_direct.cpp:5:28: warning: pure virtual ‘virtual std::unique_ptr<P> Base::create()’ called from constructor
pure_direct.cpp:(.text._ZN4BaseC2Ev[_ZN4BaseC5Ev]+0x4d): undefined reference to `Base::create()'
collect2: error: ld returned 1 exit status
// pure_trap.cpp — 생성자에서 순수 가상 팩토리 메서드를 부르면
#include <memory>
struct P { virtual ~P() = default; };
struct Base {
Base() { init(); } // 간접 호출
void init() { p = create(); }
virtual ~Base() = default;
virtual std::unique_ptr<P> create() = 0;
std::unique_ptr<P> p;
};
struct Derived : Base {
std::unique_ptr<P> create() override { return std::make_unique<P>(); }
};
int main() { Derived d; }$ g++ -std=c++20 -Wall -Wextra -O0 -o pure_trap pure_trap.cpp && ./pure_trap; echo "exit=$?"
pure virtual method called
terminate called without an active exception
bash: line 1: 33232 Aborted (core dumped) ./pure_trap
exit=134
CRTP: 가상 함수 없는 팩토리 메서드
Creator의 종류가 컴파일 시점에 정해진다면 CRTP로 정적 디스패치할 수 있다.
// crtp_fm.cpp — 가상 함수 없이 컴파일 시점에 팩토리 메서드를 고른다(CRTP)
#include <iostream>
#include <memory>
#include <string>
struct Parser { virtual ~Parser() = default; virtual std::string kind() const = 0; };
struct IniParser : Parser { std::string kind() const override { return "ini"; } };
struct TomlParser : Parser { std::string kind() const override { return "toml"; } };
template <class Derived>
class ConfigLoader {
public:
void load(const std::string &path)
{
auto p = static_cast<Derived *>(this)->createParser(); // 정적 디스패치
std::cout << path << " parsed with " << p->kind() << '\n';
}
};
struct IniLoader : ConfigLoader<IniLoader> {
std::unique_ptr<Parser> createParser() { return std::make_unique<IniParser>(); }
};
struct TomlLoader : ConfigLoader<TomlLoader> {
std::unique_ptr<Parser> createParser() { return std::make_unique<TomlParser>(); }
};
int main()
{
IniLoader{}.load("app.ini");
TomlLoader{}.load("Cargo.toml");
}$ g++ -std=c++20 -Wall -Wextra -O2 -o crtp_fm crtp_fm.cpp && ./crtp_fm
app.ini parsed with ini
Cargo.toml parsed with toml
대신 IniLoader와 TomlLoader는 공통 부모가 없는 서로 다른 타입이 되어, 하나의 컨테이너에 담아 다룰 수 없다.
정적 팩토리 함수와 구별하기
실무에서 “팩토리 메서드”라고 부르는 것의 상당수는 GoF의 그것이 아니라, 생성자 대신 이름 있는 정적 함수로 객체를 만드는 관용구(named constructor)다. 상속이 없고, 실패를 std::optional 등으로 표현할 수 있다는 장점이 있다.
// named_ctor.cpp — 정적 팩토리 함수(named constructor): GoF 팩토리 메서드와는 다른 관용구
#include <charconv>
#include <cstdint>
#include <iostream>
#include <optional>
#include <string_view>
class Color {
public:
static Color fromRgb(std::uint8_t r, std::uint8_t g, std::uint8_t b) { return {r, g, b}; }
static std::optional<Color> fromHex(std::string_view s)
{
if (s.size() != 7 || s[0] != '#')
return std::nullopt;
unsigned v = 0;
auto [p, ec] = std::from_chars(s.data() + 1, s.data() + 7, v, 16);
if (ec != std::errc{} || p != s.data() + 7)
return std::nullopt;
return Color(v >> 16, (v >> 8) & 0xff, v & 0xff);
}
void print() const { std::cout << "rgb(" << +r_ << ',' << +g_ << ',' << +b_ << ")\n"; }
private:
Color(std::uint8_t r, std::uint8_t g, std::uint8_t b) : r_(r), g_(g), b_(b) {}
std::uint8_t r_, g_, b_;
};
int main()
{
Color::fromRgb(255, 128, 0).print();
for (auto s : {"#1e90ff", "#zzzzzz", "1e90ff"}) {
auto c = Color::fromHex(s);
std::cout << s << " -> ";
c ? c->print() : void(std::cout << "invalid\n");
}
}$ g++ -std=c++20 -Wall -Wextra -O2 -o named_ctor named_ctor.cpp && ./named_ctor
rgb(255,128,0)
#1e90ff -> rgb(30,144,255)
#zzzzzz -> invalid
1e90ff -> invalid
| 구분 | GoF 팩토리 메서드 | 정적 팩토리 함수 |
|---|---|---|
| 형태 | 가상 멤버 함수 | static 멤버 함수 |
| 무엇을 바꾸나 | 서브클래스가 생성할 타입 | 생성 방법(입력 형식, 검증) |
| 상속 | 필요 | 불필요 |
| 예 | Exporter::createWriter() | Color::fromHex(), std::make_unique |
C 구현
C에서는 생성 콜백을 담은 구조체가 Creator다. 절차 함수는 콜백이 무엇을 만드는지 모른다.
/* exporter.c — C에서는 "생성 콜백"을 가진 구조체가 팩토리 메서드 역할을 한다 */
#include <stdio.h>
#include <stdlib.h>
struct writer {
void (*write)(struct writer *w, const char *name, int score);
void (*finish)(struct writer *w);
int count;
};
struct exporter {
const char *label;
struct writer *(*create_writer)(void); /* 팩토리 메서드 */
};
static void csv_write(struct writer *w, const char *n, int s) { printf("%s,%d\n", n, s); w->count++; }
static void csv_finish(struct writer *w) { printf("# %d rows\n", w->count); free(w); }
static void tsv_write(struct writer *w, const char *n, int s) { printf("%s\t%d\n", n, s); w->count++; }
static void tsv_finish(struct writer *w) { free(w); }
static struct writer *new_writer(void (*wr)(struct writer *, const char *, int),
void (*fin)(struct writer *))
{
struct writer *w = calloc(1, sizeof(*w));
if (!w)
abort();
w->write = wr;
w->finish = fin;
return w;
}
static struct writer *csv_create(void) { return new_writer(csv_write, csv_finish); }
static struct writer *tsv_create(void) { return new_writer(tsv_write, tsv_finish); }
/* 변하지 않는 절차: 어떤 writer인지 모른다 */
static void exporter_run(const struct exporter *e)
{
struct writer *w = e->create_writer();
printf("[%s]\n", e->label);
w->write(w, "kim", 90);
w->write(w, "lee", 85);
w->finish(w);
}
int main(void)
{
const struct exporter csv = { "csv", csv_create };
const struct exporter tsv = { "tsv", tsv_create };
exporter_run(&csv);
exporter_run(&tsv);
return 0;
}$ gcc -std=c11 -Wall -Wextra -O2 -fsanitize=address -o exporter_c exporter.c && ./exporter_c
[csv]
kim,90
lee,85
# 2 rows
[tsv]
kim 90
lee 85
리눅스 커널의 struct file_system_type도 비슷한 모양이다. 마운트 절차는 VFS가 갖고 있고, 각 파일시스템은 int (*init_fs_context)(struct fs_context *) 콜백에서 자기 전용 데이터와 연산 테이블을 채워 넣는다.
추상 팩토리와 비교
| 구분 | 팩토리 메서드 | 추상 팩토리 |
|---|---|---|
| 만드는 것 | 제품 하나 | 관련 제품 패밀리 |
| 확장 수단 | 상속(Creator 서브클래스) | 합성(팩토리 객체를 주입) |
| 쓰는 쪽 | Creator 자신의 절차(run()) | 팩토리를 받은 외부 클라이언트 |
주의사항
- 생성자·소멸자에서 호출하지 않는다: 소멸자도 마찬가지로, 파생 부분이 이미 파괴된 뒤라 부모 버전이 불린다.
- 제품마다 Creator 서브클래스가 생긴다: 제품 종류만큼 클래스가 늘어난다. 생성만 바꾸려는 경우라면 팩토리 함수를
std::function으로 주입하는 편이 가볍다. - 매개변수화된 형태는
nullptr처리: 모르는 식별자에 대한 반환을 정해 두지 않으면 호출자가 null을 역참조한다. 예외를 던지거나 기본 제품을 돌려주는 것도 방법이다. - 반환은
std::unique_ptr: 공유가 필요할 때만 호출자가std::shared_ptr로 바꾼다. 반대 방향 변환은 안 된다. - 공변 반환 타입은 원시 포인터만:
Derived* create() override처럼 반환 타입을 좁히는 것은 원시 포인터·참조에서만 허용되고unique_ptr에는 적용되지 않는다.
마무리
팩토리 메서드는 “절차는 부모가, 재료는 자식이”라는 분담을 생성에 적용한 것이다. 템플릿 메서드 패턴과 함께 쓰이는 경우가 많고, 추상 팩토리의 각 생성 연산도 대개 팩토리 메서드로 구현된다. 다음 글에서는 기존 객체를 복제해 새 객체를 만드는 프로토타입(Prototype)을 다룬다.
참고
- Erich Gamma 외, 『Design Patterns: Elements of Reusable Object-Oriented Software』(GoF), Factory Method
- isocpp FAQ — Calling virtual functions from constructors
- C++ 예제로 배우는 소프트웨어 설계 기법 (5) — 도형 편집기(Shape) (자기 등록 팩토리)