본문 바로가기
IT/Java

[Java] Java Composite Pattern

by kyu-nahc 2024. 9. 16.
반응형

 

Composite Pattern

Composite 패턴구조 패턴 중 하나로,

객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴이다.

Composite Pattern은 복합 객체(Composite)와 단일 객체(Leaf)동일한 컴포넌트로 취급하여,

클라이언트에게 이 둘을 구분하지 않고 동일한 인터페이스를 사용하도록 하는 구조 패턴인 것이다.

 

따라서 Composite 패턴은 전체-부분의 관계를 갖는

객체들 사이의 관계를 트리 계층 구조로 정의해야 할 때 유용하다.

사용자는 이 복합체 패턴을 통해 단일 객체와 복합 객체 모두 동일하게 다룰 수 있다.

여기서 구조 패턴이란 작은 클래스들을 상속과 합성을 이용하여

더 큰 클래스를 생성하는 방법을 제공하는 패턴이다.

이 패턴을 사용하면 서로 독립적으로 개발한 클래스 라이브러리를 마치 하나인 것처럼 사용가능하다.

또, 여러 인터페이스를 합성(Composite)하여 서로 다른 인터페이스들의 통일된 추상을 제공한다.

이는 컴파일 단계가 아닌 런타임 단계에서 복합 방법이나 대상을 변경 가능하여 유연성을 갖는다.

 

Composite Pattern 구조

  • Component
    Leaf와 Compsite를 묶는 공통적인 상위 인터페이스
  • Composite
    복합 객체로서, Leaf 역할이나 Composite 역할을 넣어 관리하는 역할을 한다.  
    Component 구현체들을 내부 리스트로 관리한다.
    add와 remove 메서드는 내부 리스트에 단일 / 복합 객체를 저장한다.
    Component 인터페이스의 구현 메서드인 operation은
    복합 객체에서 호출되면 재귀하여, 추가 단일 객체를 저장한 하위 복합 객체를 순회하게 된다.
  • Leaf
    단일 객체로서, 단순하게 내용물을 표시하는 역할을 한다.

    Component 인터페이스의 구현 메서드인 operation은
    단일 객체에서 호출되면 적절한 값만 반환하거나 로직을 실행한다.
  • Client
    클라이언트는 Component를 참조하여 단일 / 복합 객체를 하나의 객체로서 다룬다.

복합체 패턴의 핵심은 Composite와 Leaf가 동시에 구현하는

operation() 인터페이스 추상 메서드를 정의하고,

Composite 객체의 operation() 메서드는 자기 자신을 호출하는 재귀 형태로 구현하는 것이다.

왜냐하면 트리 내부 안에 트리를 넣고, 마지막에 Leaf를 넣는 트리 구조를 생각해 보면,

재귀적으로 반복되는 형식이 나타나기 때문이다.

그래서 단일체와 복합체를 동일한 개체로 취급하여 처리하기 위해 재귀 함수 원리를 이용한다.

이에 대한 예시 코드는 다음과 같다.

// Leaf와 Compsite 를 묶는 공통적인 상위 인터페이스
public interface Component {
    void operation();
}
// 복합 객체로 Leaf와 Composite 객체를 모두 List로 관리한다.
public class Composite implements Component{

    private List<Component> components = new ArrayList<>();

    // Component 객체 추가
    public void add(Component component){
        components.add(component);
    }
    // Component 객체 삭제
    public void remove(Component component){
        components.remove(component);
    }
    // Component 객체 반환
    public Component getChild(int index){
        return components.get(index);
    }
    // 자기 자신의 로직 먼저 실행,
    // for Each문을 통해 자신의 List 구성 Component에 대해 로직 호출
    @Override
    public void operation() {
        System.out.println("Composite "+this+" Called");
        for(Component component : components){
            component.operation();
        }
    }
}
// Component의 구현체로써 operation() 연산만 수행한다.
public class Leaf implements Component{

