프로그래밍/디자인패턴

컴포지트 패턴(Composite Pattern)

Jinwookoh 2025. 12. 7. 17:50

"파일 하나를 삭제하는 것"과 "폴더 전체를 삭제하는 것"은 사용자 입장에서 같은 '삭제' 행위입니다.

이처럼 개별 객체(Leaf)와 복합 객체(Composite)를 구분 없이 똑같이 다루고 싶을 때 사용하는 것이 컴포지트 패턴입니다. 하지만 이를 구현할 때는 "런타임 에러를 감수할 것인가?" vs "타입 체크를 할 것인가?" 라는 딜레마에 빠지게 됩니다.


1. 기본 개념 (시나리오)

파일 시스템을 만든다고 가정해 봅시다.

  • Component (공통 인터페이스): FileSystemNode
  • Leaf (단일 객체): File (하위 요소를 가질 수 없음)
  • Composite (복합 객체): Folder (하위 요소로 File이나 Folder를 가짐)

이때, add(추가), remove(삭제) 같은 자식 관리 메서드를 **"어디에 정의하느냐"**에 따라 두 가지 방식이 나뉩니다.


2. 방식 1: 투명한 컴포지트 (Transparent Composite)

자식 관리 메서드(add, remove)를 최상위 인터페이스(Component)에 모두 정의해버리는 방식입니다.

특징

  • "투명성(Transparency)": 클라이언트는 눈앞의 객체가 파일인지 폴더인지 전혀 신경 쓰지 않고 똑같이 add()를 호출할 수 있습니다.

코드 예시

Java
 
// 1. 공통 인터페이스 (모든 메서드를 다 정의함)
interface FileSystemNode {
    void print();
    // Leaf(파일)는 이 기능이 필요 없지만 인터페이스 통일성을 위해 포함
    void add(FileSystemNode node); 
}

// 2. Leaf (파일)
class File implements FileSystemNode {
    private String name;
    public File(String name) { this.name = name; }

    @Override
    public void print() { System.out.println("파일: " + name); }

    @Override
    public void add(FileSystemNode node) {
        // 파일에 파일을 추가할 순 없음 -> 예외 발생!
        throw new UnsupportedOperationException("파일에는 하위 요소를 추가할 수 없습니다.");
    }
}

// 3. Composite (폴더)
class Folder implements FileSystemNode {
    private List<FileSystemNode> children = new ArrayList<>();

    @Override
    public void print() { children.forEach(FileSystemNode::print); }

    @Override
    public void add(FileSystemNode node) { children.add(node); }
}

장단점

  • 장점: 클라이언트 코드가 매우 단순해집니다. if (node instanceof Folder) 같은 체크가 필요 없습니다.
  • 단점: 안전하지 않습니다. 파일(Leaf) 객체에 add()를 호출하면 런타임에 예외가 터집니다. "믿고 호출했다가 뒤통수 맞는" 격입니다.

3. 방식 2: 안전한 컴포지트 (Safe Composite)

자식 관리 메서드를 Composite(Folder) 클래스에만 정의하고, 인터페이스에는 공통 기능(print)만 남겨두는 방식입니다.

특징

  • "안전성(Safety)": 컴파일 단계에서 파일에 add()를 호출하는 실수를 막을 수 있습니다.

코드 예시

Java
 
// 1. 공통 인터페이스 (진짜 공통 기능만 정의)
interface FileSystemNode {
    void print();
}

// 2. Leaf (파일) - 깔끔함
class File implements FileSystemNode {
    public void print() { /* 출력 로직 */ }
    // add 메서드 자체가 존재하지 않음
}

// 3. Composite (폴더)
class Folder implements FileSystemNode {
    public void print() { /* 출력 로직 */ }
    
    // 자식 관리 기능은 폴더만 가짐
    public void add(FileSystemNode node) { /* 추가 로직 */ }
}

장단점

  • 장점: 안전합니다. 파일 객체에 add()를 호출하려고 하면 컴파일 에러가 나므로 실수를 원천 봉쇄합니다.
  • 단점: 클라이언트가 불편해집니다. 요소를 추가하려면 해당 객체가 폴더인지 확인하고 캐스팅(Type Casting) 해야 합니다.
  • Java
     
    if (node instanceof Folder) {
        ((Folder) node).add(newFile); // 캐스팅 필요
    }
    

4. 실무 예시 (Spring & Java)

  • Java AWT/Swing: Container(Composite)는 add()가 있고, Button(Leaf)은 없습니다. (Safe 방식에 가까움)
  • Spring CompositeCacheManager: 여러 캐시 매니저를 하나로 묶어 관리합니다. 클라이언트(CacheManager 인터페이스 사용자)는 이게 단일 매니저인지 복합 매니저인지 모르고 씁니다. (Transparent 지향)

요약: 무엇을 선택해야 할까?

구분 Transparent Composite (투명) Safe Composite (안전)
메서드 정의 인터페이스에 add/remove 포함 Composite 구현체에만 포함
Leaf 처리 예외 발생 (throw Exception) 메서드 없음 (컴파일 에러)
클라이언트 단순함 (모든 객체 동일 취급) 복잡함 (타입 확인/캐스팅 필요)
핵심 가치 사용 편의성 (Uniformity) 타입 안전성 (Type Safety)

결론

"이 트리 구조를 사용하는 클라이언트가 내부 구조를 몰라도 되게 하고 싶다"면 투명한(Transparent) 방식이 좋습니다. (실무에서는 주로 이쪽을 선호하되, add 호출 시 예외 처리를 꼼꼼히 합니다.)

반면, 시스템의 안정성이 최우선이고 컴파일 타임에 오류를 잡아야 한다면 안전한(Safe) 방식을 선택하세요.