의도
- 부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리 구조로 구성함. 사용자로 하여금 개별 객체와 복합 객체를 모두 동일하게 다룰 수 있도록 하는 패턴.
언제 쓰는가?
- 부분-객체의 객체 계통을 표현하고 싶을 때
- 사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때. 사용자는 복합 구조의 모든 객체를 똑같이 취급하게 됨.
구조
- Component : 집합 관계에 정의될 모든 객체에 대한 인터페이스를 정의함. 모든 클래스에 해당하는 인터페이스에 대해서는 공통의 행동을 구현. 전체 클래스에 속한 요소들을 관리하는 데 필요한 인터페이스를 정의. 순환 구조에서 요소들을 포함하는 전체 클래스로 접근하는 데 필요한 인터페이스를 정의하며, 적절하다면 그 인터페이스를 정의.
- Leaf : 가장 말단의 객체, 즉 자식이 없는 객체를 나타냄. 객체 합성에 가장 기본이 되는 객체의 행동을 정의.
- Composite : 자식이 있는 구성요소에 대한 행동을 정의. 자식이 복합하는 요소들을 저장하면서, Component 인터페이스에 정의된 자식 관련 연산을 구현.
- Client : Component 인터페이스를 통해 복합 구조 내의 객체들을 조작함.
협력 방법
- 사용자는 복합 구조 내 객체 간의 상호작용을 위해 Component 클래스 인터페이스를 사용. 요청받은 대상이 Leaf 인스턴스이면 자신이 정의한 행동을 직접 수행하고, 대상이 Composite이면 자식 객체들에게 요청을 위임. 위임하기 전후에 다른 처리를 수행할 수도 있음.
결과
- 기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통의 정의.
- 사용자의 코드가 단순해짐.
- 새로운 종류의 구성요소를 쉽게 추가 가능.
- 설계가 지나치게 범용성을 많이 가짐.
구현 / 고려 사항
- 포함 객체에 대한 명확한 참조자 : 자식 구성요소에서 부모를 가리키는 참조자를 관리하면 구조 관리를 단순화할 수 있음.
- 구성요소 공유 : 메모리를 아낄 수 있음. 여러 부모를 가짐으로 가능. 어떤 부모에게 메시지를 보낼 지가 애매해짐. 플라이급 패턴에서 더 다룸.
- Component 인터페이스를 최대화 : Component 클래스는 Composite, Leaf에 정의된 모든 공통의 연산은 다 정의하고 있어야 함. 어떤 연산은 Composite 클래스에만 의미가 있고 Leaf에는 의미가 없을 수도 있음. 이럴 때 Composite 클래스에 대한 연산은 기본 구현을 하고, Leaf 관련 연산은 구현을 비워두는 것으로 해결 가능.
- 자식을 관리하는 연산 선언 : Add, Remove 선언하여 자식 관리. 하지만 Leaf는 말단 노드이기 때문에 더 이상 자식이 없기 때문에 Add, Remove 연산이 필요없는 연산일 수 있음. 이것에 대한 해결책으로 Component에서 Composite의 타입을 얻어올 수 있는 함수를 만들어 그 함수의 반환값, Composite이 nullptr이 아닐 때 Composite에 대해서만 Add, Remove를 하는 것입니다. 하지만 이 방법도 Leaf가 Add, Remove를 호출할 수 있다면 문제가 되며, Leaf에 대해 Remove는 부모에게서 지운다던가 하는 해석을 달리하는 방법이 있을 수도 있음. 허나, Add에 대해선 해석을 달리하기도 쉽지 않음.
- Component가 Component의 리스트를 구현할 수 있을까? : 자식들 집합을 Component 클래스의 인스턴스 변수로 관리하고 싶은 유혹에 빠질 수 있음. 허나, 바람직하지 않은 방법인 이유는 최상위 클래스에서 자식 관리를 위해 메모리를 할당한 것은 Leaf에도 메모리를 할당한다는 의미. Leaf는 자식이 없기 때문. 자식이 몇 개 없을 때만 가치있는 방법.
- 자식 사이의 순서 정하기 : 많은 설계에서 Composite 클래스의 자식들 간에 순서를 정하는 경우가 있음. 자식 간의 순서가 의미있는 경우, 자식에게 접근, 관리하는 인터페이스 설계에 주의를 기울여야 함. 반복자 패턴이 도움을 줄 수 있음.
- 성능 개선을 위한 캐싱 : 복합 구조 내부를 수시로 순회하고 탐색해야 한다면 미리 자식을 순회하는 정보를 담고 있을 수도 있음. Composite 클래스가 탐색, 순회 결과를 실제 임시 저장하는 것. 허나 이 저장 결과가 유효한지 체크하는 연산에 대한 정의도 필요할 것.
- 누가 구성요소를 삭제하는 책임을 질까 ? : 가비지 컬렉션의 기능이 없는 언어에서는 주로 Composite 클래스가 그 삭제의 책임을 짐. 그러나 Leaf 객체가 변경될 수 없는 객체이거나 공유될 수 있는 객체라면 예외적으로 삭제할 수 없음.
- 구성요소를 저장하기 위해 가장 적당한 데이터 구조는 ? : 연결리스트, 배열, 트리, 해시 테이블 등 모두 가능. 어느 것이 효율적인가에 따라 다름. 이런 데이터 구조를 안 쓰고 Composite 클래스에 별도의 변수를 두어 특정 관리 방법을 만드는 것도 방법. 이 때 해석자 패턴을 적용해 볼 수 있음.
예제 코드
- 하나의 집(Component)을 구성하기 위해 4개의 Wall(Composite)과 1개의 Roof(Composite)로 구성되는 예제를 짜봤습니다.
- Wall은 여러개의 Brick(Leaf)로 구성되고 Roof 또한 여러개의 Brick(Leaf)로 구성되는 형태입니다.
- 최종적으로 각각의 Brick의 Price에 따라 House의 Price가 결정됩니다.
#include "Composite1.h"
#include <iostream>
void AddBrick(Composite* composite, int price, int count)
{
for (int i = 0; i < count; i++)
{
std::string name = "brick" + std::to_string(i);
Brick* brick = new Brick(name, price);
composite->Add(brick);
}
}
int main(int argc, char* argv[])
{
House house("house");
Wall wall1("wall1");
Wall wall2("wall2");
Wall wall3("wall3");
Wall wall4("wall4");
Roof roof("roof");
AddBrick(&wall1, 10, 20);
AddBrick(&wall2, 10, 20);
AddBrick(&wall3, 10, 20);
AddBrick(&wall4, 10, 20);
AddBrick(&roof, 20, 50);
house.Add(&wall1);
house.Add(&wall2);
house.Add(&wall3);
house.Add(&wall4);
house.Add(&roof);
std::cout << "Total House Price : " << house.GetPrice() << std::endl;
return 0;
}
- house는 wall1, wall2, wall3, wall4, roof를 자식으로 가지게 되고
- wall1, wlal2, wall3, wall4, roof는 각각 AddBrick 메서드에 넘겨준 price, count에 영향을 받아 그 수, 가격만큼 brick을 자식으로 가지게 됩니다.
//Composite1.h
#pragma once
#include "Composite1_Sub.h"
#include <string>
#include <vector>
class Component
{
public:
virtual ~Component() {}
virtual int GetPrice() { return int(); }
virtual void Add(Component* component) {}
virtual void Remove(Component* component) {}
virtual bool IsParent() { return false; }
std::string GetName() { return name; }
protected:
Component(std::string name) : name(name) {}
private:
std::string name;
};
class Composite : public Component
{
public:
virtual int GetPrice() override;
virtual void Add(Component* component) override;
virtual void Remove(Component* component) override;
virtual bool IsParent() override { return true; }
protected:
Composite(std::string name) : Component(name) {}
private:
std::vector<Component*> childs;
};
class House : public Composite
{
public:
House(std::string name) : Composite(name) {}
};
class Wall : public Composite
{
public:
Wall(std::string name) : Composite(name) {}
};
class Roof : public Composite
{
public:
Roof(std::string name) : Composite(name) {}
};
class Leaf : public Component
{
public:
virtual int GetPrice() override { return price; }
protected:
Leaf(std::string name, int price) : Component(name), price(price) {}
private:
int price;
};
class Brick : public Leaf
{
public:
Brick(std::string name, int price) : Leaf(name, price) {}
};
- 일단 최상위 클래스 Component를 구성하고 Component를 상속받은 Composite, Component를 상속받은 Leaf를 구성합니다.
- Composite은 그 밑으로 또 다른 Component들(childs)을 소유할 수 있고 그에 따른 Add, Remove 메서드를 각각 오버라이드합니다.
- 이 때 Composite의 최종 가격을 결정짓는 것은 childs들이 가진 price들의 합입니다.
- House, Wall, Roof는 Composite을 상속하였고, Brick은 Leaf를 상속합니다.
//Composite1.cpp
#include "Composite1.h"
int Composite::GetPrice()
{
int price = 0;
for (Component* child : childs)
{
price += child->GetPrice();
}
return price;
}
void Composite::Add(Component* component)
{
childs.push_back(component);
}
void Composite::Remove(Component* component)
{
for(auto iter = childs.begin(); iter != childs.end(); iter++)
{
if (*iter == component)
{
childs.erase(iter);
break;
}
}
}
- 위 설명대로 composite의 price는 childs들의 price합으로 결정됩니다.
- add, remove는 composite에서만 동작할 수 있게 오버라이드합니다.
'디자인 패턴 > 구조' 카테고리의 다른 글
퍼사드(Facade) (1) | 2023.12.22 |
---|---|
장식자(Decorator) (0) | 2023.12.22 |
가교(Bridge) (2) | 2023.12.21 |
적응자(Adapter) (2) | 2023.12.20 |
구조 패턴 (0) | 2023.12.20 |