폴더 안에 폴더, 그 안에 또 파일. 이런 트리 구조를 코드로 다뤄본 분이라면 한 번쯤 막막했을 거예요.
“이게 폴더야 파일이야?“를 매번 if로 갈라 치다 보면 코드가 금세 지저분해지거든요.
파일 탐색기 비슷한 걸 만들다 보면 흔히 겪는 지옥인데요. 이럴 때 꺼내 들 도구가 바로 컴포지트 패턴(Composite Pattern)입니다.
컴포지트 패턴은 개별 객체(파일)와 그 묶음(폴더)을 똑같은 타입으로 취급해서, 트리 전체를 객체 하나처럼 다루게 해주는 구조 패턴입니다.
오늘은 이 패턴을 Swift로 어떻게 풀어내는지, 제가 직접 짜본 코드와 함께 정리해볼게요.
핵심 요약 먼저
- 컴포지트 패턴 = 잎(Leaf)과 가지(Composite)를 같은 프로토콜로 묶는 것
- 클라이언트는 파일인지 폴더인지 구분하지 않고 똑같이 호출
- 폴더의 계산은 자식들에게 다시 물어보는 재귀 구조
- Swift에서는
protocol하나로 깔끔하게 구현 가능
컴포지트 패턴이 뭔가요?
말 그대로 ’합성’이에요. 작은 것들을 모아 하나의 큰 덩어리로 만드는 거죠.
가장 흔한 비유가 파일 시스템이에요.
파일 하나는 그 자체로 끝(잎)이에요. 반면 폴더는 안에 파일이나 또 다른 폴더를 품을 수 있는 가지고요.
그런데 사용자 입장은 좀 달라요. 폴더를 열든 파일을 열든, 우리는 그냥 “열어”라고 명령할 뿐이에요.
이렇게 개별 요소와 묶음을 같은 방식으로 다루게 하는 게 컴포지트 패턴의 목표예요.
구성 요소는 세 가지로 나뉩니다.
- Component: 공통 인터페이스 (프로토콜)
- Leaf: 자식이 없는 말단 객체 (파일)
- Composite: 자식을 담는 객체 (폴더)
Swift로 어떻게 구현하나요?
먼저 공통 프로토콜부터 정의해요. 이름과 크기를 물어볼 수 있게요.
아래는 파일과 폴더가 함께 따를 규칙(Component)입니다.
protocol FileComponent {
var name: String { get }
func size() -> Int // 바이트 크기 반환
}
이제 잎에 해당하는 파일이에요. 자기 크기만 딱 알려주면 끝이라 단순합니다.
struct File: FileComponent {
let name: String
let bytes: Int
func size() -> Int { bytes } // 자기 크기 그대로 반환
}
핵심은 폴더(Composite)예요. 자식들을 배열로 품고 크기를 물으면 자식들에게 다시 물어봐요.
struct Folder: FileComponent {
let name: String
var children: [FileComponent] = []
func size() -> Int {
children.reduce(0) { $0 + $1.size() } // 자식 합산(재귀)
}
}
여기서 size()가 스스로를 다시 부르는 재귀 구조라는 점이 포인트예요.
폴더 안의 폴더가 몇 겹이든, 이 한 줄이 알아서 바닥까지 훑어 내려갑니다.
실제로 쓰면 이런 느낌
이제 파일과 폴더를 섞어서 트리를 만들어볼게요.
let root = Folder(name: "문서", children: [
File(name: "메모.txt", bytes: 100),
Folder(name: "사진", children: [
File(name: "여행.jpg", bytes: 2000)
])
])
print(root.size()) // 2100 출력
보이시나요? root.size() 한 번이면 폴더 안 모든 파일 크기가 합산돼요.
제가 직접 돌려보고 제일 감탄한 부분이 이거였어요.
클라이언트 코드에서 “이게 폴더냐 파일이냐”를 단 한 번도 묻지 않는다는 것.
그냥 size()만 부르면 돼요. 나머지는 각 객체가 알아서 처리하니까요.
분기문으로 처리하는 것과 뭐가 다를까?
“그냥 if 문으로 타입 나눠서 처리하면 안 되나요?” 싶으실 거예요.
됩니다. 다만 트리가 깊어질수록 차이가 확 벌어져요.
| 구분 | 분기문(if/switch) 방식 | 컴포지트 패턴 |
|---|---|---|
| 타입 판별 | 매번 직접 확인 | 필요 없음 |
| 새 타입 추가 | 분기문 곳곳 수정 | 프로토콜만 채택 |
| 재귀 처리 | 직접 순회 코드 작성 | 객체가 알아서 |
| 코드 가독성 | 깊어질수록 복잡 | 일정하게 유지 |
분기 방식은 새로운 종류(예: 바로가기 링크)가 생기면 여기저기 if를 또 추가해야 해요.
반면 컴포지트는 FileComponent만 채택한 새 타입을 하나 만들면 끝이에요. 기존 코드는 건드릴 필요가 없죠.
자주 묻는 질문
Q. 잎과 가지의 인터페이스가 완전히 같아야 하나요?
이상적으로는 그래요. 다만 파일에는 “자식 추가” 같은 기능이 어색하죠. 이럴 땐 공통 동작(size 등)만 프로토콜에 두고 자식 관리는 폴더에만 두는 게 실용적이에요.
Q. class가 아니라 struct로 써도 되나요?
네, 위 예제처럼 값 타입으로도 충분히 동작해요. 다만 트리를 자주 변경하고 참조를 공유해야 한다면 class가 더 편할 수 있어요.
트리 구조 앞에서 if 지옥에 빠졌다면, 컴포지트 패턴을 한번 꺼내보세요.
파일 시스템뿐 아니라 뷰 계층, 메뉴, 조직도처럼 ’안에 또 안’이 있는 구조라면 어디든 잘 맞아요.
오늘 예제를 그대로 Playground에 붙여 돌려보시면 감이 확 올 거예요.
