[모던 C++로 다시 쓰는 GoF 디자인 패턴 총정리 #31] 추상 팩토리(Abstract Factory) 패턴: Concepts와 람다로 객체 패밀리 생성 유연하게 하기

이전 글에서는 전략(Strategy) 패턴을 모던 C++ 관점에서 재해석하며, 상속 없이도 람다와 Concepts, std::expected, coroutine, Ranges, std::format 등을 활용해 알고리즘(전략)을 런타임에 교체 가능하게 하고, 비동기 처리나 로깅, 조건부 선택 등 다양한 상황에 쉽게 대응할 수 있음을 확인했습니다. 이번에는 Creational(생성) 패턴 중 하나인 추상 팩토리(Abstract Factory) 패턴을 다룹니다.

추상 팩토리 패턴은 관련된 여러 객체(제품)들을 묶어 "제품군(family)"을 생성하는 인터페이스를 제공하는 패턴입니다. 전통적으로는 AbstractFactory 인터페이스와 구체 팩토리 클래스를 상속으로 정의하고, 각 제품(객체) 생성 메서드를 오버라이드하여 특정 패밀리 객체를 생성했습니다. 그러나 이는 새로운 제품군이나 조건부 생성 로직 추가 시 클래스 증가, 유지보수 어려움을 초래합니다.

C++20 이상에서는 Concepts로 추상 팩토리 요구사항을 명시하고, 람다와 함수 합성으로 제품 생성 로직을 상속 없이 구현할 수 있습니다. std::expected로 에러 처리 명확화, std::format으로 로깅, coroutine으로 비동기 객체 생성, Ranges로 여러 제품군 조합 등 다양한 요구사항에도 쉽게 대응 가능하며, 전통적 구현 대비 유지보수성과 확장성이 크게 향상됩니다.

패턴 소개: 추상 팩토리(Abstract Factory)

의도:

  • 관련된 제품 객체를 하나의 팩토리 인터페이스로 묶어, 특정 제품군을 생성하는 추상 팩토리를 정의.
  • 구체 팩토리는 이 인터페이스를 구현해 특정 제품군을 생성.
  • 예: GUI 라이브러리에서 Windows용 위젯 패밀리, Linux용 위젯 패밀리 생성.

전통적 구현 문제점:

  • AbstractFactory 인터페이스 + ConcreteFactory 상속 구조
  • 새로운 제품군 추가 시 클래스 증가
  • 조건부 생성, 비동기 생성, 에러 처리, 로깅 등 기능 추가 시 복잡성 증가

기존 C++ 스타일 구현 (전통적 방식)

예를 들어, GUI 요소(Window, ScrollBar)를 생성하는 추상 팩토리 예:

#include <iostream>
#include <memory>

struct Window {
    virtual ~Window()=default;
    virtual void draw()=0;
};

struct LinuxWindow : Window {
    void draw() override { std::cout<<"Linux Window\n"; }
};

struct WinWindow : Window {
    void draw() override { std::cout<<"Windows Window\n"; }
};

struct ScrollBar {
    virtual ~ScrollBar()=default;
    virtual void scroll()=0;
};

struct LinuxScrollBar : ScrollBar {
    void scroll() override { std::cout<<"Linux ScrollBar\n"; }
};

struct WinScrollBar : ScrollBar {
    void scroll() override { std::cout<<"Windows ScrollBar\n"; }
};

struct GUIFactory {
    virtual ~GUIFactory()=default;
    virtual std::unique_ptr<Window> createWindow()=0;
    virtual std::unique_ptr<ScrollBar> createScrollBar()=0;
};

struct LinuxFactory : GUIFactory {
    std::unique_ptr<Window> createWindow() override { return std::make_unique<LinuxWindow>(); }
    std::unique_ptr<ScrollBar> createScrollBar() override { return std::make_unique<LinuxScrollBar>(); }
};

struct WinFactory : GUIFactory {
    std::unique_ptr<Window> createWindow() override { return std::make_unique<WinWindow>(); }
    std::unique_ptr<ScrollBar> createScrollBar() override { return std::make_unique<WinScrollBar>(); }
};

int main() {
    std::unique_ptr<GUIFactory> factory = std::make_unique<LinuxFactory>();
    auto w = factory->createWindow();
    auto s = factory->createScrollBar();
    w->draw();
    s->scroll();
}

문제점:

  • GUIFactory 인터페이스, LinuxFactory, WinFactory 클래스 필요
  • 제품군 추가나 조건부 생성, 비동기 처리, 로깅 구현 시 클래스 증가 및 복잡성 상승

모던 C++20 이상 개선: Concepts, 람다, std::expected

1. Concepts로 팩토리 요구사항 정의

"팩토리는 createWindow(), createScrollBar() 메서드를 제공" 가정, std::expected 반환 형태:

#include <expected>
#include <string>

struct Window2 { std::string type; };
struct ScrollBar2 { std::string type; };

template<typename F>
concept GUIFactoryConcept = requires(F& f) {
    { f.createWindow() } -> std::convertible_to<std::expected<Window2,std::string>>;
    { f.createScrollBar() } -> std::convertible_to<std::expected<ScrollBar2,std::string>>;
};

2. 람다로 제품 생성 로직 정의

auto createLinuxWindow = []()->std::expected<Window2,std::string> {
    std::cout<<"Linux Window\n";
    return Window2{"LinuxWindow"};
};

auto createLinuxScrollBar = []()->std::expected<ScrollBar2,std::string> {
    std::cout<<"Linux ScrollBar\n";
    return ScrollBar2{"LinuxScrollBar"};
};

3. 팩토리를 함수 합성으로 구현

팩토리를 별도 클래스 없이 람다로 구성:

auto makeGUIFactory = [](auto windowCreator, auto scrollBarCreator) {
    return [=]() {
        struct Factory {
            decltype(windowCreator) wc;
            decltype(scrollBarCreator) sc;
            std::expected<Window2,std::string> createWindow() {return wc();}
            std::expected<ScrollBar2,std::string> createScrollBar() {return sc();}
        };
        return Factory{windowCreator, scrollBarCreator};
    }();
};

auto linuxFactory = makeGUIFactory(createLinuxWindow, createLinuxScrollBar);
static_assert(GUIFactoryConcept<decltype(linuxFactory)>);

사용 예:

int main() {
    auto w = linuxFactory.createWindow();
    if(!w) std::cerr<<"Error:"<<w.error()<<"\n";
    else std::cout<<w->type<<" created.\n";

    auto s = linuxFactory.createScrollBar();
    if(!s) std::cerr<<"Error:"<<s.error()<<"\n";
    else std::cout<<s->type<<" created.\n";
}

출력:

Linux Window
LinuxWindow created.
Linux ScrollBar
LinuxScrollBar created.

비교:

  • 전통적: LinuxFactory 클래스 필요
  • C++20: 람다와 Concepts로 팩토리 구현, 상속 없이 타입 안전한 인터페이스

조건부 생성, 비동기, 로깅, Ranges 적용

조건부 생성(예: OS 환경에 따라 제품 달리 생성):

auto conditionalWindowCreator = [](bool isLinux) {
    return [=]() -> std::expected<Window2,std::string> {
        if(isLinux) {std::cout<<"Linux Window\n"; return Window2{"LinuxWindow"}; }
        else {std::cout<<"Win Window\n"; return Window2{"WinWindow"}; }
    };
};

auto conditionalScrollBarCreator = [](bool isLinux) {
    return [=]() -> std::expected<ScrollBar2,std::string> {
        if(isLinux) {std::cout<<"Linux ScrollBar\n"; return ScrollBar2{"LinuxScrollBar"};}
        else {std::cout<<"Win ScrollBar\n"; return ScrollBar2{"WinScrollBar"};}
    };
};

auto conditionalFactory = makeGUIFactory(conditionalWindowCreator(true), conditionalScrollBarCreator(true));

로깅(std::format):

auto loggingCreator = [&](auto baseCreator, std::string productName) {
    return [=]() -> std::expected<decltype(baseCreator().value()),std::string> {
        std::cout << std::format("[LOG] Creating {}\n", productName);
        auto res = baseCreator();
        if(!res) std::cout << std::format("[LOG] Error: {}\n", res.error());
        else std::cout << "[LOG] Success.\n";
        return res;
    };
};

auto loggedLinuxFactory = makeGUIFactory(
    loggingCreator(createLinuxWindow,"Window"),
    loggingCreator(createLinuxScrollBar,"ScrollBar")
);

비동기 생성: coroutine으로 제품 생성을 비동기 I/O 기반으로 구현 가능.

Ranges: 여러 팩토리 리스트에서 제품 생성 파이프라인 처리 가능.

#include <vector>
#include <ranges>

std::vector<decltype(linuxFactory)> factories = {linuxFactory, loggedLinuxFactory};
for (auto wres : factories | std::views::transform([](auto& f){return f.createWindow();})) {
    if(!wres) std::cerr<<"Window creation error: "<<wres.error()<<"\n";
}

비교:

  • 전통적: 조건부 생성, 비동기, 로깅 추가 시 ConcreteFactory 증가
  • C++20: 람다 합성으로 기능 추가, coroutine으로 비동기 처리, Ranges로 리스트 파이프라인 처리, std::expected로 에러 명확화

전통적 구현 vs C++11/14/17 vs C++20 이상 비교

전통적(C++98/03):

  • AbstractFactory 인터페이스 + ConcreteFactory 상속 구조
  • 새로운 제품군 추가 시 클래스 증가
  • 조건부 생성, 비동기 처리, 로깅, 에러 처리 등 구현 시 복잡성 증가

C++11/14/17:

  • 람다, std::function 활용해 일부 단순화 가능
  • Concepts, std::expected, coroutine, Ranges 미지원 → 정적 타입 제약, 비동기/에러 처리 표현 한계

C++20 이상(모던 C++):

  • Concepts로 AbstractFactory 요구사항 정의, 상속 없이 타입 안전한 인터페이스
  • 람다 합성과 std::expected로 에러 처리, std::format으로 로깅, coroutine으로 비동기 처리, Ranges로 제품군 리스트 처리
  • 클래스 증가 없이 확장 가능, 유지보수성, 확장성, 타입 안전성 대폭 향상

결론

추상 팩토리(Abstract Factory) 패턴은 관련 제품군을 생성하는 인터페이스를 제공하는 핵심 패턴이지만, 전통적 구현은 AbstractFactory 인터페이스와 구체 팩토리 클래스로 인한 클래스 증가, 유지보수 어려움을 안고 있었습니다.

C++20 이상에서는 Concepts, 람다, std::function, std::expected, std::format, coroutine, Ranges 등을 활용해 상속 없이도 다양한 제품군을 쉽게 생성할 수 있는 팩토리를 구성할 수 있으며, 조건부 생성, 비동기 처리, 로깅, 에러 처리 등 다양한 요구사항에도 적은 코드로 대응 가능합니다. 전통적 구현 대비 유지보수성과 확장성이 크게 개선됩니다.

반응형