복합체 패턴(Composite Pattern)

  • 복합체 패턴은 객체 간의 계층적 구조화를 통해 객체를 확장하는 패턴입니다.
  • 계층적 구조는 재귀(Recursive)와 트리(Tree) 구조를 생각하면 좋습니다.
  • 일반적인 복합 객체로부터 복합체 패턴으로 확장해보겠습니다.

복합 객체

  • 복합 객체는 한 객체가 다른 객체를 포함하고 있는 관계를 의미합니다.
  • 대표적인 복합 객체로 컴퓨터가 있습니다.
  • 컴퓨터는 다양한 객체(모니터, 메모리, 저장장치 등)를 포함하고 있습니다.
class Computer
{
public:
    void setMonitor(std::shared_ptr<Monitor> monitor)
    {
        m_monitor = monitor;
    }

    void setDisk(std::shared_ptr<Disk> disk)
    {
        m_disk = disk;
    }

    void setMemory(std::shared_ptr<Memory> memory)
    {
        m_memory = memory;
    }

    std::shared_ptr<Monitor> m_monitor;
    std::shared_ptr<Disk> m_disk;
    std::shared_ptr<Memory> m_memory;

    std::string m_name = "Computer Composite";
};
  • ComputerMonitor, Disk, Memory를 포함하고 있는 복합 객체입니다.
  • 복합 객체는 복합 객체를 포함하는것이 가능합니다.
  • Monitor 예제를 살펴보겠습다.
class Monitor
{
public:
    void addMonitor(std::shared_ptr<Monitor32> monitor)
    {
        m_screen.push_back(monitor);
    }

    void show()
    {
        for (const auto& item : m_screen)
        {
            std::cout << item->m_name << std::endl;
        }
    }

    std::string m_name = "Monitor";
    std::vector<std::shared_ptr<Monitor32>> m_screen;
};

class Monitor32
{
public:
    std::string m_name = "32-inch Monitor";
};
  • Monitor는 32인치 모니터 Monitor32를 포함하고 있습니다.
  • DiskMemory도 확인해보겠습니다.
class Disk
{
public:
    std::string m_name = "Disk";
};

class Memory
{
public:
    std::string m_name = "Memory";
};
  • 이러한 복합 객체를 사용하는 방법은 다음과 같습니다.
int main(const int argc, const char* argv[])
{
    std::shared_ptr<Computer> computer = std::make_shared<Computer>();
    computer->setMonitor(std::make_shared<Monitor>());
    computer->m_monitor->addMonitor(std::make_shared<Monitor32>());
    computer->m_monitor->addMonitor(std::make_shared<Monitor32>());

    computer->setDisk(std::make_shared<Disk>());
    computer->setMemory(std::make_shared<Memory>());

    std::cout << computer->m_name << std::endl;
    std::cout << computer->m_monitor->m_name << std::endl;
    computer->m_monitor->show();
    std::cout << computer->m_disk->m_name << std::endl;
    std::cout << computer->m_memory->m_name << std::endl;
    return 0;
}
  • 출력 결과
Computer Composite
Monitor
32-inch Monitor
32-inch Monitor
Disk
Memory
  • 이렇게 복합 객체는 다른 객체를 포함할 수 있습니다.

복합체 패턴의 구조

  • 복합체 패턴은 복합 객체의 특징을 활용한 패턴입니다.

  • 복합체 패턴은 재귀적 결합을 통해 하나의 객체가 다수의 연결 객체를 가질 수 있습니다.

  • 복합체 패턴은 4개의 구성 요소를 갖고 있습니다.

    • 컴포넌트(Component)
      • Composite와 Leaf 클래스를 모두 관리하기 위한 인터페이스 클래스입니다.
    • 컴포지트(Composite)
      • Compisite 클래스는 다른 객체를 포함할 수 있는 클래스입니다.
    • 리프(Leaf)
      • Leaf 클래스는 마지막 노드로 다른 객체를 포함할 수 없는 클래스입니다.
    • 클라이언트(Client)
  • 또 하나 특이한 점은 복합체 패턴은 객체지향의 원칙인 **단일 책임 원칙(SRP)**을 만족하지 않습니다.

  • 다른 객체를 포함하면서 자연스럽게 두 개 이상의 책임을 갖게 됩니다.

복합체 패턴: 컴포넌트(Component) 구조

  • 컴포넌트는 Composite, Leaf를 관리할 수 있는 인터페이스 클래스입니다.
class Component
{
public:
    void setName(std::string name)
    {
        m_name = name;
    }

    std::string getName()
    {
        return m_name;
    }

    virtual void print(int idx) = 0;

private:
    std::string m_name;
};

복합체 패턴: 컴포지트(Composite) 구조

  • 다른 객체를 포함할 수 있는 Composite 입니다.
  • Leaf와는 다르게 다른 객체를 추가하는 메서드가 있습니다.
class Composite :
    public Component
{
public:
    Composite(std::string name)
    {
        setName(name);
    }

    void addNode(std::shared_ptr<Component> component)
    {
        m_comp.push_back(component);
    }

    void print(int idx = 0) override final
    {
        for (int i = 0; i < idx; i++)
        {
            std::cout << "\t";
        }
        std::cout << "Directory : " << getName() << std::endl;

        for (const auto& item : m_comp)
        {
            item->print(idx + 1);
        }
    }

private:
    std::vector<std::shared_ptr<Component>> m_comp;
};

복합체 패턴: 리프(Leaf) 구조

  • 복합체 패턴에서 다른 객체를 포함하지 않고 제일 마지막 객체를 Leaf라고 합니다.
class Leaf :
    public Component
{
public:
    Leaf(std::string name)
    {
        this->setName(name);
    }

    void print(int idx = 0) override final
    {
        for (int i = 0; i < idx; i++)
        {
            std::cout << "\t";
        }
        std::cout << "File : " << getName() << std::endl;
    }
};

복합체 패턴: 실행 코드

  • 복합체 패턴은 컴퓨터의 디렉토리 구조와 닮아있습니다.
  • 디렉토리는 다른 디렉토리 또는 파일(Leaf)를 포함하고 있습니다.
int main(const int argc, const char* argv[])
{
    auto dirRed = std::make_shared<Composite>("Red");
    auto dirGreen = std::make_shared<Composite>("Green");
    auto dirBlue = std::make_shared<Composite>("Blue");

    auto dirBlack = std::make_shared<Composite>("Black");
    auto dirWhite = std::make_shared<Composite>("White");

    dirBlack->addNode(std::make_shared<Leaf>("A"));
    dirBlack->addNode(std::make_shared<Leaf>("B"));
    dirBlack->addNode(std::make_shared<Leaf>("C"));

    dirRed->addNode(dirGreen);
    dirGreen->addNode(dirBlue);
    dirBlue->addNode(dirBlack);

    dirBlue->addNode(dirWhite);
    dirWhite->addNode(std::make_shared<Leaf>("D"));
    dirWhite->addNode(std::make_shared<Leaf>("E"));

    dirRed->print();

    return 0;
}
  • 실행 결과
Directory : Red
        Directory : Green
                Directory : Blue
                        Directory : Black
                                File : A
                                File : B
                                File : C
                        Directory : White
                                File : D
                                File : E