빌더 패턴(Builder Pattern)

  • 클래스의 인스턴스인 객체는 단일 객체와 복합 객체로 구분합니다.
  • 단일 객체는 하나의 클래스로 생성된 객체를 의미합니다.
    • 팩토리, 팩토리 메서드, 추상 팩토리 모두 단일 객체를 생성하는 패턴입니다.
  • 복합 객체는 하나의 객체가 다른 객체를 포함하는 관계(has-a)를 의미합니다.
    • 클래스를 확장하는 방법에는 상속하는 관계(is-a)도 있습니다.
    • 하지만 상속은 강력한 상-하 결합 관계와 불필요한 정보도 포함되는 단점이 있습니다.
    • 현대적인 객체지향 개발 방법은 상속을 최소화하고 복합 객체로 구성합니다.
    • (참고) 인터페이스 클래스를 상속하여 구현하는 것은 상-하 상속 관계가 아닌 선언과 구현으로 구분합니다.

빌더 패턴의 구조

  • 빌더 패턴은 다른 객체를 포함하는 객체를 생성하는 패턴입니다.
  • 빌더는 대상 객체에 포함되어 있는 객체를 생성하는 전략과 방법을 정의합니다.

빌더 패턴: 예제

  • 주변에 존재하는 대부분의 객체는 복합 객체로 추상화가 가능합니다.
  • 컴퓨터의 경우 컴퓨터 객체는 CPU, 메모리(RAM), 저장장치(Storage) 등 다양한 객체를 포함하는 관계(has-a)입니다.
  • 컴퓨터를 설계하는 예제로 빌더 패턴을 공부해보겠습니다.
class Computer
{
public:
    Computer()
    {
        m_ram.clear();
        m_storage.clear();
    }

    void setMemory(const Memory& memory)
    {
        Memory tmpMemory = memory;
        m_ram.push_back(tmpMemory);
    }

    void setStorage(const Storage& storage)
    {
        Storage tmpStorage = storage;
        m_storage.push_back(tmpStorage);
    }

    void setCpu(const std::string& cpu)
    {
        m_cpu = cpu;
    }

    void showInfo()
    {
        std::cout << toString() << std::endl;
    }

private:
    int getMemory()
    {
        int size = 0;
        for (size_t i = 0; i < m_ram.size(); i++)
        {
            size += m_ram[i].getSize();
        }

        return size;
    }

    int getStorage()
    {
        int size = 0;
        for (size_t i = 0; i < m_storage.size(); i++)
        {
            size += m_ram[i].getSize();
        }

        return size;
    }

    std::string toString()
    {
        std::string infoComputer = "[Computer] This computer's spec is...\nCPU: " +
            m_cpu + ",\nRAM: " +
            std::to_string(getMemory()) + "GB,\nStorage: " +
            std::to_string(getStorage()) + "GB.\n";

        return infoComputer;
    }

    std::string m_info;

    std::string m_cpu;
    std::vector<Memory> m_ram;
    std::vector<Storage> m_storage;
};
  • Computer 클래스는 CPU, Memory, Storage 객체를 포함하고 있습니다. 따라서 멤버 변수로 정의합니다.
  • private 접근 제어 지시자로 선언한 멤버 변수에 대한 접근을 위해 게터(Getter)와 세터(Setter)를 정의합니다.
  • Computer 클래스는 포함하고 있는 객체들의 정보를 표시하는 showInfo() 메서드를 갖고 있습니다.

빌더 패턴: Memory, Storage 클래스

  • Computer 클래스의 복합 객체 요소인 MemoryStorage 클래스를 정의합니다.
class Memory
{
public:
    Memory(const int& size)
    {
        m_size = size;
    }

    void setSize(const int& size)
    {
        m_size = size;
    }

    const int getSize()
    {
        return m_size;
    }

private:
    int m_size;
};

class Storage
{
public:
    Storage(const int& size)
    {
        m_size = size;
    }

    void setSize(const int& size)
    {
        m_size = size;
    }

    const int getSize()
    {
        return m_size;
    }

private:
    int m_size;
};

빌더 패턴: 인터페이스 클래스

  • 복합 객체를 생성하기 위해 세부적으로 복합 객체들을 생성할 인터페이스 클래스를 선언합니다.
  • 복합 객체들의 생성 방법이 다를 수 있어서 Algorithm 인터페이스 클래스를 포함합니다.
