"파일 하나를 삭제하는 것"과 "폴더 전체를 삭제하는 것"은 사용자 입장에서 같은 '삭제' 행위입니다.
이처럼 개별 객체(Leaf)와 복합 객체(Composite)를 구분 없이 똑같이 다루고 싶을 때 사용하는 것이 컴포지트 패턴입니다. 하지만 이를 구현할 때는 "런타임 에러를 감수할 것인가?" vs "타입 체크를 할 것인가?" 라는 딜레마에 빠지게 됩니다.
1. 기본 개념 (시나리오)
파일 시스템을 만든다고 가정해 봅시다.
- Component (공통 인터페이스): FileSystemNode
- Leaf (단일 객체): File (하위 요소를 가질 수 없음)
- Composite (복합 객체): Folder (하위 요소로 File이나 Folder를 가짐)
이때, add(추가), remove(삭제) 같은 자식 관리 메서드를 **"어디에 정의하느냐"**에 따라 두 가지 방식이 나뉩니다.
2. 방식 1: 투명한 컴포지트 (Transparent Composite)
자식 관리 메서드(add, remove)를 최상위 인터페이스(Component)에 모두 정의해버리는 방식입니다.
특징
- "투명성(Transparency)": 클라이언트는 눈앞의 객체가 파일인지 폴더인지 전혀 신경 쓰지 않고 똑같이 add()를 호출할 수 있습니다.
코드 예시
Java
// 1. 공통 인터페이스 (모든 메서드를 다 정의함)
interface FileSystemNode {
void print();
// Leaf(파일)는 이 기능이 필요 없지만 인터페이스 통일성을 위해 포함
void add(FileSystemNode node);
}
// 2. Leaf (파일)
class File implements FileSystemNode {
private String name;
public File(String name) { this.name = name; }
@Override
public void print() { System.out.println("파일: " + name); }
@Override
public void add(FileSystemNode node) {
// 파일에 파일을 추가할 순 없음 -> 예외 발생!
throw new UnsupportedOperationException("파일에는 하위 요소를 추가할 수 없습니다.");
}
}
// 3. Composite (폴더)
class Folder implements FileSystemNode {
private List<FileSystemNode> children = new ArrayList<>();
@Override
public void print() { children.forEach(FileSystemNode::print); }
@Override
public void add(FileSystemNode node) { children.add(node); }
}
장단점
- 장점: 클라이언트 코드가 매우 단순해집니다. if (node instanceof Folder) 같은 체크가 필요 없습니다.
- 단점: 안전하지 않습니다. 파일(Leaf) 객체에 add()를 호출하면 런타임에 예외가 터집니다. "믿고 호출했다가 뒤통수 맞는" 격입니다.
3. 방식 2: 안전한 컴포지트 (Safe Composite)
자식 관리 메서드를 Composite(Folder) 클래스에만 정의하고, 인터페이스에는 공통 기능(print)만 남겨두는 방식입니다.
특징
- "안전성(Safety)": 컴파일 단계에서 파일에 add()를 호출하는 실수를 막을 수 있습니다.
코드 예시
Java
// 1. 공통 인터페이스 (진짜 공통 기능만 정의)
interface FileSystemNode {
void print();
}
// 2. Leaf (파일) - 깔끔함
class File implements FileSystemNode {
public void print() { /* 출력 로직 */ }
// add 메서드 자체가 존재하지 않음
}
// 3. Composite (폴더)
class Folder implements FileSystemNode {
public void print() { /* 출력 로직 */ }
// 자식 관리 기능은 폴더만 가짐
public void add(FileSystemNode node) { /* 추가 로직 */ }
}
장단점
- 장점: 안전합니다. 파일 객체에 add()를 호출하려고 하면 컴파일 에러가 나므로 실수를 원천 봉쇄합니다.
- 단점: 클라이언트가 불편해집니다. 요소를 추가하려면 해당 객체가 폴더인지 확인하고 캐스팅(Type Casting) 해야 합니다.
-
Java
if (node instanceof Folder) { ((Folder) node).add(newFile); // 캐스팅 필요 }
4. 실무 예시 (Spring & Java)
- Java AWT/Swing: Container(Composite)는 add()가 있고, Button(Leaf)은 없습니다. (Safe 방식에 가까움)
- Spring CompositeCacheManager: 여러 캐시 매니저를 하나로 묶어 관리합니다. 클라이언트(CacheManager 인터페이스 사용자)는 이게 단일 매니저인지 복합 매니저인지 모르고 씁니다. (Transparent 지향)
요약: 무엇을 선택해야 할까?
| 구분 | Transparent Composite (투명) | Safe Composite (안전) |
| 메서드 정의 | 인터페이스에 add/remove 포함 | Composite 구현체에만 포함 |
| Leaf 처리 | 예외 발생 (throw Exception) | 메서드 없음 (컴파일 에러) |
| 클라이언트 | 단순함 (모든 객체 동일 취급) | 복잡함 (타입 확인/캐스팅 필요) |
| 핵심 가치 | 사용 편의성 (Uniformity) | 타입 안전성 (Type Safety) |
결론
"이 트리 구조를 사용하는 클라이언트가 내부 구조를 몰라도 되게 하고 싶다"면 투명한(Transparent) 방식이 좋습니다. (실무에서는 주로 이쪽을 선호하되, add 호출 시 예외 처리를 꼼꼼히 합니다.)
반면, 시스템의 안정성이 최우선이고 컴파일 타임에 오류를 잡아야 한다면 안전한(Safe) 방식을 선택하세요.
'프로그래밍 > 디자인패턴' 카테고리의 다른 글
| 템플릿 메서드 패턴(Template Method Pattern) 총정리 (0) | 2025.12.07 |
|---|---|
| 빌더 패턴(Builder Pattern) 총정리 (0) | 2025.12.07 |
| 어댑터 패턴(Adapter Pattern) 완벽 정리 (1) | 2025.12.07 |
| 프록시 패턴(Proxy Pattern) (0) | 2025.12.07 |
| 옵저버 패턴 vs Pub/Sub 패턴 (0) | 2025.12.07 |