무엇인가
무엇을 만들지 고르는 일을 밖으로 빼낸다. 쓰는 쪽은 구상 타입을 모른 채 추상 타입만 받아 쓴다.
이 패턴은 부분적으로만 접힌다. 공장은 함수 하나라 접히지만, 만들어지는 물건 쪽 계층은 그대로 남는다. 그 구분이 이 문서의 요점이다.
구조
graph TD CLI[쓰는 쪽] -->|이름으로 요청| F[공장] F -->|추상 타입으로 반환| P["«interface» Ingredient"] P --> P1[Dough] P --> P2[Cheese] P --> P3[Tomato]
클래스판
공장도 계층, 물건도 계층이다.
#include <iostream>
#include <memory>
#include <string>
struct Ingredient {
virtual ~Ingredient() = default;
virtual void draw() const = 0;
};
struct Dough : Ingredient {
void draw() const override { std::cout << "반죽\n"; }
};
struct Cheese : Ingredient {
void draw() const override { std::cout << "치즈\n"; }
};
struct Tomato : Ingredient {
void draw() const override { std::cout << "토마토\n"; }
};
struct Factory {
virtual ~Factory() = default;
virtual std::unique_ptr<Ingredient> create(const std::string& name) const = 0;
};
struct PizzaFactory : Factory {
std::unique_ptr<Ingredient> create(const std::string& name) const override {
if (name == "dough") return std::make_unique<Dough>();
if (name == "cheese") return std::make_unique<Cheese>();
if (name == "tomato") return std::make_unique<Tomato>();
return nullptr;
}
};
int main() {
std::unique_ptr<Factory> factory = std::make_unique<PizzaFactory>();
factory->create("cheese")->draw();
factory->create("tomato")->draw();
}클래스가 여섯이다 — 물건 인터페이스 1 + 물건 3 + 공장 인터페이스 1 + 공장 1.
람다판
Factory의 메서드는 create 하나다. 공장 쪽 두 클래스가 접힌다.
#include <functional>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <unordered_map>
struct Ingredient {
virtual ~Ingredient() = default;
virtual void draw() const = 0;
};
struct Dough : Ingredient {
void draw() const override { std::cout << "반죽\n"; }
};
struct Cheese : Ingredient {
void draw() const override { std::cout << "치즈\n"; }
};
struct Tomato : Ingredient {
void draw() const override { std::cout << "토마토\n"; }
};
// 공장은 이제 타입이 아니라 함수다
using Factory = std::function<std::unique_ptr<Ingredient>(const std::string&)>;
using Creator = std::function<std::unique_ptr<Ingredient>()>;
Factory makePizzaFactory() {
// 이름 → 만드는 법. if 사슬이 표로 바뀐다
auto creators = std::make_shared<std::unordered_map<std::string, Creator>>(
std::unordered_map<std::string, Creator>{
{"dough", [] { return std::make_unique<Dough>(); }},
{"cheese", [] { return std::make_unique<Cheese>(); }},
{"tomato", [] { return std::make_unique<Tomato>(); }},
});
return [creators](const std::string& name) -> std::unique_ptr<Ingredient> {
auto it = creators->find(name);
if (it == creators->end()) throw std::runtime_error("unknown: " + name);
return it->second();
};
}
int main() {
Factory factory = makePizzaFactory();
factory("cheese")->draw();
factory("tomato")->draw();
// 공장을 통째로 바꿔 끼우는 것도 값 대입 한 줄이다
factory = [](const std::string&) -> std::unique_ptr<Ingredient> {
return std::make_unique<Dough>(); // 뭘 달라 하든 반죽만 주는 공장
};
factory("cheese")->draw();
}클래스가 넷으로 줄었다 — 물건 인터페이스 1 + 물건 3. 공장 두 개가 사라졌다.
곁들여 두 가지가 좋아졌다. if 사슬이 이름→생성자 표가 되어 항목 추가가 한 줄이 됐고,
공장을 바꿔 끼우는 일이 값 대입 한 줄이 됐다(테스트에서 가짜 공장을 넣기 쉽다).
무엇이 안 접혔나
Ingredient 계층은 그대로 넷이다. 접힐 수가 없다.
Ingredient가 메서드 하나(draw)뿐이라 얼핏 접힐 것 같지만, 만들어진 물건은 저장되고 돌아다닌다.std::function<void()>로 바꾸면 “그릴 수는 있으나 무엇인지는 알 수 없는 것”이 되어, 나중에 이름을 묻거나 종류로 갈라 볼 수 없다.- 물건에 메서드가 하나만 있는 경우는 드물다. 실제 재료라면
draw말고도 무게·가격·알레르기 정보가 붙는다. 메서드가 둘만 돼도 접기 조건을 위반한다.
즉 이 패턴에서 접히는 것은 “만드는 방법”이지 “만들어진 것”이 아니다. Command·Strategy가 통째로 접혔던 것과 갈리는 지점이 여기다.
무엇이 달라졌나
| 클래스판 | 람다판 | |
|---|---|---|
| 공장 쪽 클래스 | 2 | 0 |
| 물건 쪽 클래스 | 4 | 4 |
| 항목 추가 | if 한 줄 추가 | 표에 한 줄 |
| 공장 교체 | 새 클래스 | 값 대입 |
언제 접고 언제 접지 않나
접는다 — 공장 쪽은 거의 항상. 생성 함수 하나짜리 인터페이스에 클래스를 둘 만들 이유가 적다.
접지 않는다
- 공장이 여러 종류를 만들어야 할 때. GoF의 Abstract Factory는 원래
createButton()·createWindow()처럼 관련된 것들을 한 벌로 만드는 패턴이다. 그러면 메서드가 여럿이라 접기 조건을 위반한다. (위 예제는 이름을 받는 단일 생성 함수라 접힌 것이다.) - 공장이 상태를 많이 들고 있을 때.
- 만들어지는 물건 쪽은 접지 않는다. 위에서 본 그대로다.
생성 표를
std::shared_ptr로 감싼 이유가 있다. 람다가 표를 복사 캡처하면 공장을 복사할 때마다 표가 통째로 복사된다. 참조 캡처하면 지역 변수가 죽어 매달린 참조가 된다. 공유 소유가 이 자리에서는 가장 단순한 답이다.