Composite
Composite는 개별 객체와 그 객체들의 그룹을 동일한 인터페이스로 다룰 수 있게 만드는 구조 패턴입니다. 트리 구조를 구성하면서, 클라이언트가 '이것이 말단인지 묶음인지' 신경 쓰지 않고 같은 메서드를 호출할 수 있게 합니다.
▶아키텍처 다이어그램
🔍 구조 다이어그램점선 애니메이션은 데이터 또는 요청의 흐름 방향을 나타냅니다
파일 시스템에서 폴더 전체의 크기를 구하려면 어떻게 해야 할까요? 폴더 안에 파일이 있을 수도 있고, 하위 폴더가 있을 수도 있습니다. 하위 폴더 안에 또 폴더가 있을 수도 있습니다. 이런 트리 구조에서 클라이언트가 '지금 이것이 파일인지 폴더인지'를 매번 확인하고 분기 처리를 해야 한다면, 코드는 타입 체크와 조건문으로 뒤덮입니다. 새로운 노드 타입이 추가될 때마다 모든 분기를 수정해야 하고, 재귀 탐색 로직이 여기저기 흩어집니다. 트리의 깊이가 깊어질수록 이 복잡성은 기하급수적으로 커집니다.
GUI 프레임워크 초기 설계에서 이 문제가 가장 먼저 부각됐습니다. 화면을 구성하는 요소는 버튼이나 텍스트 같은 말단 위젯일 수도 있고, 패널이나 윈도우처럼 다른 위젯들을 담는 컨테이너일 수도 있습니다. 그려야 할 때 '이것이 컨테이너인지 단일 위젯인지' 매번 분기하면 렌더링 코드가 위젯 타입 수에 비례해 복잡해집니다. Smalltalk의 MVC 프레임워크와 ET++ 같은 초기 GUI 라이브러리가 이 문제를 반복적으로 겪으면서, GoF는 부분과 전체를 동일하게 다루는 재귀 합성 구조를 Composite 패턴으로 정리했습니다. 오늘날 React의 컴포넌트 트리, DOM 트리, 파일 시스템 API 모두 이 원리 위에 서 있습니다.
Composite의 구조는 러시아 인형(마트료시카)과 비슷합니다. 큰 인형 안에 작은 인형이 있고, 그 안에 또 작은 인형이 있습니다. 하지만 마트료시카와 다른 점은, 큰 인형이든 가장 작은 인형이든 밖에서 보면 모양이 같다는 것입니다. 세 가지 역할로 구성됩니다. Component 인터페이스가 모든 노드의 공통 계약을 정의합니다. Leaf는 더 이상 자식이 없는 말단 객체로, 실제 작업을 수행합니다. Composite는 자식 Component 목록을 가지며, 자신의 operation()이 호출되면 모든 자식에게 같은 operation()을 재귀적으로 위임합니다. 예를 들어 폴더의 `getSize()`를 호출하면, 폴더는 자식 파일과 하위 폴더 각각의 `getSize()`를 호출하고 결과를 합산합니다. 하위 폴더도 같은 과정을 반복합니다. 클라이언트는 최상위에 한 번만 요청하면 트리 전체가 알아서 처리됩니다.
Composite와 Decorator는 둘 다 재귀적 합성 구조를 사용한다는 점에서 자주 비교됩니다. 차이는 목적에 있습니다. Composite는 여러 객체를 하나처럼 묶어 다루기 위한 것이고, Decorator는 하나의 객체에 기능을 겹겹이 추가하기 위한 것입니다. Composite는 트리 안에 자식이 여럿이지만, Decorator는 감싸는 대상이 항상 하나입니다. Iterator 패턴과도 접점이 있습니다. Composite가 트리 구조를 만들면, Iterator가 그 트리를 순회하는 방법을 분리합니다. 구조를 만드는 것은 Composite의 역할이고, 순회 전략을 결정하는 것은 Iterator의 역할입니다. 트리 구조가 필요한지, 순회만 분리하면 되는지를 먼저 판단하면 어떤 패턴이 맞는지 갈립니다.
Composite는 데이터나 UI가 자연스럽게 트리 형태를 띠는 곳이라면 거의 어디서든 등장합니다. 파일 탐색기에서 폴더와 파일을 같은 방식으로 복사·이동·삭제하는 것, React에서 컴포넌트 안에 컴포넌트를 넣고 동일한 라이프사이클을 적용하는 것, 그래픽 에디터에서 도형 그룹을 개별 도형처럼 이동·리사이즈하는 것이 모두 Composite 패턴입니다. 도입 신호는 분명합니다. '이것이 단일 객체인지 묶음인지'에 따라 분기하는 코드가 여러 곳에 반복될 때, 트리 구조를 순회하면서 각 노드에 동일한 작업을 적용해야 할 때가 Composite를 고려할 시점입니다. 다만 모든 계층 구조에 Composite를 적용하면 Leaf와 Composite의 차이를 의도적으로 감추기 때문에, Leaf에만 의미 있는 연산과 Composite에만 의미 있는 연산이 뒤섞여 인터페이스가 비대해질 수 있습니다. 트리 구조가 실제로 필요한 상황인지, 단순 리스트로 충분한지를 먼저 판단하는 것이 중요합니다.