의도

  • 부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리 구조로 구성함. 사용자로 하여금 개별 객체와 복합 객체를 모두 동일하게 다룰 수 있도록 하는 패턴.

 

언제 쓰는가?

  • 부분-객체의 객체 계통을 표현하고 싶을 때
  • 사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때. 사용자는 복합 구조의 모든 객체를 똑같이 취급하게 됨.

 

구조

GoF의 디자인 패턴 중 P.228
GoF의 디자인 패턴 중 P.228

  • Component : 집합 관계에 정의될 모든 객체에 대한 인터페이스를 정의함. 모든 클래스에 해당하는 인터페이스에 대해서는 공통의 행동을 구현. 전체 클래스에 속한 요소들을 관리하는 데 필요한 인터페이스를 정의. 순환 구조에서 요소들을 포함하는 전체 클래스로 접근하는 데 필요한 인터페이스를 정의하며, 적절하다면 그 인터페이스를 정의.
  • Leaf : 가장 말단의 객체, 즉 자식이 없는 객체를 나타냄. 객체 합성에 가장 기본이 되는 객체의 행동을 정의.
  • Composite : 자식이 있는 구성요소에 대한 행동을 정의. 자식이 복합하는 요소들을 저장하면서, Component 인터페이스에 정의된 자식 관련 연산을 구현.
  • Client : Component 인터페이스를 통해 복합 구조 내의 객체들을 조작함.

 

협력 방법

  • 사용자는 복합 구조 내 객체 간의 상호작용을 위해 Component 클래스 인터페이스를 사용. 요청받은 대상이 Leaf 인스턴스이면 자신이 정의한 행동을 직접 수행하고, 대상이 Composite이면 자식 객체들에게 요청을 위임. 위임하기 전후에 다른 처리를 수행할 수도 있음.

 

결과

  • 기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통의 정의.
  • 사용자의 코드가 단순해짐.
  • 새로운 종류의 구성요소를 쉽게 추가 가능.
  • 설계가 지나치게 범용성을 많이 가짐.

 

구현 / 고려 사항

  1. 포함 객체에 대한 명확한 참조자 : 자식 구성요소에서 부모를 가리키는 참조자를 관리하면 구조 관리를 단순화할 수 있음. 
  2. 구성요소 공유 : 메모리를 아낄 수 있음. 여러 부모를 가짐으로 가능. 어떤 부모에게 메시지를 보낼 지가 애매해짐. 플라이급 패턴에서 더 다룸.
  3. Component 인터페이스를 최대화 : Component 클래스는 Composite, Leaf에 정의된 모든 공통의 연산은 다 정의하고 있어야 함. 어떤 연산은 Composite 클래스에만 의미가 있고 Leaf에는 의미가 없을 수도 있음. 이럴 때 Composite 클래스에 대한 연산은 기본 구현을 하고, Leaf 관련 연산은 구현을 비워두는 것으로 해결 가능.
  4. 자식을 관리하는 연산 선언 : Add, Remove 선언하여 자식 관리. 하지만 Leaf는 말단 노드이기 때문에 더 이상 자식이 없기 때문에 Add, Remove 연산이 필요없는 연산일 수 있음. 이것에 대한 해결책으로 Component에서 Composite의 타입을 얻어올 수 있는 함수를 만들어 그 함수의 반환값, Composite이 nullptr이 아닐 때 Composite에 대해서만 Add, Remove를 하는 것입니다. 하지만 이 방법도 Leaf가 Add, Remove를 호출할 수 있다면 문제가 되며, Leaf에 대해 Remove는 부모에게서 지운다던가 하는 해석을 달리하는 방법이 있을 수도 있음. 허나, Add에 대해선 해석을 달리하기도 쉽지 않음.
  5. Component가 Component의 리스트를 구현할 수 있을까? : 자식들 집합을 Component 클래스의 인스턴스 변수로 관리하고 싶은 유혹에 빠질 수 있음. 허나, 바람직하지 않은 방법인 이유는 최상위 클래스에서 자식 관리를 위해 메모리를 할당한 것은 Leaf에도 메모리를 할당한다는 의미. Leaf는 자식이 없기 때문. 자식이 몇 개 없을 때만 가치있는 방법.
  6. 자식 사이의 순서 정하기 : 많은 설계에서 Composite 클래스의 자식들 간에 순서를 정하는 경우가 있음. 자식 간의 순서가 의미있는 경우, 자식에게 접근, 관리하는 인터페이스 설계에 주의를 기울여야 함. 반복자 패턴이 도움을 줄 수 있음.
  7. 성능 개선을 위한 캐싱 : 복합 구조 내부를 수시로 순회하고 탐색해야 한다면 미리 자식을 순회하는 정보를 담고 있을 수도 있음. Composite 클래스가 탐색, 순회 결과를 실제 임시 저장하는 것. 허나 이 저장 결과가 유효한지 체크하는 연산에 대한 정의도 필요할 것.
  8. 누가 구성요소를 삭제하는 책임을 질까 ? : 가비지 컬렉션의 기능이 없는 언어에서는 주로 Composite 클래스가 그 삭제의 책임을 짐. 그러나 Leaf 객체가 변경될 수 없는 객체이거나 공유될 수 있는 객체라면 예외적으로 삭제할 수 없음.
  9. 구성요소를 저장하기 위해 가장 적당한 데이터 구조는 ? : 연결리스트, 배열, 트리, 해시 테이블 등 모두 가능. 어느 것이 효율적인가에 따라 다름. 이런 데이터 구조를 안 쓰고 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

+ Recent posts