class IBuilder
{
public:
    IBuilder(std::shared_ptr<Algorithm> algorithm) :
        m_algorithm(algorithm)
    {
        // 빌더 인터페이스 클래스를 생성할 때 객체를 생성할 알고리즘 객체가 필요하다.
    }

    auto getInstance()
    {
        return m_algorithm->getInstance();
    }

    virtual Computer build() = 0;

protected:
    std::shared_ptr<Algorithm> m_algorithm;
};

빌더 패턴: 알고리즘 인터페이스 클래스

  • 복합 객체의 생성 방식이 다를 경우를 대비하여 인터페이스 클래스로 선언합니다.
  • Algorithm 클래스는 생성 객체인 Computer를 반환하는 getInstance() 메서드를 갖고 있습니다.
class Algorithm
{
public:
    virtual void showAlgorithm() = 0;
    virtual void setCpu(const std::string& cpu) = 0;
    virtual void setRam(const std::vector<Memory>& ram) = 0;
    virtual void setStorage(const std::vector<Storage>& storage) = 0;

    Computer getInstance()
    {
        return m_computer;
    }

protected:
    Computer m_computer;
};

빌더 패턴: 알고리즘 인터페이스 클래스 구현

  • Algorithm 인터페이스 클래스를 상속하여 std::vector<T> 방법으로 복합객체를 구성하는 구현 클래스를 정의합니다.
class AlgorithmVector final :
    public Algorithm
{
public:
    void showAlgorithm() override
    {
        std::cout << "[AlgorithmVector] Vector Builder" << std::endl;
    }

    void setCpu(const std::string& cpu) override
    {
        m_computer.setCpu(cpu);
    }

    void setRam(const std::vector<Memory>& ram) override
    {
        for (const auto& item : ram)
        {
            m_computer.setMemory(item);
        }
    }

    void setStorage(const std::vector<Storage>& storage) override
    {
        for (const auto& item : storage)
        {
            m_computer.setStorage(item);
        }
    }
};

빌더 패턴: 빌더 인터페이스 클래스 구현

  • 빌더 인터페이스 클래스를 상속받는 세부적인 구현 클래스를 정의합니다.
  • 부모 클래스인 인터페이스 클래스의 생성자를 호출하여 생성자 호출 시 초기화를 수행합니다.
  • 빌더의 구현 클래스는 생성할 객체를 반환하기 때문에 Computer 객체를 반환하는 build() 메서드를 구현합니다.
  • IBuilderm_algorithm의 타입을 확인하고, 복합 객체의 생성 방식에 따라 build() 메서드를 다르게 구현할 수도 있습니다.
  • Builder 클래스와 동일한 역할을 수행하는 클래스를 다양하게 만들어서 원하는 Computer 객체를 생성할 수 있습니다.
class Builder final :
    public IBuilder
{
public:
    Builder(std::shared_ptr<Algorithm> algorithm) : IBuilder(algorithm)
    {
        // 실제 구현 클래스에서 부모 클래스의 생성자를 호출한다.
    }

    Computer build() override
    {
        std::cout << "[Builder] Build..." << std::endl;
        m_algorithm->showAlgorithm(); // algorithm 클래스의 메서드 호출.

        m_algorithm->setCpu("i7"); // algorithm 클래스의 메서드 호출.

        std::vector<Memory> memory;
        memory.push_back(Memory(8));
        memory.push_back(Memory(8));
        m_algorithm->setRam(memory); // algorithm 클래스의 메서드 호출.

        std::vector<Storage> storage;
        storage.push_back(Storage(256));
        storage.push_back(Storage(512));
        m_algorithm->setStorage(storage); // algorithm 클래스의 메서드 호출.

        return m_algorithm->getInstance();
    }
};

빌더 패턴: 실행 코드

  • 빌더 패턴을 사용한 Computer 객체 생성 방법입니다.
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Algorithm> algorithm = std::make_shared<AlgorithmVector>();
    std::shared_ptr<IBuilder> factory = std::make_shared<Builder>(algorithm);

    Computer computer = factory->build();
    computer.showInfo();

    return 0;
}
  • 빌더 패턴을 사용한 결과는 다음과 같습니다.
[Builder] Build...
[AlgorithmVector] Vector Builder
[Computer] This computer's spec is...
CPU: i7,
RAM: 16GB,
Storage: 16GB.
  • 빌더 패턴은 구조가 매우 복잡합니다.
  • 복합 객체의 생성 전략을 외부에서 받아오는 전략 패턴(Strategy)을 사용한 패턴입니다.
  • 이해가 부족한 경우 전략 패턴을 참고합니다.