Outdated/Column

[Design Pattern] 팩토리 패턴(factory pattern)

해달 2020. 3. 24. 08:00

목차

  1. 생성자 다시보기

  2. 팩토리

  3. 정리하며

  4. 참고자료



1. 생성자 다시보기

생성자의 첫 번째 단점

생성자에게는 두 가지 단점이 있다. 첫 번째는 메소드 이름이 항상 타입과 같은 이름을 가져 이름에 추가적인 정보를 표시할 수가 없다는 것이다. 다음의 예시를 보자. 좌표점을 나타내기 위해 Point 클래스를 설계하였고, 직교 좌표계와 극 좌표계를 모두 지원하려고 한다.


class Point

{

    float x, y;

    float distance, radian;

 

public:

    // 직교 좌표계를 위한 생성자

    Point(float x, float y)

        : x{ x }, y{ y }

    {

        // ...

    }

 

    // 극 좌표계를 위한 생성자

    Point(float distance, float radian)

        : distance{ distance }, radian{ radian }

    {

        // ...

    }

};

 

Point p(3.5, 4.2) // ERROR : 어떤 생성자를 호출할 것인가?

컴파일러가 모호하다고 징징대는 것을 볼 수 있다.


위의 문제는 무엇이었는가? 바로 함수의 서명이 같다는 것이다. 그렇다고 추가적인 매개변수를 이용하거나 상속을 이용하는 것은 무언가 우아해 보이지는 않는다.


생성자의 두 번째 단점

생성자의 문제점 두 번째는 무엇일까? 생성자는 반환값이 없기 때문에 객체 생성을 실패했을 때 이를 알려줄 방법이 예외 뿐이라는 것이다. 아래의 예제를 보자.


class A

{

    int* container = new int[10];

public:

    virtual ~A()

    {

        delete[] container;

        container = nullptr;

    }

};

 

class B : public A

{

    A* obj = new A;

public:

    B()

    {

        throw std::exception("Error!");

    }

 

    // 생성자에서 예외를 발생시켰다면

    // 객체 생성이 실패했으므로

    // 당연히 소멸자가 불러지지 않는다.

    ~B()

    {

        delete obj;

        obj = nullptr;

    }

};

아무 이상 없는 듯 보이지만..?


B의 생성자에서 예외가 발생하게 되어, B의 멤버 변수들을 정리할 때 메모리 누수가 발생하게 된다. 팩토리 패턴은 생성자의 이러한 단점을 보완한 디자인 패턴이다.

2. 팩토리[각주:1]

팩토리 메소드

자 생성자의 문제점을 다시 한 번 상기해보자. 메소드의 이름이 타입의 이름과 같아서 호출이 모호할 수 있다는 것과 반환 값이 없어 객체 생성 실패 시 예외를 던지는 외에는 달리 방법이 없다는 것이다. 이에 대한 간단한 해결책은 무엇일까? 객체를 만드는 함수를 만드는 것이다.


팩토리 메소드(factory method) 혹은 가상 생성자(virtual constructor)라고도 불리는 이 함수는 객체를 대신 생성하여 전달한다. [각주:2]생성자보다 좀 더 명확한 명명이 가능하고, 반환값을 이용해 자연스러운 객체 실패도 가능하다. 위의 Point 예제를 팩토리 메소드를 사용하여 바꿔보자.


class Point

{

    float x, y;

 

protected:

    Point(float x, float y)

        : x { x }, y { y } { }

 

public:

    static Point CreateCartesian(float x, float y)

    {

        return { x, y };

    }

 

    static Point CreatePolar(float distance, float radian)

    {

        return { distance * cos(radian), distance * sin(radian) };

    }

};

이제는 무엇을 생성할 지 명확하다.


내부 팩토리

좀 더 분명히 하기 위해 따로 클래스로 모아둘 수도 있다. 이를 팩토리(factory)라고 한다.


class Point

{

    float x, y;

 

protected:

    Point(float x, float y)

        : x { x }, y { y } { }

 

public:

    class Factory

    {

        static Point CreateCartesian(float x, float y)

        {

            return { x, y };

        }

 

        static Point CreatePolar(float distance, float radian)

        {

            return { distance * cos(radian), distance * sin(radian) };

        }

    }

};

 

Point p = Point::Factory::CreateCartesian(3.4, 4.5);

내부 팩토리


외부 팩토리

클래스 내부에 팩토리를 만들어 놓으면 API 사용성이 좋아진다. 만일 여러 타입을 이용해야 한다면 어떻게 해야 할까? 이때는 클래스 내부에 만들기 보다는 클래스 외부에 만드는 것이 구현에 있어 깔끔할 것이다.


