C++/디자인 패턴

디자인 패턴 - 팩토리 메서드 패턴, 추상 팩토리 패턴

오의현 2024. 4. 2. 15:53

객체지향 디자인 패턴에는 팩토리 메서드 패턴과 추상 팩토리 패턴이 있다.
두 패턴에 대해 알아보기 전에 먼저 심플 팩토리에 대해 알아보자.

 

심플 팩토리는 두 디자인 패턴의 기본이 되는 방식이다.

객체지향 원칙을 다소 위배하는 부분이 있어, 디자인 패턴으로 분류되지는 않고 일종의 관용구라고 생각하면 좋다.

그리고, 이 패턴의 단점을 해결한 패턴이 팩토리 메서드 패턴과 추상 팩토리 패턴인 것이다.

 

심플 팩토리란?

먼저, 이 패턴이 왜 나왔는지에 대해 이해해보자.

 

음료수를 마시고 싶은 손님과 이를 판매하는 상황을 예로 들어보겠다.

소비자는 Client클래스이며, 음료수는 GrapeBeverage, AppleBeverage 두 객체가 있다고 해보자.

 

일반적으로는 이렇게 음료수 객체를 생성할 것이다.

class Client
{
    void BuyBeverage()
    {
    	GrapeBeverage* GrapeBG = new GrapeBeverage();
    	AppleBeverage* AppleBG = new AppleBeverage();
    }
};

 

이렇게, new를 통해 객체를 직접적으로 생성하게 되면 객체지향적인 부분에서 문제가 생긴다.

Client 객체와 음료수 객체 간의 결합도가 높아진다는 것이다.


지금이야 코드가 간단하니 괜찮아 보여도 코드 길이가 길어진다고 가정해보면, GrapeBeverage, AppleBeverage 객체를 수정하게 될 때 그에 따라서 Client의 코드를 수정해야 하는 상황도 심심찮게 발생할 것이다.

 

이러한 결합도, 의존성을 제거하기 위해 만들어진 것이 심플 팩토리 패턴이다.

 

그렇다면 심플 팩토리 패턴은 어떤 방식으로 설계해야 할까?

class Beverage
{
public:
    virtual void Drink() = 0;
};

class GrapeBeverage : public Beverage
{
public:
    void Drink()
    {
        std::cout << "포도 주스를 마셨다." << std::endl;
    }
};

class AppleBeverage : public Beverage
{
public:
    void Drink()
    {
        std::cout << "사과 주스를 마셨다." << std::endl;
    }
};

 

먼저 이렇게 음료수를 상속 구조로 바꿔보자.

GrapeBeverage와 AppleBeverage는 모두 Beverage 클래스를 상속받고 있다.

 

이번엔, 이 음료수를 생성해주는 클래스를 만들어 볼 것이다.

class BeverageFactory
{
public:
    enum class BeverageType
    {
        Grape,
        Apple,
    };

    Beverage* CreateBeverage(BeverageType _InputType)
    {
        Beverage* ReturnBeverage = nullptr;

        switch (_InputType)
        {
        case BeverageType::Grape:
            ReturnBeverage = new GrapeBeverage();
            break;
        case BeverageType::Apple:
            ReturnBeverage = new AppleBeverage();
            break;
        }

        return ReturnBeverage;
    }
};

 

이렇게 enum타임만 입력받으면 내부에서 음료수는 만들어서 부모 클래스 타입으로 반환해주는 클래스를 만들게 된다면

Client클래스에선 BeverageFactory 클래스 하나만 알아도 모든 종류의 음료수를 다 만들 수 있는 것이다.

 

이제 위의 클라이언트 코드를 수정해보면

class Client
{
public:
    void BuyBeverage()
    {
        BeverageFactory* BGFactory = new BeverageFactory();

        Beverage* GrapeBG = BGFactory->CreateBeverage(BeverageFactory::BeverageType::Grape);
        Beverage* AppleBG = BGFactory->CreateBeverage(BeverageFactory::BeverageType::Apple);

        GrapeBG->Drink();
        AppleBG->Drink();
    }
};

 

Beverage와 BeverageFactory클래스만 알고 있다면,

음료수의 종류가 100개가 되든 200개가 되든 직접적인 결합 없이 생성할 수 있게 되는 것이다.

 

이 것이 심플 팩토리 패턴이다.

객체를 직접 알지 않아도 생성할 수 있도록 하는 것이다.

 

다만, 이 심플 팩토리 패턴의 경우엔 위에서 말했듯이 객체지향의 원칙을 위반하는 것이 있다.

개방-폐쇠 원칙에 위배된다.

 

왜냐면, 음료수를 추가할 때마다 CreateBeverage 내부의 함수를 고쳐야 한다.

물론 switch 분기 하나 추가하는 정도긴 하지만, 객체지향 원칙을 제대로 지키려면 개방에는 열려있어야 하지만 수정에는 닫혀있어야 한다.

 

이 문제를 커버하기 위해 나온 패턴이 팩토리 메서드 패턴과 추상 팩토리 패턴이다.

 

 

 

팩토리 메서드 패턴


팩토리 메서드 패턴은 심플 팩토리 패턴을 기반으로 설계된다.