    @Override
    public void operation(){
        System.out.println("Leaf "+this+" Called");
    }
}
public class Client {

    public static void main(String[] args) {

        // 첫번째 Composite 객체 생성
        Composite component1 = new Composite();
        // 두번째 Composite 객체 생성
        Composite component2 = new Composite();

        // 첫번째 Leaf 생성
        Leaf leaf1 = new Leaf();
        // 첫번째 Composite에 Leaf1과 Composite2를 추가
        component1.add(leaf1);
        component1.add(component2);

        Leaf leaf2 = new Leaf();
        Leaf leaf3 = new Leaf();
        Leaf leaf4 = new Leaf();
        // 두 번째 Composite에 Leaf2,Leaf3,Leaf4를 추가
        component2.add(leaf2);
        component2.add(leaf3);
        component2.add(leaf4);
        component1.operation();
    }
}

 

 

 

Composite Pattern 특징

패턴 적용 시기

  • 데이터를 다룰 때 계층적 트리 표현을 다루어야 할 때
  • 복잡하고 난해한 단일 / 복합 객체 관계를 간편히 단순화하여 균일하게 처리하고 싶을 때

패턴 장점

  • 단일체와 복합체를 동일하게 여기기 때문에 묶어서 연산하거나 관리할 때 편리하다.
  • 다형성 재귀를 통해 복잡한 트리 구조를 보다 편리하게 구성할 수 있다. 
  • 수평적, 수직적 모든 방향으로 객체를 확장할 수 있다.
  • 새로운 Leaf 클래스를 추가하더라도 클라이언트는 추상화된 인터페이스 만을 바라보기 때문에
    OCP(개방 폐쇄 원칙)를 준수한다. (단일 부분의 확장이 용이)

패턴 단점

  • 재귀 호출 특징 상 트리의 깊이(depth)가 깊어질수록 디버깅에 어려움이 생긴다.
  • 설계가 지나치게 범용성을 갖기 때문에 새로운 요소를 추가할 때,
    복합 객체에서 구성 요소에 제약을 갖기 힘들다.
  • 예를 들어, 계층형 구조에서 leaf 객체와 composite 객체들을
    모두 동일한 인터페이스로 다루어야 하는데, 이 공통 인터페이스 설계가 까다로울 수 있다.
    • 복합 객체가 가지는 부분 객체의 종류를 제한할 필요가 있을 때
    • 수평적 방향으로만 확장이 가능하도록 Leaf를 제한하는 Composite를 만들 때

 

Composite Pattern 적용 예시

Composite Pattern은 트리 구조로 구성된 객체들을 처리할 때,

개별 객체(leaf)와 그들을 묶은 복합 객체(composite)를 동일하게 다루기 위해 사용되는데,

이는 특히 복잡한 계층 구조를 표현할 때 유용하다.

실무에서 자주 볼 수 있는 파일 시스템(파일과 디렉토리)을 예시로 들 수 있다.

여기서는 파일과 디렉토리 모두 공통된 인터페이스로 처리되고,

디렉토리는 여러 파일 또는 하위 디렉토리를 포함할 수 있는 구조이다.

실제로 리눅스 환경에서는 윈도우와 다르게 디렉토리와 파일 모두 같은 파일로 취급된다.

즉 리눅스의 파일 시스템 또한 Composite Pattern를 통해 설계 가능하다고 볼 수 있다.

파일 시스템 구성을 Composite Pattern을 통해 구현하는 예시 코드를 살펴보면 다음과 같다.

interface FileSystemComponent {
    void showDetails();  // 공통 메서드
    long getSize();  // 크기를 구하는 메서드
}
class File implements FileSystemComponent {
    private String name;
    private long size;

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

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

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

 

FileSystemComponent은 파일과 디렉토리의 공통 인터페이스로,

showDetails()getSize() 메서드를 정의한다. FileLeaf 객체로, 실제 파일을 나타낸다.

파일은 자신의 이름과 크기를 가지고 있으며, showDetails() 메서드로 파일의 정보를 출력한다.

class Directory implements FileSystemComponent {
    private String name;
    private List<FileSystemComponent> components = new ArrayList<>();

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

