안녕하세요! GoF 디자인 패턴 연재, 이번 시간에는 구조 패턴(Structural Patterns)의 한 종류로, 개별 객체와 객체의 집합을 동일한 방식으로 다룰 수 있게 해주는 컴포지트(Composite) 패턴에 대해 알아보겠습니다.


정의

컴포지트 패턴객체들을 트리 구조로 구성하여, '부분-전체' 계층을 표현하는 패턴입니다. 이 패턴을 사용하면 클라이언트는 개별 객체(Leaf)와 복합 객체(Composite)를 동일하게 취급할 수 있습니다.

가장 대표적인 예는 컴퓨터의 파일 시스템입니다.

  • 파일(File) 은 개별 객체입니다.
  • 폴더(Folder)는 다른 파일이나 또 다른 폴더들을 담을 수 있는 복합 객체입니다.

우리는 파일의 크기를 확인하거나 폴더의 전체 크기를 확인할 수 있습니다. 이때 폴더의 크기는 그 안에 포함된 모든 파일과 하위 폴더들의 크기를 합한 값이죠. 컴포지트 패턴을 사용하면, 클라이언트는 '파일'이든 '폴더'든 구분하지 않고 "크기를 알려줘"라는 동일한 요청을 보낼 수 있습니다. 이처럼 단일 객체와 객체 집합에 대해 동일한 인터페이스를 사용하게 하여 클라이언트 코드를 단순화하는 것이 이 패턴의 핵심입니다.

  • Component (컴포넌트): LeafComposite를 아우르는 공통 인터페이스입니다. 트리 내의 모든 객체들이 구현해야 할 메서드(예: operation())를 선언합니다. 또한, 자식을 관리하는 메서드( add(), remove() 등)를 선언할 수도 있습니다.)
  • Leaf (리프): 트리의 가장 말단에 있는 개별 객체입니다. 자식을 가지지 않으며, Component 인터페이스의 메서드를 구현합니다. (예: File)
  • Composite (컴포지트): 자식(Component)들을 가지는 복합 객체입니다. Component 인터페이스의 메서드를 구현하며, 보통 자신의 자식들에게 요청을 재귀적으로 전달하여 처리합니다. (예: Folder)
  • Client (클라이언트): Component 인터페이스만을 사용하여 트리 구조의 객체들을 다룹니다.

사용예시

앞서 설명한 파일 시스템 예제를 자바(Java) 코드로 구현해 보겠습니다.

1. 컴포넌트 (Component) 인터페이스 정의

파일과 디렉토리(폴더)가 공통으로 가질 기능의 인터페이스를 정의합니다.

// Component 인터페이스
public interface FileSystemItem {
    String getName();
    int getSize(); // 파일 크기 또는 디렉토리 총 크기
    void print(); // 계층 구조 출력
}

2. 리프 (Leaf) 클래스 구현

개별 객체인 File 클래스를 구현합니다.

// Leaf 클래스
public class File implements FileSystemItem {
    private String name;
    private int size;

    public File(String name, int size) {
        this.name = name;
        this.size = size;
    }

    @Override
    public String getName() { return name; }

    @Override
    public int getSize() { return size; }

    @Override
    public void print() { System.out.println("- " + getName() + " (size: " + getSize() + "KB)"); }
}

 

3. 컴포지트 (Composite) 클래스 구현

다른 FileSystemItem들을 자식으로 가질 수 있는 Directory 클래스를 구현합니다.

import java.util.ArrayList;
import java.util.List;

// Composite 클래스
public class Directory implements FileSystemItem {
    private String name;
    private List<FileSystemItem> children = new ArrayList<>();

    public Directory(String name) { this.name = name; }

    public void add(FileSystemItem item) { children.add(item); }

    public void remove(FileSystemItem item) { children.remove(item); }

    @Override
    public String getName() { return name; }

    @Override
    public int getSize() {
        // 재귀적으로 모든 자식들의 크기를 합산하여 반환
        int totalSize = 0;
        for (FileSystemItem item : children) {
            totalSize += item.getSize();
        }
        return totalSize;
    }

    @Override
    public void print() {
        System.out.println("D " + getName() + " (total size: " + getSize() + "KB)");
        // 자식들을 순회하며 print() 호출
        for (FileSystemItem item : children) {
            item.print();
        }
    }
}

4. 클라이언트 (Client) 코드

클라이언트는 FileSystemItem 인터페이스만으로 파일과 디렉토리를 동일하게 다룹니다.

public class Client {
    public static void main(String[] args) {
        // 루트 디렉토리 생성
        Directory root = new Directory("root");

        // 파일 생성
        File file1 = new File("file1.txt", 10);
        File file2 = new File("file2.txt", 20);
        
        // 하위 디렉토리 및 파일 생성
        Directory subDir = new Directory("sub-dir");
        File file3 = new File("file3.txt", 30);
        subDir.add(file3);

        // 루트에 추가
        root.add(file1);
        root.add(file2);
        root.add(subDir);

        // 전체 구조 출력
        root.print();
        
        // 실행 결과:
        // D root (total size: 60KB)
        // - file1.txt (size: 10KB)
        // - file2.txt (size: 20KB)
        // D sub-dir (total size: 30KB)
        // - file3.txt (size: 30KB)
    }
}

결론

  • 객체들이 트리 구조를 형성하고, 이 구조를 일관된 방식으로 다루고 싶을 때 : UI 컴포넌트(패널 안에 버튼, 다른 패널 등이 포함되는 구조), 조직도, 파일 시스템 등
  • 클라이언트 코드를 단순화하고 싶을 때 : 클라이언트는 개별 객체인지 복합 객체인지 신경 쓸 필요 없이 공통 인터페이스만 사용하면 되므로 코드가 간결해집니다.
  • 새로운 종류의 Component를 쉽게 추가할 때 : 개방-폐쇄 원칙(OCP)을 잘 따르는 구조로, 새로운 LeafComposite 클래스를 추가해도 기존 코드는 거의 영향을 받지 않습니다.

직관적이고 이해가 쉬운 구조를 가지고 있지만 다음과 같은 조건을 고려해야합니다

  • 설계의 일반화 : 때로는 시스템을 너무 일반화시켜 인터페이스에 특정 클래스에만 적용되는 새로운 기능을 추가하기 어려울 수 있습니다.
  • 자식 관리 메서드의 위치: Component 인터페이스에 자식을 추가/삭제하는 add(), remove() 메서드를 넣으면 Leaf 클래스는 이 메서드들을 불필요하게 구현해야 합니다. (예제처럼 예외를 던지거나 아무것도 하지 않도록 구현). 반대로 Composite 클래스에만 넣으면 클라이언트가 CompositeLeaf를 구분해서 사용해야 하므로 투명성이 깨집니다. 이는 설계 시 트레이드오프 관계에 있습니다.

컴포지트 패턴은 복잡한 트리 구조를 단순하고 우아하게 처리할 수 있는 강력한 도구입니다. 부분과 전체를 동일하게 다루는 아이디어를 잘 기억해두시면 다양한 상황에서 유용하게 활용할 수 있습니다. 기술사공부의 키워드는 부분-전체 / 트리구조 / Lead-Composite 가 있겠네요 예제의 코드가 제일 이해가 쉬울 거 같습니다. 다음으로는 Decorator 패턴에 대해서 알아보겠습니다

+ Recent posts