도서 요약 / / 2023. 2. 17. 20:57

디자인 패턴 - 컴포지트(composite) 패턴

udemy 강좌(Java Design Patterns & SOLID Design Principles)를 정리한 내용이다.

https://www.udemy.com/course/design-patterns-in-java-concepts-hands-on-projects/learn/lecture/9604610?start=0#overview


컴포지트는 무엇인가?

이제 컴포지트 디자인 패턴을 구현하는 방법과 왜 구조적 디자인 패턴이 필요한지 알아볼 것이다.

우선 왜 컴포지트가 필요하고 언제 사용할 수 있을까?

  • 객체의 부분-전체 관계 혹은 계층구조가 있을 때 이 구조에서 단일 형태로 처리하고 싶을 때
  • 단지 객체지향 프로그래밍의 합성 개념은 아니고 좀 더 강화된 내용이다.
  • 트리 구조를 다루는 복합 패턴을 생각하면 된다.

컴포지트 구현

  • 컴포넌트에 대한 추상 클래스/인터페이스를 생성하면서 시작
    • 컴포넌트는 리프(leaf)와 컴포지트 양쪽에 적용될 수 있는 모든 메소드를 정의
    • 누가 자식의 동작을 관리할 지 정해야 한다. (컴포넌트 혹은 컴포지트)
    • 그리고 나서 컴포지트를 구현하다. 컴포지트에서 실행되는 동작은 모든 자식에 전파된다.
    • 리프(leaf) 노드에서는 자식을 추가/삭제와 같이 적용될 수 없는 동작에 대해서 처리해야 한다.
  • 결국에 컴포지트 패턴 구현으로 노드가 리프인지와 상관없이 알고리즘을 사용할 수 있게 된다.

예제: UML

Java 구현

@AllArgsConstructor
@Getter
public abstract class File {

    private String name;

    public abstract void ls();

    public abstract void addFile(File file);

    public abstract File[] getFiles();

    public abstract boolean removeFile(File file);
}

여기에 File 이라는 추상 클래스가 있고 컴포지트 디자인 패턴에서 컴포넌트가 될 것이다. 이것은 리프 노드와 컴포지트 노드가 이 File 클래스를 확장한다는 것을 나타낸다. 이 File 클래스에서 이름을 나타내는 name이라는 하나의 필드를 가지고 있다. 그리고 파일명을 받는 생성자가 있고 getter 메서드와 setter 메서드가 있다.

그리고 추상 오퍼레이션을 정의했다.

파일을 삭제하고 추가하는 등 컴포지트에만 적용가능한 자식을 관리하는 오퍼레이션을 추가했다.

public class BinaryFile extends File {

    private long size;

    public BinaryFile(String name, long size) {
        super(name);
        this.size = size;
    }

    @Override
    public void ls() {
        System.out.println(getName() + "\t" + size);
    }

    @Override
    public void addFile(File file) {
        throw new UnsupportedOperationException("Leaf node doesn't support and operation");
    }

    @Override
    public File[] getFiles() {
        throw new UnsupportedOperationException("Leaf node doesn't support and operation");
    }

    @Override
    public boolean removeFile(File file) {
        throw new UnsupportedOperationException("Leaf node doesn't support and operation");
    }
}

이 BinaryFile은 File을 확장한다.

public class Directory extends File {

    private List<File> children = new ArrayList<>();

    public Directory(String name) {
        super(name);
    }

    @Override
    public void ls() {
        System.out.println(getName());
        children.forEach(File::ls);
    }

    @Override
    public void addFile(File file) {
        children.add(file);
    }

    @Override
    public File[] getFiles() {
        return children.toArray(new File[children.size()]);
    }

    @Override
    public boolean removeFile(File file) {
        return children.remove(file);
    }
}

Directory 클래스는 컴포지트이다.

public class Client {

    public static void main(String[] args) {
        File root1 = createTreeOne();
        root1.ls();

        System.out.println("*************************");

        File root2 = createTreeTwo();
        root2.ls();;
    }

    private static File createTreeOne() {
        File file1 = new BinaryFile("File1", 1000);
        Directory dir1 = new Directory("dir1");
        dir1.addFile(file1);
        File file2 = new BinaryFile("file2", 2000);
        File file3 = new BinaryFile("file3", 150);
        Directory dir2 = new Directory("dir2");
        dir2.addFile(file2);
        dir2.addFile(file3);
        dir2.addFile(dir1);

        return dir2;
    }

    private static File createTreeTwo() {
        return new BinaryFile("Another file", 200);
    }
}

이것은 클라이언트 코드이다.

구현 고려사항

  • 부모 노드로 접근할 수 있는 메소드를 제공할 수 있다. 이 방법은 단순히 전체 트리를 순회하도록 하는 것이다.
  • 컴포지트 대신에 기반 컴포넌트에서 자식을 유지하도록 컬렉션 필드를 정의할 수 있지만 그 필드는 리프 클래스에서 사용하지는 않는다.
  • 리프 객체는 계층구조에서 반복적으로 사용될 수 있다면 리프노드를 공유함으로써 메모리를 절약하고 초기화 비용을 줄일 수 있다. 적은 양의 노드에 캐쉬를 사용하는 것이 더 비용이 들 수 있기 때문에 노드의 개수가 주요 결정 요소가 된다.

디자인 고려사항

  • 자식을 관리하는 오퍼레이션이 어디서 정의될 지 결정해야 한다. 컴포넌트에서 정의되면 투명성을 제공하지만 리프노드는 해당 메소드를 구현해야만 한다. 컴포지트에서 정의하면 더 안전하지만 클라이언트는 컴포지트를 인지해야 할 필요가 있다.
  • 디자인의 전반적인 목적은 클라이언트가 컴포지트를 사용할 때 구현하게 쉽게 만들어야 한다. 클라이언트가 오직 컴포넌트 인터페이스로만 동작하고 리프-컴포지트 구분을 할 필요가 없어야 한다.

위험요소

  • 계층 구조에 추가되는 것을 제한하기 어렵다. 다양한 유형의 리프노드가 시스템에 존재한다면 클라이언트는 해당 동작이 가능한지 런타임에 확인해야 한다.
  • 노드 재사용을 위해 캐쉬를 사용하고 노드 수가 많다면 기본적인 계층구조를 만드는 것이 복잡해 질 수 있다.



반응형
  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유