[디자인 패턴] Chapter 11. Composite(복합적인) 패턴

4 분 소요

그릇과 내용물의 동일시

01. Composite 패턴

  • 컴퓨터의 파일 시스템
    • 디렉토리(폴더) 안에 파일이나 또 다른 디렉토리가 존재한다
    • 디렉토리와 파일을 합쳐서, ‘디렉토리 엔트리’라고 한다
  • 재귀적인 구조
    • 그릇 안에 내용물을 넣을 수도 있고, 작은 그릇을 넣을 수도 있다. 작은 그릇 안에는 더 작은 그릇이나 내용물을 넣을 수도 있다
  • Composite 패턴
    • 그릇과 내용물을 동일시 해서 재귀적인 구조를 만드는 패턴
    • composite: 혼합물, 복합물이라는 뜻

02. 예제 프로그램

파일과 디렉토리를 그림으로 표현하는 프로그램

이름 해설
Entry File과 Directory를 동일시 하는 추상 클래스
File 파일을 나타내는 클래스
Directory 디렉토리를 나타내는 클래스
FileTreatmentException 파일에 Entry를 추가하려고 했을 때에 발생하는 예외 클래스
Main 동작 테스트용 클래스

클래스 다이어그램

기본 아이디어

  • 트리 형태의 디렉토리 구조
  • 클라이언트가 root에 대해서 getSize()를 호출하면,
    • root는 자신의 내용물인 bin, tmp, usr에게 getSize()를 호출한다
    • bin은 자신의 내용물인 dir2, a.txt, b.txt에게 getSize()를 호출한다
      ⇒ 재귀적 호출 이용

Entry 클래스

  • 추상 클래스이고, 디렉토리 엔트리를 표현함
  • File 클래스와 Directory 클래스를 하위 클래스로 가진다
  • 이름과 size를 가지고 있다
  • add()
    • 디렉토리나 파일을 넣을 때 호출되는 메소드
    • 구현은 하위 클래스인 Directory가 제공한다
    • Entry 클래스에서는 이 메소드가 호출되면 예외를 발생시킨다
  • printList(), printList(String): 오버로드 된다
    • 호출될 때 인수의 모양에 따라 적절한 메소드가 실행
    • printList(String)는 protected로 하위 클래스에서만 접근 가능
  • toString(): 이름과 size를 문자열로 표현함
    • Template Method 패턴 (getName, getSize 추상 메소드)
public abstract class Entry {
  public abstract String getName();
  public abstract int getSize();
  public Entry add(Entry entry) throws FileTreatmentException {
    throw new FileTreatmentException();
  }
  public void printList() {
    printList("");
  }
  protected abstract void printList(String prefix);
  public String toString() {
    return getName() + "(" + getsize() + ")";
  }
}

File 클래스

  • 파일을 표현하는 클래스
  • name과 size 속성
  • 생성자 File(): 이름과 크기를 인자로 받아들임
    • new File(“readme.txt”, 1000)
  • getName(): 파일의 이름을 반환
  • getSize(): 파일의 크기를 반환
  • PrintList(String) 구현
    • prefix와 자신의 문자열 표현을 ‘/’로 묶어서 표현
    • 아래의 식은 모두 동일
      prefix + "/" + this
      prefix + "/" + this.toString()
      prefix + "/" + toString()
public class File extends Entry {
  private String name;
  private int size;
  public File(String name, int size) {
    this.name = name;
    this.size = size;
  }
  public String getName() {
    return name;
  }
  public int getSize() {
    return size;
  }
  protected void printList(String prefix) {
    System.out.println(prefix + "/" + this);
  }
}

Directory 클래스

  • name 필드 존재
  • size 필드는 존재하지 않는다
  • getSize()에서 디렉토리 사이즈를 동적으로 계산해서 반환한다
    • 현재 디렉토리 클래스에 포함된 모든 요소(디렉토리 또는 파일)를 하나하나 꺼내서 그 사이즈를 합한다
    • 아래 코드에서, entry가 File의 인스턴스나 Directory의 인스턴스중 어떤 것이라도 상관없다
      • 이유: entry는 Entry 타입으로 선언되어 있고, Entry는 File이나 Directory의 부모 클래스이기 때문에 둘 다 참조할 수 있다
        size += entry.getSize();
    • entry가 디렉토리인 경우에는, 다시 이 디렉토리의 getSize()가 재귀적으로 호출된다
      Composite 패턴의 재귀적 구조와 일치함
  • printList()
    • getSize 메소드와 마찬가지고 디렉토리에 포함된 Entry의 printList를 재귀적으로 호출한다
    • entry가 File의 인스턴스인지, Directory의 인스턴스인지 상관없음
      • 그릇과 내용물이 동일시 된다
import java.util.Iterator;
import java.util.ArrayList;