class A; class B; class C;

 

class D

{

    friend class Factory;

 

    A a;

    B b;

    C c;

 

protected:

    D(A a, B b, C c);

};

 

class Factory

{

public:

    static D CreateD(A a, B b, C c)

    {

        return { a, b, c };

    }

};


팩토리에서는 클래스 내부에 있는 생성자를 접근해줘야 하기 때문에 friend 선언이 불가피하다. 이러한 외부 팩토리를 매개변수 기반 팩토리 메소드(parametrized factory method)라고도 한다.


추상 팩토리

추상 팩토리는 여러 타입의 군(family)을 생성할 때 사용한다. 어떤 타입을 만들것인지 지정하지 않아도 연관된 혹은 독립된 객체들의 군을 생성할 수 있는 인터페이스를 가지고 있다. 복잡한 시스템에서 유용하다. 아래 음료를 만드는 예제를 보자.


class Beverage

{

public:

    virtual void Prepare() = 0;

};

 

// 아래 두 클래스는 Prepare()을 적절히 재정의 했다고 가정한다.

class Coffee : public Beverage { };

class Tea : public Beverage { };

 

// 추상 팩토리

// 실제로 사용하려면 구체 팩토리가 있어야 한다.

class BeverageFactory

{

public:

    std::unique_ptr<Beverage> Make() const = 0;

};

 

// 구체 팩토리

class CoffeeFactory : public BeverageFactory { };

class TeaFactory : public BeverageFactory { };

 

// 추상 팩토리 사용례

class Store

{

    std::unordered_map<std::string, std::unique_ptr<BeverageFactory>> beverageFactories;

public:

    Store()

    {

        beverageFactories["Coffee"] = std::make_unique<CoffeeFactory>();

        beverageFactories["Tea"] = std::make_unique<TeaFactory>();

    }

 

    std::unique_ptr<Beverage> GetDrink(const std::string& kind)

    {

        return beverageFactories[kind]->Make();

    }

};

추상 팩토리


추상 팩토리를 사용할 때 주의할 점은 구체 팩토리를 만들어야 사용할 수 있다는 것이다. 실제로 사용하는 케이스는 많지 않다고 한다.


함수형 팩토리

팩토리 메소드는 함수형으로도 만들 수 있다. 호출 객체(callable object)를 이용하면 된다.


class Store

{

    std::unordered_map<std::string, std::unique_ptr<BeverageFactory>> beverageFactories;

public:

    Store()

    {

        beverageFactories["Coffee"] = [] { return std::make_unique<Coffee>(); };

        beverageFactories["Tea"] = [] { return std::make_unique<Tea>(); }

    }

 

    std::unique_ptr<Beverage> GetDrink(const std::string& kind)

    {

        return beverageFactories[kind]->Make();

    }

};

람다 함수를 이용한 함수형 팩토리

3. 정리하며

팩토리는 생성자 대신에 객체 생성을 해주는 디자인 패턴이다. 어떤 구체적 클래스의 인스턴스가 생성되는지 캡슐화 해주며 언제, 누가, 어떻게 생성하는지 숨긴다. 팩토리 패턴은 아래와 같은 장단점이 있다.


장점

  • 객체 생성과 관련된 복잡한 과정을 추상화 할 수 있다.

  • 객체 생성과 관련된 다양한 최적화와 필요한 제한을 할 수 있다.

    • e.g. 싱글톤 패턴, 객체 생성 풀

  • 명명을 통해 가독성이 향상된다.

  • 한 종류의 객체가 아니라 인자에 따라 여러 종류의 객체 중 하나를 생성할 수 있다.

  • 팩토리의 응집성이 매우 높다.

  • 사용하는 제품군의 변경을 용이하게 해준다.


단점

  • 팩토리와 대상과의 결합도가 높아진다.

  • 새로운 종류의 객체를 생성해야 할 때, 기존의 코드를 수정해야 할 수 있다.

  • 추상 팩토리에서 제품군에 새 종류의 제품을 추가하는 것이 어렵다.

4. 참고자료


  1. 관련 리팩토링 패턴으로 Replace Constructors with Creation Method와 Move Creation Knowledge to Factory, Encapsulate Classes with Factory가 있다. [본문으로]
  2. 디자인 패턴 중 생성 패턴이라고 한다. 생성 패턴은 어떤 구체적 클래스의 인스턴스가 생성되는지 캡슐화해주며 언제, 누가, 어떻게 생성하는지 숨긴다. [본문으로]