Menu
Coddy logo textTech

컴포지트 패턴

Coddy PHP 여정의 객체 지향 프로그래밍 섹션에 포함된 레슨 — 91개 중 80번째.

컴포지트 패턴(Composite Pattern)은 객체들을 트리 구조로 구성한 뒤, 이러한 구조를 개별 객체인 것처럼 다룰 수 있게 해주는 구조적 디자인 패턴입니다. 이는 단일 객체와 객체 그룹을 동일하게 취급하려는 부분-전체 계층 구조를 표현하는 데 완벽한 방법입니다.

파일 시스템을 생각해 봅시다. 폴더는 파일과 다른 폴더를 포함할 수 있습니다.

전체 크기를 계산할 때, 파일과 폴더 모두에서 동일한 메서드를 호출하고 싶을 것이며, 폴더는 그 내용을 재귀적으로 합산해야 합니다. 컴포지트 패턴(Composite Pattern)은 리프(leaf, 파일)와 컴포지트(composite, 폴더) 모두에게 동일한 인터페이스를 제공함으로써 이를 매끄럽게 처리할 수 있게 해줍니다.

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

이제 트리 구조를 구축하고 이를 일관되게 처리할 수 있습니다:

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize();

출력:

1810

클라이언트 코드는 단일 파일인지 아니면 깊게 중첩된 폴더 구조인지 신경 쓰지 않고 getSize()를 호출합니다. 이 패턴은 내부적으로 재귀를 처리하여 복잡한 계층 구조를 쉽게 다룰 수 있게 해줍니다.

challenge icon

챌린지

쉬움

컴포지트 패턴(Composite Pattern)을 사용하여 조직도 시스템을 구축해 보겠습니다. 회사는 부서가 직원과 다른 하위 부서를 포함할 수 있는 계층 구조를 가지고 있습니다. 이는 조직 전체에 걸쳐 합계를 균일하게 계산하려는 경우에 적합한 트리 구조입니다.

코드는 다음 네 개의 파일로 구성됩니다:

  • OrganizationUnit.php — 개별 직원과 부서가 모두 구현할 OrganizationUnit 인터페이스를 정의합니다. 이 인터페이스는 getName(): stringgetSalary(): int라는 두 가지 메서드를 가져야 합니다. 이 공통 인터페이스를 통해 비용을 계산할 때 단일 직원과 전체 부서를 동일한 방식으로 처리할 수 있습니다.
  • Employee.php — OrganizationUnit 인터페이스를 포함하고 이를 구현하는 Employee 클래스를 생성합니다. 직원은 조직 트리의 리프 노드(leaf node)입니다. 이름과 급여를 가지지만 다른 단위를 포함할 수는 없습니다. 생성자 프로모션(constructor promotion)을 사용하여 이름과 급여를 받고, 두 인터페이스 메서드를 모두 구현하세요.
  • Department.php — OrganizationUnit 인터페이스를 포함하고 이를 구현하는 Department 클래스를 생성합니다. 부서는 직원과 다른 부서를 모두 포함할 수 있는 컴포지트(composite)입니다. 부서 클래스는 다음을 수행해야 합니다:
    • 생성자에서 이름을 받습니다.
    • 직원이나 하위 부서를 추가하기 위한 add(OrganizationUnit $unit): void 메서드를 가집니다.
    • 포함된 모든 단위의 총 급여를 반환하도록 getSalary()를 구현합니다 (내부의 모든 항목을 재귀적으로 합산).
  • main.php — Employee 및 Department 파일을 포함합니다. 조직 구조를 나타내는 JSON 문자열이라는 하나의 입력을 받게 됩니다.

    JSON은 다음과 같은 형식입니다:

    {"Engineering": [{"name": "Alice", "salary": 80000}, {"name": "Bob", "salary": 75000}], "Sales": [{"name": "Carol", "salary": 65000}]}

    각 키는 부서 이름이며, 값은 이름과 급여를 가진 직원들의 배열입니다.

    "Company"라는 이름의 루트 Department를 생성합니다. JSON의 각 부서에 대해 Department를 생성하고 직원을 추가한 다음, 해당 부서를 Company에 추가합니다. 마지막으로 다음과 같은 형식으로 회사의 총 급여를 출력합니다:

    Total Salary: [amount]

컴포지트 패턴의 장점은 Company 부서에서 getSalary()를 호출하면 모든 중첩된 부서와 직원의 합계가 자동으로 계산된다는 것입니다. 클라이언트 코드는 내부 구조를 알 필요가 없습니다.

치트 시트

컴포지트 패턴(Composite Pattern)은 객체들을 트리 구조로 구성하여 개별 객체와 복합 객체를 균일하게 다룰 수 있게 하는 구조적 디자인 패턴입니다.

이 패턴은 리프 노드(leaf node)와 복합 노드(composite node) 모두에 대해 동일한 인터페이스를 사용하고자 하는 부분-전체 계층 구조에 이상적입니다.

주요 구성 요소

  • Component Interface — 리프와 복합체 모두를 위한 공통 작업을 정의합니다
  • Leaf — 다른 객체를 포함할 수 없는 개별 객체를 나타냅니다
  • Composite — 자식(리프 또는 다른 복합체)을 포함하며, 자식에게 작업을 위임하여 기능을 구현합니다

예제: 파일 시스템

<?php
interface FileSystemItem {
    public function getName(): string;
    public function getSize(): int;
}

class File implements FileSystemItem {
    public function __construct(
        private string $name,
        private int $size
    ) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function getSize(): int {
        return $this->size;
    }
}

class Folder implements FileSystemItem {
    private array $items = [];
    
    public function __construct(private string $name) {}
    
    public function getName(): string {
        return $this->name;
    }
    
    public function add(FileSystemItem $item): void {
        $this->items[] = $item;
    }
    
    public function getSize(): int {
        $total = 0;
        foreach ($this->items as $item) {
            $total += $item->getSize();
        }
        return $total;
    }
}

사용법

<?php
$docs = new Folder("Documents");
$docs->add(new File("resume.pdf", 250));
$docs->add(new File("cover.docx", 50));

$images = new Folder("Images");
$images->add(new File("photo.jpg", 1500));

$root = new Folder("Root");
$root->add($docs);
$root->add($images);
$root->add(new File("readme.txt", 10));

echo $root->getSize(); // 1810

클라이언트는 파일과 폴더 모두에서 getSize()를 균일하게 호출합니다. 복합체는 내부적으로 재귀를 처리하여 복잡한 계층 구조를 간단하게 다룰 수 있게 해줍니다.

직접 해보기

<?php
require_once 'Employee.php';
require_once 'Department.php';

// JSON 입력을 읽습니다.
$input = trim(fgets(STDIN));
$data = (array)json_decode($input, true);

// TODO: "Company"라는 이름의 루트 부서(Department)를 생성합니다.

// TODO: JSON 데이터의 각 부서를 루프(순회)합니다.
// 각 부서에 대해:
//   - 새로운 부서(Department) 객체를 생성합니다.
//   - 각 직원을 해당 부서에 추가합니다.
//   - 해당 부서를 Company에 추가합니다.

// TODO: 총 급여를 다음 형식으로 출력합니다: Total Salary: [amount]

?>
quiz icon실력 점검

이 레슨에는 짧은 퀴즈가 포함되어 있습니다. 레슨을 시작해 문제를 풀고 진행 상황을 기록하세요.

객체 지향 프로그래밍의 모든 레슨