정의
- 복합체 패턴 (Composite Pattern)이란 전체 객체를 마치 하나의 객체인 것처럼 조작할 수 있도록 객체 집합을 트리 구조로 그룹화하는 패턴이다. 쉽게 말해 “일괄적인 관리” 가 가능한 것이다.
- A라는 회사가 있다고 가정해보자. 회사가 잘 돌아가려면 구조 체계가 명확히 잡혀 있어야 한다. 하위 계층에는 직원들과 여러 팀 단위가 존재할 것이고, 그 팀 안에는 또 직원들과 여러 팀 단위가 존재할 것이다. 이러한 구조를 “트리 구조” 라고 한다.

- A 회사가 이제 직원들에게 중요한 공지사항 이메일을 보내고자 한다. 그런데 일일이 직원들에게 공지사항 이메일을 보내는 것은 굉장히 시간 낭비이지 않을까? 언제 일일이 직원들에게 이메일을 보내고 있겠나?
- 이럴 때 필요한 것이 바로 복합체 패턴 (Composite Pattern)이다. 복합체 패턴은 “프로토콜 하나 (Components)”를 이용해 모든 그룹들을 마치 하나의 객체처럼 관리할 수 있게끔 한다.

복합체 패턴 (Composite Pattern) 의 구조

- Components ⇒
- 복합체 패턴에 존재하는 Leaf와 Composite 객체 모두에 접근할 수 있는 프로토콜을 생성한다.
- Leaf ⇒
- 하위 계층 객체가 없는 가장 기본적인 단위의 객체.
- Components 프로토콜을 채택한 후 내부를 구현한다.
- Composite ⇒
- Leaf와 Composite를 하위 계층으로 갖는 객체.
- 하위 계층들의 내부는 자세히 알 수 없지만 Components 프로토콜을 통해 하위 계층들에 접근하여 원하는 값들을 얻어 올 수 있다.
- Client ⇒
- Components 프로토콜을 이용해 복합체 패턴에 존재하는 모든 객체를 사용하고자 하는 객체.
복합체 패턴 적용 과정 예시
- 다시 A 회사 이야기로 돌아와서, A 회사가 직원들에게 일일이 이메일을 보내는 것이 아니라 모든 직원들에게 EmailSendingSystem 프로토콜에 가입하도록 한다. 이것을 Components 프로토콜이라고 한다.
protocol EmailSendingSystem {
func sendEmail(content: String)
}
- 직원은 반드시 회사의 EmailSendingSystem에 동의해야 한다. 직원에게는 하위 계층이 없으며 하위 계층이 없는 객체를 Leaf라고 한다.
final class Employee: EmailSendingSystem {
var name: String
init(name: String) {
self.name = name
}
func sendEmail(content: String) {
print("\(self.name)님에게 이메일을 보냄 : \(content)")
}
}
- 팀 또한 회사의 EmailSendingSystem에 동의해야 한다. 팀에게는 직원들이나 다른 팀들이라는 하위 계층이 존재한다. 하위 계층이 존재하는 객체를 Composite라고 한다.
final class Team: EmailSendingSystem {
var teams: [EmailSendingSystem] = []
var name: String
init(name: String) {
self.name = name
}
func add(_ worker: EmailSendingSystem) {
teams.append(worker)
}
func sendEmail(content: String) {
for team in teams {
team.sendEmail(content: content)
}
}
}
- Client란 복합체 패턴 내에 있는 모든 객체들을 관리하고 Components 프로토콜을 이용해 실행하는 영역이다. 회사 이메일 시스템에 들어와 있는 모든 팀들과 사원들을 여기에 저장하며 모든 이메일 시스템을 관리하는 영역이다.
final class EmailSystemManager {
let iOSTeam = Team(name: "iOS")
let leo = Employee(name: "레오")
let elly = Employee(name: "엘리")
let jimmy = Employee(name: "지미")
let sam = Employee(name: "샘")
let marigold = Employee(name: "메리골드")
private func makeTeam() {
iOSTeam.add(leo)
iOSTeam.add(elly)
iOSTeam.add(jimmy)
iOSTeam.add(sam)
iOSTeam.add(marigold)
}
func sendEmailTo(employee: Employee, content: String) {
employee.sendEmail(content: content)
}
func sendEmailToAll(content: String) {
makeTeam()
iOSTeam.sendEmail(content: content)
}
}
- “모두에게 이메일 보내기” 버튼을 누르면 모두에게 공지사항을 보내는 로직을 실행한다.
import SwiftUI
struct SendingEmailView: View {
var body: some View {
ZStack {
Color.sendingEmailBackground
.ignoresSafeArea(.all)
makeEmailSendButton()
.padding()
}
}
private func makeEmailSendButton() -> some View {
return sendEmail()
}
private func sendEmail() -> some View {
let emailSystemManager = EmailSystemManager()
return Button {
emailSystemManager.sendEmailToAll(content: "모두에게 알립니다!")
} label: {
Label("모두에게 이메일 보내기", systemImage: "envelope")
}
.buttonStyle(.borderedProminent)
.tint(.sendingEmailBrown)
}
}
패턴 사용 시기
- 트리구조 (그룹 내에 또다른 하위 계층이 존재하는 객체) 를 한 번에 관리하고자 할 때.
- 대표적으로 회원들에게 공지사항 이메일을 전송하고자 할 때이다. 즉, 일일이 회원들에게 이메일을 보내는 것이 아닌 한 번에 그룹으로 묶어 모두에게 메일을 전송하고자 할 때 사용하면 좋다.
패턴의 장점
- OCP를 만족한다 ⇒ 기존에 작동하는 트리 구조 코드를 직접 손대지 않고도 Components 프로토콜을 이용해 기능을 추가 및 확장시킬 수 있다.
- 복잡한 트리 구조를 보다 간편하게 관리할 수 있다.
패턴의 단점
- 트리의 Depth가 길어질수록 라인 단위의 디버깅에 어려움이 생긴다.
참고 문헌
클라이언트 입장에서는 내부에서 어떻게 지지고 볶아서 전달하든 상관 없이 한 곳에만 call을 하면 되는 거군요