[ Programming > Design Pattern ]
[디자인 패턴] Composite 패턴
Composite 패턴
Composite 패턴을 구현해서 개별 객체와 객체 그룹(부분-전체 계층구조)를 동일한 방식으로 다루는 방법을 알려드려요. 이 패턴으로 트리 구조의 복잡한 계층을 단순하게 처리하고, 부분과 전체를 구별하지 않고 일관된 코드를 작성할 수 있어요.
Composite 패턴이 왜 필요한가요?
실무에서는 보통 카테고리같은 기능을 구현하는데 자주 마주치는 문제에요. 만약 파일 시스템을 만든다고 예를 들어봐요. 파일 시스템을 만드는데 파일과 폴더를 따로 처리해야 해요. 폴더는 파일을 담을 수 있고, 폴더 안에 또 폴더가 들어갈 수 있죠. 그런데 용량을 계산하거나 출력할 때마다 파일인지 폴더인지 확인하는 코드를 작성해야 해요.
// Composite 패턴 없이 구현한 파일 시스템class File { constructor(name, size) { this.name = name; this.size = size; } getSize() { return this.size; }}class Folder { constructor(name) { this.name = name; this.children = []; } add(item) { this.children.push(item); } getSize() { let totalSize = 0; for (const child of this.children) { // 파일인지 폴더인지 매번 확인해야 함 if (child instanceof File) { totalSize += child.getSize(); } else if (child instanceof Folder) { totalSize += child.getSize(); } } return totalSize; }}// Client 코드const file1 = new File('document.txt', 100);const file2 = new File('image.png', 500);const folder = new Folder('My Folder');folder.add(file1);folder.add(file2);// 출력할 때도 타입 체크 필요function printStructure(item, indent = '') { if (item instanceof File) { console.log(`${indent}📄 ${item.name} (${item.size}KB)`); } else if (item instanceof Folder) { console.log(`${indent}📁 ${item.name}`); for (const child of item.children) { printStructure(child, indent + ' '); } }}printStructure(folder);
위 코드에 어떤 문제점들이 보이나요?
문제점
파일과 폴더를 처리할 때마다 타입을 체크해야 해요
새로운 타입(심볼릭 링크, 압축 파일...)이 추가되면 모든 조건문을 수정해야 해요
코드가 복잡하고 실수하기 쉬워요
파일과 폴더를 동일하게 다룰 수 없어요
이제 위와 같은 문제점은 Composite 패턴으로 해결할 수 있어요. Composite 패턴을 적용하면 File과 Folder를 동일한 인터페이스로 다룰 수 있게되요.
Composite 패턴으로 해결하기
Composite 패턴을 적용하면 개별 객체(파일)와 복합 객체(폴더)를 동일한 인터페이스로 다룰 수 있어요.
// Component: 공통 인터페이스class FileSystemComponent { constructor(name) { this.name = name; } getSize() { throw new Error("getSize()를 구현해야 합니다"); } print(indent = '') { throw new Error("print()를 구현해야 합니다"); }}// Leaf: 개별 객체 (파일)class File extends FileSystemComponent { constructor(name, size) { super(name); this.size = size; } getSize() { return this.size; } print(indent = '') { console.log(`${indent}📄 ${this.name} (${this.size}KB)`); }}// Composite: 복합 객체 (폴더)class Folder extends FileSystemComponent { constructor(name) { super(name); this.children = []; } add(component) { this.children.push(component); } remove(component) { const index = this.children.indexOf(component); if (index !== -1) { this.children.splice(index, 1); } } getSize() { // 타입 체크 없이 동일한 메서드 호출 return this.children.reduce((total, child) => { return total + child.getSize(); }, 0); } print(indent = '') { console.log(`${indent}📁 ${this.name}`); // 타입 체크 없이 동일한 메서드 호출 this.children.forEach(child => child.print(indent + ' ')); }}// Client 코드 - 파일과 폴더를 구별하지 않고 동일하게 처리const file1 = new File('document.txt', 100);const file2 = new File('image.png', 500);const file3 = new File('video.mp4', 2000);const subFolder = new Folder('Photos');subFolder.add(file2);subFolder.add(new File('photo2.jpg', 800));const mainFolder = new Folder('My Documents');mainFolder.add(file1);mainFolder.add(subFolder);mainFolder.add(file3);// 타입 체크 없이 간단하게 출력mainFolder.print();// 타입 체크 없이 간단하게 용량 계산console.log(`\n총 용량: ${mainFolder.getSize()}KB`);
그러면 위코드는 다음과 같이 출력될 수 있을 것 같아요.
📁 My Documents 📄 document.txt (100KB) 📁 Photos 📄 image.png (500KB) 📄 photo2.jpg (800KB) 📄 video.mp4 (2000KB)총 용량: 3400KB
이제 파일이든 폴더든 동일 인터페이스로 getSize()와 print()를 호출하면 돼요. 타입 체크가 사라졌어요.
Composite 패턴 클래스 다이어그램
Composite 패턴의 클래스 다이어그램이 어떻게 되는지 보여드릴게요.

client는 Component 추상 클래스만 보면 되요. 그리고 위 그림을 아래와 같은 구성 요소로 나눌 수 있어요.
구성 요소:
Component: 개별 객체와 복합 객체가 공유하는 공통 인터페이스
Leaf: 자식을 가질 수 없는 개별 객체 (파일)
Composite: 자식을 가질 수 있는 복합 객체 (폴더)
Client: Component 인터페이스만 사용해서 개별/복합 객체를 동일하게 처리
핵심 원리: Composite도 Component 인터페이스를 구현하므로, 클라이언트는 개별 객체인지 복합 객체인지 구별할 필요가 없어요.
Composite 패턴 vs 직접 구현 비교
구분 | Composite 패턴 | 직접 구현 |
---|---|---|
타입 체크 | 불필요 | 매번 필요 (if-else, instanceof) |
코드 일관성 | 개별/그룹을 동일하게 처리 | 타입마다 다른 코드 |
확장성 | 새 타입 추가가 쉬움 | 모든 조건문 수정 필요 |
재귀 처리 | 자연스럽게 지원 | 복잡한 로직 필요 |
마무리
이렇게 Composite 패턴을 통해서 트리 구조(계층 구조)를 가지는 경우 if문을 없애면서 동일 인터페이스로 깔끔하게 처리하는 방법에 대해 알아봤어요. 그런데 사실 if문을 쓰기도 해요. 더 안전하게 처리하기 위해 type 체크를 하기도 하죠. 그래서 적절하게 사용해야 해요. Composite 패턴의 중요한 점은 부분과 전체를 동일하게 다룰 수 있다는 점이에요. 이를 통해 복잡한 계층 구조를 단순하고 일관된 코드로 처리할 수 있어요!