개별 객체와 그것들의 묶음을 같은 방식으로 다루는 패턴.
interface Component { long getSize(); }
class File implements Component {
public long getSize() { return size; }
}
class Directory implements Component {
private final List<Component> children = new ArrayList<>();
public long getSize() {
return children.stream().mapToLong(Component::getSize).sum(); // 재귀
}
}
// 클라이언트는 파일인지 폴더인지 신경 쓰지 않는다
Component c = ...;
System.out.println(c.getSize());
"부분과 전체를 구분하지 않는다" 가 이 패턴의 가치다 — 클라이언트 코드에서 if (isDirectory) 분기가 사라진다.
어디에 나타나나
-
파일 시스템 (파일 · 디렉터리)
-
UI 컴포넌트 트리 (버튼 · 레이아웃)
-
조직도 · 메뉴 · 도형 그룹
-
수식 트리 · AST
-
"자기 자신을 담을 수 있는 구조" 면 대체로 컴포지트다
설계 선택 — 안전성 vs 투명성
-
투명한 방식 — Component 에 add/remove 를 둔다
- 클라이언트가 완전히 동일하게 다룬다
- 대신 Leaf 에서 add 를 부르면 런타임 예외
-
안전한 방식 — add/remove 를 Composite 에만 둔다
- 타입 안전하다
- 대신 클라이언트가 다운캐스팅을 해야 한다
-
GoF 는 투명성을 우선했지만 실무는 상황에 따라 갈린다
Flyweight — 공유로 메모리를 아낀다
// 변하지 않는 부분(내재 상태)을 공유한다
class TreeType { // 이름 · 색 · 텍스처 — 수천 그루가 공유
private final String name, texture;
}
class Tree { // 좌표만 각자 갖는다 (외재 상태)
private final int x, y;
private final TreeType type; // 공유 객체를 참조
}
나무 100만 그루를 그린다
- 전부 따로 만들면 텍스처 데이터가 100만 벌
- 종류가 10가지뿐이면 TreeType 10개만 만들어 공유한다
자바의 Integer 캐시(-128~127)가 이 원리다
면접 함정
- ❌ "Composite는 상속 구조" → 트리를 만드는 것은 합성이다. 인터페이스만 공유한다.
- ❌ "Flyweight는 항상 이득" → 공유 객체 조회 비용과 코드 복잡도가 늘어난다. 객체 수가 아주 많을 때만 값을 한다.
재귀 처리의 함정
// 깊이가 깊으면 스택 오버플로가 난다
public long getSize() {
return children.stream().mapToLong(Component::getSize).sum();
}
깊은 트리(수천 단계)에서는 반복문 + 스택으로 바꾼다
Deque<Component> stack = new ArrayDeque<>(); stack.push(root); while (!stack.isEmpty()) { ... }
순환 참조
A 가 B 를 담고 B 가 A 를 담으면 무한 재귀가 된다
- 추가할 때 순환을 검사하거나
- 순회 시 방문한 노드를 기록한다
Flyweight의 조건
-
객체 수가 아주 많다
-
상당 부분의 상태를 공유할 수 있다 (내재 상태)
-
나머지는 밖에서 넘겨줄 수 있다 (외재 상태)
-
셋 중 하나라도 안 맞으면 복잡도만 는다
-
공유 객체는 반드시 불변이어야 한다
- 한 곳에서 바꾸면 모든 사용처에 영향이 간다