무엇인가

무엇을 만들지 고르는 일을 밖으로 빼낸다. 쓰는 쪽은 구상 타입을 모른 채 추상 타입만 받아 쓴다.

이 패턴은 부분적으로만 접힌다. 공장은 함수 하나라 접히지만, 만들어지는 물건 쪽 계층은 그대로 남는다. 그 구분이 이 문서의 요점이다.

구조

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가 통째로 접혔던 것과 갈리는 지점이 여기다.

무엇이 달라졌나

클래스판람다판
공장 쪽 클래스20
물건 쪽 클래스44
항목 추가if 한 줄 추가표에 한 줄
공장 교체새 클래스값 대입

언제 접고 언제 접지 않나

접는다 — 공장 쪽은 거의 항상. 생성 함수 하나짜리 인터페이스에 클래스를 둘 만들 이유가 적다.

접지 않는다

  • 공장이 여러 종류를 만들어야 할 때. GoF의 Abstract Factory는 원래 createButton()·createWindow()처럼 관련된 것들을 한 벌로 만드는 패턴이다. 그러면 메서드가 여럿이라 접기 조건을 위반한다. (위 예제는 이름을 받는 단일 생성 함수라 접힌 것이다.)
  • 공장이 상태를 많이 들고 있을 때.
  • 만들어지는 물건 쪽은 접지 않는다. 위에서 본 그대로다.

생성 표를 std::shared_ptr로 감싼 이유가 있다. 람다가 표를 복사 캡처하면 공장을 복사할 때마다 표가 통째로 복사된다. 참조 캡처하면 지역 변수가 죽어 매달린 참조가 된다. 공유 소유가 이 자리에서는 가장 단순한 답이다.