[ 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; } add() { throw new Error("add()를 구현해야 합니다"); } remove() { throw new Error("remove()를 구현해야 합니다"); } 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 패턴의 중요한 점은 부분과 전체를 동일하게 다룰 수 있다는 점이에요. 이를 통해 복잡한 계층 구조를 단순하고 일관된 코드로 처리할 수 있어요!