먼저 어떻게 설계되는지 보자.

class BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() = 0;
};

 

BeverageFactory를 위와 같이 추상 클래스로 선언해준다.

 

class GrapeBeverageFactory : public BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() override
    {
        Beverage* ReturnBeverage = new GrapeBeverage();
        return ReturnBeverage;
    }
};

class AppleBeverageFactory : public BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() override
    {
        Beverage* ReturnBeverage = new AppleBeverage();
        return ReturnBeverage;
    }
};

 

이후, 이렇게 BeverageFactory를 상속받은 클래스를 각각 만들고, 각 클래스가 하나의 음료만 담당하도록 하자.

Client에선 아래와 같이 사용한다.

class Client
{
public:
    void BuyBeverage()
    {
        BeverageFactory* GrapeBGFactory = new GrapeBeverageFactory();
        BeverageFactory* AppleBGFactory = new AppleBeverageFactory();

        Beverage* GrapeBG = GrapeBGFactory->CreateBeverage();
        Beverage* AppleBG = AppleBGFactory->CreateBeverage();

        GrapeBG->Drink();
        AppleBG->Drink();
    }
};

 

이런 식으로 각 음료당 하나의 클래스를 담당하여 Factory클래스를 구성하게 되면, 음료룰 추가할 때 객체의 코드를 구성할 필요가 없고 새로운 클래스를 하나 더 추가하면 된다.

 

즉 개방-폐쇄 원칙을 위반하지 않는 셈이 된다.

 

다만, 이 패턴의 단점은 음료의 수가 많아질수록 관리해야 하는 클래스가 점점 많아진다는 것이다.

그런 단점에도 불구하고 의존성 분리에 있어 효과적인 디자인 패턴이기 때문에 의외로 많은 곳에서 접할 수 있는 패턴이다.

 

 

 

추상 팩토리 패턴


추상 팩토리 패턴은 팩토리 메서드 패턴과 거의 유사한 패턴이다.

 

이번엔 음료수를 하나 사면 경품도 추가로 준다고 가정해보자.

포도 주스를 구매하면 포도 빵을 주고 사과 주스를 사면 사과 빵 준다고 가정해보겠다.

 

class Bread
{
    virtual void Eat() = 0;
};

class GrapeBread : public Bread
{
    virtual void Eat() override
    {
        std::cout << "포도 빵을 먹었다" << std::endl;
    }
};

class AppleBread : public Bread
{
    virtual void Eat() override
    {
        std::cout << "사과 빵을 먹었다" << std::endl;
    }
};

 

먼저 이렇게 빵을 선언해주자.

 

이후, BeverageFactory 클래스에 순수가상함수를 하나 추가해주겠다.

class BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() = 0;
    virtual Bread* CreatePrize() = 0;
};

 

 

이후 이 클래스를 상속받은 하위 클래스들이 구현해주면 끝이다.

class GrapeBeverageFactory : public BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() override
    {
        Beverage* ReturnBeverage = new GrapeBeverage();
        return ReturnBeverage;
    }

    virtual Bread* CreatePrize()
    {
        Bread* ReturnBread = new GrapeBread();
        return ReturnBread;
    }
};

class AppleBeverageFactory : public BeverageFactory
{
public:
    virtual Beverage* CreateBeverage() override
    {
        Beverage* ReturnBeverage = new AppleBeverage();
        return ReturnBeverage;
    }

    virtual Bread* CreatePrize()
    {
        Bread* ReturnBread = new AppleBread();
        return ReturnBread;
    }
};

 

언뜻 보기엔 팩토리 메서드 패턴과 대체 뭐가 다르지? 싶다.

다른 점은 하나이다. 연관된 객체들을 함께 관리할 수 있다는 것이다.

 

class Client
{
public:
    void BuyBeverage()
    {
        BeverageFactory* GrapeBGFactory = new GrapeBeverageFactory();
        Beverage* GrapeBG = GrapeBGFactory->CreateBeverage();
        Bread* GrapeBread = GrapeBGFactory->CreatePrize();
        
        GrapeBG->Drink();
        GrapeBread->Eat();

        BeverageFactory* AppleBGFactory = new AppleBeverageFactory();
        Beverage* AppleBG = AppleBGFactory->CreateBeverage();
        Bread* AppleBread = AppleBGFactory->CreatePrize();

        AppleBG->Drink();
        AppleBread->Eat();
    }
};

 

위와 같이, 하나의 팩토리가 가진 멤버함수를 사용하여 연관된 객체를 고민없이 생성할 수 있다.

 

예를 들어 캐릭터가 여러 종류 있고, 캐릭터 별로 따라다니는 펫의 종류가 다르다고 한다면

현재 캐릭터와 매칭되는 펫이 무엇인지 고민할 필요 없이 팩토리에서 펫을 만들어주는 함수만 호출하면 되는 것이다.

 

팩토리 메서드 패턴에서 더 캡슐화된 형식이라고 보면 될 것 같다.

 

여기까지 팩토리 메서드 패턴과 추상 팩토리 패턴에 대해 알아보았다.

다음 게시물에선 컴포넌트 패턴을 올려볼까 한다.