public class Directory extends Entry {
  private String name; // 디렉토리 이름
  private ArrayList directory = new ArrayList(); // 디렉토리 엔트리의 집합
  public Directory(String name) {
    this.name = name;
  }
  public String getName() { 
    // 이름을 얻는다
    return name;
  }
  public int getSize() {
    // 크기를 얻는다
    int size = 0;
    Iterator it = directory.iterator();
    while (it.hasNext()) {
      Entry entry = (Entry)it.next();
      size += entry.getSize();
    }
    return size;
  }
  public Entry add(Entry entry) {
    // 엔트리 추가
    directory.add(entry);
    return this;
  }
  protected void printList(String prefix) {
    System.out.println(prefix + "/" + this);
    Iterator it = directory.iterator();
    while (it.hasNext()) {
      Entry entry = (Entry)it.next();
      entry.printList(prefix + "/" + name);
    }
  }
}

FileTreatmentException 클래스

  • FileTreatmentException 클래스
  • RuntimeExcepton을 상속받아서 정의됨
  • Entry 클래스에 add() 메소드가 정의되어 있고, File 클래스에는 add() 메소드가 없다
    • 따라서, File 클래스에 대해서 add() 메소드가 호출되면, Entry로부터 상속받은 add()가 실행된다
    • 결국, FileTreatmentExceoption 예외가 발생
public class FileTreatmentException extends RuntimeException {
  public FileTreatmentException() {
  }
  public FileTreatmentException(String msg) {
    super(msg);
  }
}

Main 클래스

  • 교재 203 페이지와 같은 디렉토리 계층을 만든다. 그 후에, 이 디렉토리 구조를 출력
    rootdir.printList();
    • 재귀적으로, 디렉토리 안에 있는 요소(디렉토리나 파일)의 printList()가 호출된다
public class Main {
  public static void main(String[] args) {
    try{
      System.out.println("Making root entries...");
      Directory rootdir = new Directory("root");
      Directory bindir = new Directory("bin");
      Directory tmpdir = new Directory("tmp");
      Directory usrdir = new Directory("usr");
      rootdir.add(bindir);
      rootdir.add(tmpdir);
      rootdir.add(usrdir);
      bindir.add(new File("vi", 10000));
      bindir.add(new File("latex", 20000));
      rootdir.printList();

      System.out.println("");
      System.out.println("Making user entries...");
      Directory Kim = new Directory("Kim");
      Directory Lee = new Directory("Lee");
      Directory Park = new Directory("Park");
      usrdir.add(Kim);
      usrdir.add(Lee);
      usrdir.add(Park);
      Kim.add(new File("diary.html", 100));
      Kim.add(new File("Composite.java", 200));
      Lee.add(new File("memo.tex", 300));
      Park.add(new File("game.doc", 400));
      Park.add(new File("junk.mail", 500));
      rootdir.printList();
    } catch (FileTreatmentException e) {
      e.printStackTrace();
    }
  }
}

03. 등장 역할

  • Leaf(잎사귀) 역할
    • ‘내용물’에 해당되는 역할
    • 이 안에는 다른 것을 넣을 수 없다
    • 예제에서는 File 클래스가 해당됨
  • Composite(복합체) 역할
    • ‘그릇’을 나타내는 역할
    • Leaf와 Composite을 넣을 수 있다
    • 예제에서는 Directory 클래스가 해당됨
  • Component의 역할
    • Leaf 역할과 Composite 역할을 동일시 하기 위한 역할
    • Leaf와 Composite의 상위 클래스로 구현됨
    • 예제에서는 Entry 클래스가 해당됨
  • Client(의뢰자)의 역할
    • 예제에서는 Main 클래스가 해당됨

04. 독자의 사고를 넓혀주는 힌트

복수와 단수의 동일시

  • Composite 패턴은, 그릇과 내용물을 동일시 하는 패턴
    • 그릇이, 또 다른 그릇의 내용물이 될 수 있다

add를 어디에 두어야 하나?

  • 방법1: Entry 클래스에서 구현하고, 에러로 처리한다
    • 예제 프로그램이 여기에 해당
  • 방법2: Entry 클래스에서 구현하고, 아무것도 실행하지 않는다
  • 방법3: Entry 클래스에서 추상 메소드로 선언은 하지만, 구현은 하지 않는다
    • 하위 클래스에서 필요하면 정의하고, 필요하지 않으면 에러로 처리한다
  • 방법4: Directory 클래스에만 넣는다
    • 이 방법은, Entry형의 변수에 add할 때, Directory형으로 일일이 타입캐스트(형변환)해야 하는 번거로움이 있다
      (Directory)entry.add();

재귀적 구조는 여러 곳에서 등장한다

  • 예: 윈도우 안에는 자식 윈도우를 가진다
  • 일반적으로 트리 구조는 Composite 패턴에 속한다

05. 관련 패턴

  • Command 패턴(22장)
  • Visitor 패턴(13장)
  • Decorator 패턴(12장)

06. 요약

  • 그릇과 내용물을 동일시하고, 재귀적인 구조를 형성하는 Composite 패턴

연습문제

  • 각자 공부할 것

댓글남기기