Search
🎨

[디자인 패턴 - 구조 패턴] 컴포지트 패턴 (Composite)

클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다루고 싶을 때

1. 의도

GoF
부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리구조로 구성합니다.
사용자로 하여금 개별 객체와 복합 객체를 모두 동일하게 도룰 수 있도록 하는 패턴입니다.
Head First
객체를 트리구조로 구성해서 부분-전체 계층을 구현합니다.
컴포지트 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 똑같은 방법으로 다룰 수 있습니다.
부분-전체 계층구조를 가진 객체 컬렉션에서 그 객체들을 모두 똑같은 방식으로 다루고 싶을 때 사용

2. 활용성

GoF
부분 - 전체의 객체 계통을 표현하고 싶을 때
사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때, 사용자는 복합 구조(composite structure)의 모든 객체를 똑같이 취급하게 됩니다.
Head First
컴포지트 패턴
객체의 구성과 개별 객체를 노드로 가지는 트리의 형태로 객체 구조 생성 가능
복합 객체와 개별 객체를 구분할 필요가 없음
복합 구조 (composite structure)를 사용하면 복합 객체와 개별 객체를 대상으로 똑같은 작업을 적용할 수 있음

3. 구조

UML Class Diagram
구성요소(Component)의 종류
복합 객체(Composite)
잎 (Leaf)
재귀 구조를 띔
복합 객체에는 자식들이 들어 있으며, 그 자식들은 복합 객체일 수도 있고 잎일 수 도 있음
데이터를 이런 식을 조직화 하다보면 복합 객체 → 가지 → 잎 으로 끝나는 트리구조를 띔

4. 참여자

참여자
역할
예시
Component
집합 관계에 정의될 모든 객체에 대한 인터페이스 정의 - 모든 클래스에 해당하는 인터페이스에 대해서 공통의 행동 구현 - 전체 클래스에 속한 요소들을 관리하는데 필요한 인터페이스를 정의 복합 객체 내 들어 있는 모든 객체의 인터페이스를 정의 - 복합 노드와 잎에 관한 메소드도 정의
Leaf
가장 말단의 객체, 즉 자식이 없는 객체를 나타냄. 객체 합성에 가장 기본이 되는 객체의 행동을 정의 Composite에서 지원하는 기능 구현
Composite
자식이 있는 구성요소에 대한 행동을 정의 자식이 복합하는 요소들을 저장하면서, Component 인터페이스에 정의된 자식 관련 연산을 구현 자식이 있는 구성요소의 행동을 정의하고, 자식 구성 요소를 저장하는 역할을 맡음 Leaf에 관련된 기능도 구현해야함. 그런 기능들을 복합 객체에게 별 쓸모가 없다면 예외 처리로 대체 가능
Client
Component 인터페이스를 통해 복합 구조 내 객체들을 조작

5. 협력 방법

사용자는 복합 구조 내 객체간의 상호 작용을 위해 Component 클래스 인터페이스를 사용합니다.
요청받은 대상이 Leaf 인스턴스이면 자신이 정의한 행동을 직접 수행
대상이 Composite이면 자식 객체들에게 요청을 위임
위임 하기 전/후에 다른 처리를 수행할 수도 있음

6. 결과

GoF
기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통을 정의합니다.
사용자의 코드가 단순해 집니다.
사용자는 객체의 특성이 복합 구조인지 단일 구조인지 알필요 없음
새로운 종류의 구성요소를 쉽게 추가할 수 있습니다.
설계가 지나치게 범용성을 많이 가집니다.
새로운 요소를 쉽게 추가할 때의 단점은 복합체의 구성요소에 제약을 가하기 어려움
예) 복합체가 오직 한개의 구성요소만 가지고 싶을 때
Head First
단일 역할 원칙을 깨는 대신 투명성을 확보하는 패턴
Component 인터페이스에는 자식들을 관리하는 기능과 잎으로써의 기능을 전부 넣어, 클라이언트가 복합 객체와 잎을 똑같은 방식을 처리할 수 있도록 만들 수 있음.
어떤 원소가 복합 객체인지 잎이지 클라이언트에게는 투명하게 보임
투명하게 동작하기 위해 복합 객체 내 모든 객체는 인터페이스가 똑같아야함
인터페이스를 통일하다보면 객체에 따라 아무 의미없는 메소드가 생길 수도 있음
예외처리, Do Nothing 등 다양한 방법으로 처리 가능
클라이언트를 단순화 시킬 수 있음
복합 객체를 사용하는지, 잎 객체를 사용하는지 신경 쓸 필요가 없음

7. 예시 코드

// Component interface interface Employee { name: string; salary: number; getSalary(): number; } // Leaf node class for employees class Developer implements Employee { public name: string; public salary: number; constructor(name: string, salary: number) { this.name = name; this.salary = salary; } public getSalary(): number { return this.salary; } } // Composite node class for managers class Manager implements Employee { public name: string; public salary: number; public subordinates: Employee[]; constructor(name: string, salary: number) { this.name = name; this.salary = salary; this.subordinates = []; } public add(employee: Employee): void { this.subordinates.push(employee); } public remove(employee: Employee): void { const index = this.subordinates.indexOf(employee); if (index !== -1) { this.subordinates.splice(index, 1); } } public getSalary(): number { let totalSalary = this.salary; for (const subordinate of this.subordinates) { totalSalary += subordinate.getSalary(); } return totalSalary; } } // Client code const john = new Developer("John Doe", 50000); const jane = new Developer("Jane Smith", 60000); const mike = new Manager("Mike Smith", 100000); mike.add(john); mike.add(jane); const mary = new Manager("Mary Johnson", 120000); mary.add(mike); console.log(mary.getSalary()); // 220000
TypeScript
복사
예시 2
interface Graphic { draw(): void; move(x: number, y: number): void; } class Circle implements Graphic { constructor(private x: number, private y: number, private radius: number) {} public draw(): void { console.log(`Drawing a circle at (${this.x},${this.y}) with radius ${this.radius}`); } public move(x: number, y: number): void { this.x += x; this.y += y; } } class Rectangle implements Graphic { constructor(private x: number, private y: number, private width: number, private height: number) {} public draw(): void { console.log(`Drawing a rectangle at (${this.x},${this.y}) with width ${this.width} and height ${this.height}`); } public move(x: number, y: number): void { this.x += x; this.y += y; } } class Picture implements Graphic { private graphics: Graphic[] = []; public add(graphic: Graphic): void { this.graphics.push(graphic); } public remove(graphic: Graphic): void { const index = this.graphics.indexOf(graphic); if (index !== -1) { this.graphics.splice(index, 1); } } public draw(): void { console.log("Drawing picture:"); for (const graphic of this.graphics) { graphic.draw(); } } public move(x: number, y: number): void { for (const graphic of this.graphics) { graphic.move(x, y); } } } // Client code const circle1 = new Circle(10, 10, 5); const circle2 = new Circle(20, 20, 10); const rectangle1 = new Rectangle(30, 30, 15, 20); const rectangle2 = new Rectangle(40, 40, 30, 10); const picture1 = new Picture(); picture1.add(circle1); picture1.add(rectangle1); const picture2 = new Picture(); picture2.add(circle2); picture2.add(rectangle2); const picture3 = new Picture(); picture3.add(picture1); picture3.add(picture2); picture3.draw(); picture3.move(5, 5); picture3.draw();
TypeScript
복사