팩토리 패턴(Factory Pattern)

  • 팩토리 패턴은 생성 패턴 중에서도 가장 기본이 되는 패턴입니다.
  • C++에서는 클래스의 인스턴스를 생성하기 위해서 new 키워드를 사용합니다.
    • C++11 이상의 모던 C++(Mordern C++)에서는 객체의 생성으로 new 키워드를 사용하는 것을 추천하지 않습니다.
    • shared_ptr이나 unique_ptr과 같은 스마트 포인터(Smart Pointer)를 사용하길 권고합니다.
  • 일반적인 객체를 생성하는 방법은 다음과 같습니다.
class Sample
{
public:
    void hello()
    {
        std::cout << "Hello world" << std::endl;
    }
};

int main()
{
    Sample sample = new Sample();
    std::shared_ptr<Sample> sample = std::make_shared<Sample>(); // 스마트 포인터

    return 0;
}
  • 이렇게 객체를 생성하는 키워드 new나 C++ STL 스마트 포인터(std::make_shared<T>)를 사용하여 객체를 직접 생성하면 객체의 의존성이 발생하는 문제가 있습니다. (결합 관계가 발생하였다고도 합니다.)
  • 강력한 결합 관계가 되면 클래스 이름의 수정과 같이 변경사항이 발생할 경우 모든 코드를 찾아 수정해야 합니다.
  • 팩토리 패턴은 이러한 결합 관계를 최소화하려는 목적이 있으며, 객체의 생성을 간접적으로 수행합니다.

팩토리 패턴의 의미

  • 팩토리 패턴은 객체 생성을 별도의 클래스로 분리하여 생성을 위임합니다.
  • 즉 객체를 생성하고 캡슐화하여 위임하는 것을 의미합니다.

팩토리 패턴: 생성할 객체의 인터페이스 클래스 선언

  • C++에는 가상 함수(Virtual Function)를 사용하면 많은 이점이 있습니다.
  • 팩토리 패턴에도 가상 함수를 사용합니다.
// 생성할 객체의 타입
enum class LanguageType
{
    Korean = 0,
    English
    // ... 생성할 객체가 더 있다면 여기에 추가
} Language;

// 생성할 객체들의 인터페이스 클래스
class ILanguage
{
public:
    virtual void text() = 0; // 가상 함수
};
  • 팩토리로 생성할 객체들의 타입을 구분할 Language가 필요합니다.
  • 객체의 타입을 구분하지 않더라도, 공통된 인터페이스를 갖는 인터페이스 클래스 ILanguage가 필요합니다.

팩토리 패턴: 객체 생성 클래스 구현

  • 객체 생성의 역할을 수행할 클래스를 하나 만듭니다.
class Factory final
{
public:
    static std::shared_ptr<ILanguage> getInstance(const LanguageType& type)
    {
        // 객체의 타입을 알아야 하는 단점이 있다.
        if (type == LanguageType::Korean)
        {
            return std::make_shared<Korean>();
        }
        else if (type == LanguageType::English)
        {
            return std::make_shared<English>();
        }
    }
};
  • 객체를 생성할 팩토리는 반드시 생성할 객체의 타입을 알아야 합니다.
  • 객체의 타입을 모른다면 어떤 객체를 만들어야 할지 모르기 때문입니다.

팩토리 패턴: 실제 임무를 수행할 객체 클래스 구현

  • 인터페이스 클래스를 상속받아, 실제로 사용할 클래스를 구현합니다.
  • C++ 문법적으로 상속(Inheritance)이지만 관용적 표현으로 인터페이스를 구현한다.라는 표현을 사용합니다.
    • 인터페이스 클래스는 추상화된 클래스로 구현체가 없기 때문입니다.
    • 실제로 인터페이스 클래스를 상속하는 자식 클래스가 해당 인터페이스를 실제로 구현하기 때문입니다.
class English final :
    public ILanguage
{
    void text() override
    {
        std::cout << "Hello, English class" << std::endl;
    }
};

class Korean final :
    public ILanguage
{
    void text() override
    {
        std::cout << "Hello, Korean class" << std::endl;
    }
};
  • English 클래스와 Korean 클래스는 모두 ILanguage 인터페이스의 text() 가상 함수를 오버로딩하여 구현하고 있습니다.

팩토리 패턴: 객체의 생성을 호출할 클래스 구현

  • 팩토리를 사용하여 객체들을 생성할 클래스를 구현해봅니다.
class Hello final
{
public:
    void greeting(const LanguageType& type)
    {
        // 인터페이스 클래스로 받으면 타입에 관계 없이 하나로 받을수 있다.
        std::shared_ptr<ILanguage> language = Factory::getInstance(type);
        language->text();
    }
};
  • 한 가지 중요한 점은 팩토리 클래스의 getInstance()의 반환 값은 std::make_shared<Korean>() 또는 std::make_shared<English>()입니다.
  • 하지만 이들의 추상체인 ILanguage 클래스로도 KoreanEnglish를 받을 수 있으며, 이들의 공통 메서드인 text()를 호출할 수 있습니다.
  • 타입이 ILanguage여도 실제로 반환한 KoreanEnglish 클래스의 text() 메서드가 실행됩니다.

팩토리 패턴: 실행 코드

int main(const int argc, const char* argv[])
{
    // 팩토리 패턴
    std::shared_ptr<Hello> hello = std::make_shared<Hello>();
    hello->greeting(LanguageType::Korean);
    hello->greeting(LanguageType::English);

    return 0;
}
  • 이처럼 팩토리 패턴은 생성과 관련된 모든 처리를 별도의 클래스로 위임합니다.

  • 팩토리 패턴의 장/단점은 다음과 같습니다.

  • 장점

    • 클래스의 유연성과 확장성이 개선된다.
    • 어떤 객체를 생성할지 모르는 초반에 유용하다.
  • 단점

    • 생성을 전담할 클래스가 생기므로, 관리해야하는 클래스가 늘어난다.
    • 팩토리가 실제로 생성할 객체들의 타입을 알아야 한다.
  • 객체의 타입을 알아야 하는 팩토리 패턴의 단점은 객체의 타입까지도 추상화한 팩토리 메서드 패턴으로 발전하게 됩니다.

심플 팩토리(Simple Factory)

  • 팩토리 패턴은 객체의 생성을 외부 클래스로 위임한다고 하였습니다.
  • 심플 팩토리는 객체의 생성을 외부 클래스가 아닌 내부적으로 해결하는 방법입니다.
class SimpleFactory final
{
public:
    void greeting()
    {
        std::shared_ptr<ILanguage> language = factory(LanguageType::Korean);
        language->text();
    }

    // 객체의 메서드로 객체 생성을 담당한다.
    static std::shared_ptr<ILanguage> factory(const LanguageType& type)
    {
        if (type == LanguageType::Korean)
        {
            return std::make_shared<Korean>();
        }
        else if (type == LanguageType::English)
        {
            return std::make_shared<English>();
        }
    }
};
  • 이처럼 객체의 생성이 내부 메서드로 이동하였습니다.
  • 하지만 실제로 생성할 객체의 타입을 알아야 한다는 단점은 변함 없습니다.