합성(合成, composition)이란, 어떤 타입의 객체들이 그와 다른 타입의 객체들을 포함하고 있을 경우에 성립하는 그 타입들 사이의 관계를 일컫습니다. 포함된 객체들을 모아서 이들을 포함한 다른 객체를 합성한다는 뜻인데, 이를테면 다음과 같은 경우죠.
1 2 3 4 5 6 7 8 9 10 11 12 13 | class Address { ... }; class PhoneNumber { ... }; class Person { public: ... private: std::string name; // 이 클래스를 이루는 객체 중 하나 Address address; // 마찬가지 PhoneNumber voiceNumber; // 역시 마찬가지 PhoneNumber faxNumber; // 이것도 마찬가지 }; | cs |
예제를 보시면 알겠지만 Person 객체는 string, Address, PhoneNumber 객체로 이루어져 있습니다. 개발자들 사이에선 '합성' 대신에 다른 용어들도 많이 쓰입니다. 이를테면 레이어링(layering), 포함(containment), 통합(aggregation) 혹은 내장(embedding) 등으로도 알려져 있지요.
public 상속의 의미가 "is-a(...는 ...의 일종이다)"라는 뜻을 가진 것처럼, 객체 합성 역시 의미를 가지고 있습니다. 실제로는 뜻이 두 개나 되는데, "has-a(...는 ...를 가짐)"을 뜻할 수도 있고 "is-implemented-in-terms-of(...는 ...를 써서 구현됨)"을 뜻할 수도 있습니다. 이렇게 뜻이 두 개인 이유는 소프트웨어 개발에서 여러분이 대하는 영역(domain)이 두 가지이기 때문입니다. 객체 중에는 사람, 이동수단, 비디오 프레임 등 우리 일상생활에서 볼 수 있는 사물을 본 뜬 것들이 있는데, 이런 객체는 소프트웨어의 응용 영역(application domain)에 속합니다. 응용 영역에 속하지 않는 나머지들은 버퍼, 뮤텍스, 탐색 트리 등 순수하게 시스템 구현만을 위한 인공물인데. 이런 종류의 객체가 속한 부분은 소프트웨어의 구현 영역(implementation domain)이라고 합니다. 여기서 객체 합성이 응용 영역의 객체들 사이에서 일어나면 has-a 관계입니다. 반면, 구현영역에서 일어나면 그 객체 합성의 의미는 is-implemented-in-terms-of 관계를 나타내는 것입니다.
위의 예제에서 Person 클래스가 나타내는 관계는 has-a 관계입니다. 하나의 Person 객체는 이름, 주소, 음성전화 및 팩스전화 번호를 가지고 있습니다. 사람이 이름의 일종(Person is a name)이라든지 사람이 주소의 일종(Person is a address)이라고는 말할 수 없겠지요. 사람이 이름을 가지며 사람이 주소를 가진다고 말하는 것이 자연스럽습니다.
상대적으로 오락가락하는 부분이 바로 is-a 관계와 is-implemented-in-terms-of 관계의 차이점일 것입니다. 예를 들어 객체로 구성된 작은 집합(set), 정확히 말해서 중복 원소가 없는 집합체를 나타내고 저장 공간도 적게 차지하는 클래스의 템플릿이 하나 필요하다고 가정합시다. 여러분은 표준 C++ 라이브러리에 list 템플릿을 재사용하여 Set 템플릿을 만들기로 합니다. 다시 말해, Set<T>는 list<T>로부터 상속을 받아, 실제로 Set 객체는 list 객체의 일종이 되는 것입니다.
1 2 | template<typename T> // Set을 만드는 데 list를 잘못 쓰는 방법 class Set: public std::list<T> { ... }; | cs |
public 상속을 받아 is-a 관계가 성립하면 list<T>에서 참인 것들이 전부 Set<T>에서도 참이어야 합니다. 하지만 list 객체는 중복 원소를 가질 수 있는 컨테이너입니다. 그러니까 3051이란 값이 list<int>에 두 번 삽입되면, 이 리스트는 3051의 사본 두 개를 품게 됩니다. 이와 대조적으로, Set 객체는 원소가 중복되면 안 됩니다. 따라서 Set이 list의 일종(is-a)이라는 명제는 참이 아닙니다. list 객체에 해당하는 사실들이 Set 객체에서도 통하는 게 아니니까요.
이들 두 클래스 사이의 관계가 is-a가 될 리 없으므로, public 상속은 지금의 관계를 모형화하는 데 맞지 않습니다. 정답은 이번 항목의 제목에 있는데, Set 객체는 list 객체를 써서 구현되는(is implemented in terms of) 형태의 설계가 가능하다는 사실을 잡아내는 것입니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 | template<class T> // Set을 만드는 데 list를 제대로 쓰는 방법 class Set { public: bool member(const T& item) const; void insert(const T& item); void remove(const T& item); std::size_t size() const; private: std::list<T> rep; // Set 데이터의 내부 표현부 }; | cs |
Set의 멤버 함수는 list에서 이미 제공하는 기능 및 표준 C++ 라이브러리의 다른 구성 요소를 잘 버무려서 만들기만 하면 되기 때문에, 실제 구현은 아주 쉽게 이해할 수 있을 정도로 간단합니다.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | template<typename T> bool Set<T>::member(const T& item) const { return std::find(rep.begin(), rep.end(), item) != rep.end(); } template<typename T> void Set<T>::insert(const T& item) { if (!member(item)) rep.push_back(item); } template<typename T> void Set<T>::remove(const T& item) { typename std::list<T>::iterator it = std::find(rep.begin(), rep.end(), item); if (it !+ rep.end()) rep.erase(it); } template<typename T> std::size_t Set<T>::size() const { return rep.size(); } | cs |
어짜피 만든 거, STL 컨테이너 규약에 똑바로 맞추어서 구현했다면 "인터페이스 설계는 제대로 쓰기엔 쉽게, 엉터리로 쓰기엔 어렵게 하자"라고 주장하는 이야기에 더 잘 부합하지 않곘냐는 생각을 할 수도 있습니다. 그러나 그 규약에 맞추려면 Set에 이렇게 저렇게 우겨 넣어야 할 것들이 많은데, 이걸 다 하다가는 결국 Set과 list 사이의 관계가 드러나지 못할 게 뻔합니다. 이번 항목에서 중요한 것은 바로 이 '관계'이므로, 원칙적인 STL 호환성을 살짝 뒤로 하고 내용 전달의 명확성을 살린 거라고 보시면 되겠습니다. 이 관계는 is-a가 아니라, is-implemented-in-terms-of입니다.
객체 합성(composition)의 의미는 public 상속이 가진 의미와 완전히 다릅니다.
응용 영역에서 객체 합성의 의미는 has-a(...는 ...를 가짐)입니다.
구현 영역에서는 is-implemented-in-terms-of(...는 ...를 써서 구현됨)의 의미를 갖습니다.