    // 디렉토리에 파일 또는 다른 디렉토리를 추가하는 메서드
    public void addComponent(FileSystemComponent component) {
        components.add(component);
    }

    // 디렉토리의 상세 정보를 출력
    @Override
    public void showDetails() {
        System.out.println("디렉토리: " + name);
        for (FileSystemComponent component : components) {
            component.showDetails();  // 파일이나 디렉토리의 세부 정보를 출력
        }
    }

    // 디렉토리 내부 모든 파일의 크기를 합산
    @Override
    public long getSize() {
        long totalSize = 0;
        for (FileSystemComponent component : components) {
            totalSize += component.getSize();  // 재귀적으로 파일 크기 합산
        }
        return totalSize;
    }
}
public class Client {

    public static void main(String[] args) {

        // 개별 파일 생성
        File file1 = new File("file1.txt", 500);
        File file2 = new File("file2.txt", 300);
        File file3 = new File("file3.jpg", 1500);

        // 상위 디렉토리 생성
        Directory dir1 = new Directory("Documents");
        dir1.addComponent(file1);  // 디렉토리에 파일 추가
        dir1.addComponent(file2);

        // 하위 디렉토리 생성
        Directory dir2 = new Directory("Pictures");
        dir2.addComponent(file3);

        // 최상위 디렉토리 생성 및 서브 디렉토리 추가
        Directory rootDir = new Directory("Root");
        rootDir.addComponent(dir1);  // 디렉토리에 다른 디렉토리 추가
        rootDir.addComponent(dir2);

        // 전체 디렉토리 구조 출력
        rootDir.showDetails();

        // 전체 크기 출력
        System.out.println("전체 파일 크기: " + rootDir.getSize() + " bytes");
    }
}

 

DirectoryComposite 객체로, 여러 파일 또는 다른 디렉토리를 포함할 수 있다.

addComponent() 메서드를 통해 하위 파일이나 디렉토리를 추가할 수 있으며,

getSize()를 통해 재귀적으로 전체 크기를 계산한다.

Directory는 위의 구조 코드와 동일하게 재귀적 구조를 가지는데,

Directory는 자신의 하위에 또 다른 Directory 또는 File을 포함할 수 있다.

이를 통해 트리 구조를 표현하며, 모든 객체를 동일한 방식으로 다룰 수 있게 되는 것이다.

 

실무에서는 이러한 패턴을 파일 시스템, 그래픽 객체 트리, UI 구성 요소 트리 등

다양한 계층적 구조를 관리할 때 사용할 수 있다. 예를 들어, 복잡한 메뉴 시스템, 조직도 관리,

프로젝트 구조 등 다양한 계층적 데이터를 Composite 패턴으로 처리할 수 있다.

이처럼 Composite Pattern은 복잡한 계층 구조를 단순하게 처리할 수 있는 강력한 도구이다.

 

 

참고자료

https://inpa.tistory.com/entry/GOF-%F0%9F%92%A0-%EB%B3%B5%ED%95%A9%EC%B2%B4Composite-%ED%8C%A8%ED%84%B4-%EC%99%84%EB%B2%BD-%EB%A7%88%EC%8A%A4%ED%84%B0%ED%95%98%EA%B8%B0

https://readystory.tistory.com/131

반응형

'IT > Java' 카테고리의 다른 글

[Java] Java State Pattern  (2) 2024.09.16
[Java] Java Template Method Pattern  (1) 2024.09.16
[Java] Java Iterator Pattern  (0) 2024.09.15
[Java] Java Adapter Pattern  (1) 2024.09.15
[Java] Java Singleton Pattern  (2) 2024.09.14

loading