목차
개별과 묶음을 다르게 다뤄야 한다
파일 시스템에서 어떤 항목의 전체 크기를 구한다고 하자. 항목은 파일이거나 폴더다. 파일이면 자기 크기가 답이고, 폴더면 안에 든 것들을 다 더해야 한다.
int size(Object node) {
if (node instanceof File file) {
return file.getSize();
} else if (node instanceof Folder folder) {
int total = 0;
for (Object child : folder.getChildren()) {
total += size(child); // 폴더면 재귀
}
return total;
}
// ...
}클라이언트가 파일인지 폴더인지 매번 구분하고, 폴더면 재귀까지 직접 돌린다. 트리를 다루는 코드마다 이 instanceof 분기와 재귀가 되풀이된다.
복합체 = 개별과 묶음을 같은 것으로
복합체 패턴이 이걸 푼다.
개별 객체(잎)와 그것들의 묶음(가지)을 같은 인터페이스로 다뤄, 클라이언트가 둘을 구분하지 않게 한다.
파일이든 폴더든 그냥 “크기를 물어보면” 답하게 만든다. 폴더가 자식을 순회하는 재귀는 구조 안에 숨기고, 클라이언트는 잎인지 가지인지 몰라도 되게 한다.
공통 인터페이스로 묶는다
파일과 폴더를 하나의 인터페이스 아래 둔다.
public interface FileNode {
int size();
}파일도 FileNode, 폴더도 FileNode다. 그래서 “이건 파일인가 폴더인가”를 묻지 않고 FileNode.size()만 부르면 된다.
잎과 가지
**잎(leaf)**은 자식이 없다. 자기가 직접 답한다.
public class File implements FileNode {
private final int size;
public File(int size) { this.size = size; }
@Override
public int size() { return size; } // 자기 크기가 답
}**가지(composite)**는 자식을 들고 있다. 자식들에게 물어 모은다.
public class Folder implements FileNode {
private final List<FileNode> children = new ArrayList<>();
public void add(FileNode node) { children.add(node); }
@Override
public int size() {
int total = 0;
for (FileNode child : children) {
total += child.size(); // 자식에게 되묻는다
}
return total;
}
}묶음은 자식에게 되묻는다
Folder.size()의 핵심은 child.size()다. 그 자식이 파일이면 자기 크기를 주고, 또 폴더면 그 폴더가 다시 자기 자식들에게 묻는다. 재귀가 Folder 안에 들어 있는 것이다.
그래서 클라이언트의 코드에서 instanceof도 재귀도 사라진다. 트리를 얼마나 깊이 파고들지는 각 노드가 알아서 한다. 구조가 자기 자신을 처리하는 법을 아는 셈이다.
클라이언트는 구분하지 않는다
이제 어떤 노드를 받든 똑같이 다룬다.
FileNode node = ...; // 파일일 수도, 폴더일 수도
int total = node.size(); // 똑같이 부른다root.size()를 부르면 a(10) + sub의 크기가 더해지고, sub은 다시 b(20) + c(30)을 더해 60을 낸다. 클라이언트는 이 재귀를 모른 채 그냥 size() 한 번을 부른다.
트리가 자연스럽게 자란다
폴더 안에 폴더, 그 안에 또 폴더 - 얼마나 깊어져도 코드는 안 바뀐다. 새 층이 생겨도 Folder가 자식에게 묻는 방식은 그대로라, 트리가 자라도 처리 코드는 고정이다.
이게 복합체의 값어치다. 부분과 전체가 같은 모양이라, 재귀적으로 무한히 중첩되는 구조를 단순한 코드로 다룬다. 트리의 깊이나 형태가 미리 정해져 있지 않아도 된다.
투명성과 안전성
한 가지 설계 선택이 있다. add()·remove() 같은 자식 관리 메서드를 어디에 둘까.
- 공통 인터페이스에 둔다(투명성) -
FileNode에add()를 넣으면, 파일도add()를 갖는다. 클라이언트는 잎/가지를 완전히 똑같이 대할 수 있지만, 파일에add()를 부르는 의미 없는 호출이 가능해진다(보통 예외). - 가지에만 둔다(안전성) -
add()를Folder에만 두면 파일엔 없어 안전하지만, 자식을 추가하려면 클라이언트가 “이건 폴더다”를 알아야 해서 구분이 되살아난다.
GoF는 “구분 없이 다룬다”는 목적을 살려 투명성 쪽을 기본으로 보지만, 상황에 따라 안전성을 택하기도 한다. 완전한 통일과 타입 안전성 사이의 맞바꿈이다.
실무: 트리가 있는 곳이면
복합체는 부분-전체 트리가 있는 곳이면 어디에나 있다. 파일 시스템(파일·폴더), UI 컴포넌트(위젯·컨테이너), 메뉴(항목·서브메뉴), 조직도(사원·부서), 중첩된 주문 항목 등이다.
공통점은 “묶음도 개별처럼 취급된다”는 것이다. 폴더도 하나의 FileNode이고, 컨테이너도 하나의 컴포넌트다. 이 통일이 필요하면 복합체가 답이다.
정리
| 복합체란 | 개별(잎)과 묶음(가지)을 같은 인터페이스로 다룬다 |
| 문제 | 트리를 다룰 때마다 타입 구분(instanceof)과 재귀가 반복된다 |
| 고침 | 잎·가지가 공통 인터페이스를 구현하고, 가지는 자식에게 되묻는다 |
| 효과 | 클라이언트는 구분 없이 한 메서드로, 재귀는 구조 안에 숨는다 |
| 트리 성장 | 아무리 깊게 중첩돼도 처리 코드는 안 바뀐다 |
| 설계 선택 | add/remove를 공통(투명성)에 둘지 가지에만(안전성) 둘지 |
부분과 전체를 같은 모양으로 만들어, 트리를 구분 없이 다루는 것. 재귀를 클라이언트에서 걷어내 구조 안에 넣는 것 - 그게 복합체다.