컴포짓 패턴

GoF의 디자인패턴 중 구조의 컴포짓 패턴을 Java 로 정리한 글

객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체복합 객체 모두 동일하게 다루도록 함

위의 다이어그램의 각각의 역할은 다음과 같다.

  • Component → 모든 객체(부분, 전체)의 추상 인터페이스

    • 공통적인 기능을 선언

  • LeafComponent 인터페이스의 단일 객체 구현체

  • CompositeComponent 인터페이스의 전체 관리 객체 구현체

    • Leaf와 똑같은 Component의 구현체이지만 CompositeLeaf들을 관리하는 기능(add, remove)이 있다.

컴포짓 패턴은 전체(composite)-부분(Leaf) 관계(Ex: Directory-File)를 갖는 객체들 사이의 관계 정의할 때 사용 → 즉 클라이언트에서 composite 구현체를 이용해 Leaf를 관리하는 것인데 이것을 클라이언트는 Component라는 추상 인터페이스로만 기능을 사용하면 된다.

바로 예제를 보자.

1. 예제

//component
public interface Saramin {
    int getWorkerNum();
    int getMaleNum();
    int getFemaleNum();
}

//composite
public class ITLaboratory implements Saramin {
    private List<Saramin> saramins = new ArrayList<>();
    @Override
    public int getWorkerNum() {return saramins.stream().mapToInt(Saramin::getWorkerNum).sum();}
    @Override
    public int getMaleNum() {return saramins.stream().mapToInt(Saramin::getMaleNum).sum();}
    @Override
    public int getFemaleNum() {return saramins.stream().mapToInt(Saramin::getFemaleNum).sum();}
    public void addSaramin(Saramin saramin) {this.saramins.add(saramin);}
}

//composite
public class PlanDepartment implements Saramin {
    private List<Saramin> saramins = new ArrayList<>();
    @Override
    public int getWorkerNum() {return saramins.stream().mapToInt(Saramin::getWorkerNum).sum();}
    @Override
    public int getMaleNum() {return saramins.stream().mapToInt(Saramin::getMaleNum).sum();}
    @Override
    public int getFemaleNum() {return saramins.stream().mapToInt(Saramin::getFemaleNum).sum();}
    public void addSaramin(Saramin saramin) {this.saramins.add(saramin);}
}

//composite의 composite?
public class Department implements Saramin {
    private List<Saramin> saramins = new ArrayList<>();
    @Override
    public int getWorkerNum() {return saramins.stream().mapToInt(Saramin::getWorkerNum).sum();}
    @Override
    public int getMaleNum() {return saramins.stream().mapToInt(Saramin::getMaleNum).sum();}
    @Override
    public int getFemaleNum() {return saramins.stream().mapToInt(Saramin::getFemaleNum).sum();}
    public void addSaramin(Saramin saramin) {this.saramins.add(saramin);}
}

//leaf
@AllArgsConstructor
public class Planner implements Saramin {
    private String sex;
    @Override
    public int getWorkerNum() {return 1;}
    @Override
    public int getMaleNum() {return sex.equals("male") ? 1 : 0;}
    @Override
    public int getFemaleNum() {return sex.equals("female") ? 1 : 0;}
}

//leaf
@AllArgsConstructor
public class Developer implements Saramin {
    private String sex;
    @Override
    public int getWorkerNum() {return 1;}
    @Override
    public int getMaleNum() {return sex.equals("male") ? 1 : 0;}
    @Override
    public int getFemaleNum() {return sex.equals("female") ? 1 : 0;}
}

//client
public class Main {
    public static void main(String[] args) {
        ITLaboratory itLaboratory = new ITLaboratory();
        Developer d1 = new Developer("male");
        Developer d2 = new Developer("female");
        itLaboratory.addSaramin(d1);
        itLaboratory.addSaramin(d2);

        PlanDepartment planDepartment = new PlanDepartment();
        Planner p1 = new Planner("male");
        Planner p2 = new Planner("female");
        planDepartment.addSaramin(p1);
        planDepartment.addSaramin(p2);

        Department department = new Department();
        department.addSaramin(itLaboratory);
        department.addSaramin(planDepartment);

        client(d1);
        client(itLaboratory);
        client(planDepartment);
        client(department);
    }
    public static void client(Saramin saramin) {
        System.out.printf("worker:%d  male:%d  female:%d\n", saramin.getWorkerNum(), saramin.getMaleNum(), saramin.getFemaleNum());
    }
}

//결과
worker:1  male:1  female:0
worker:2  male:1  female:1
worker:2  male:1  female:1
worker:4  male:2  female:2

컴포짓 패턴에 매칭 시키면 다음과 같다.

  • SaraminComponent

  • DepartmentComposite (어찌보면 Composite Of Composite)

  • ITLaboratoryComposite

  • PlanDepartmentComposite

  • DeveloperLeaf

  • PlannerLeaf

위의 코드를 해석하면 부서직원은 모두 Saramin에 소속되어 있다. 그리고 직원개발자 ,기획자가 있고 개발자IT연구소 소속이고 기획자기획부서 소속이다.

클라이언트에서는 Leaf, Composite를 구별하지 않고 Saramin이라는 인터페이스의 추상 기능만으로 사용할 수 있다. 만약 추후 IT연구소에 다른 직원군이 생겨도 Saramin을 상속하고 구현만 하면 되고 클라이언트 코드는 변경될 필요가 없기 때문에 OCP를 만족하게 된다.

2. 장점과 단점

장점

  • 복잡한 트리 구조를 편하게 사용할 수 있습니다.

    • 클라이언트는 Component의 getPrice 메서드만 사용하면 되기 때문

  • 다형성과 재귀를 활용할 수 있습니다.

    • 하나의 getPrice 메서드가 구현체 마다 다르게 동작하는 다형성Leaf 객체를 찾기 위해 DFS와 같은 재귀를 활용하게 됩니다.

  • 클라이언트 코드를 변경하지 않고 새로운 구현체를 추가할 수 있습니다.

컴포짓 패턴을 사용함으로써 OCP(Open-Closed Principle) 즉, 개방 폐쇄 원칙을 지키면서 프로그래밍을 할 수 있다는 것을 알 수 있습니다.

단점

  • 트리를 만들야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에) 지나치게 일반화 해야 하는 경우가 생길 수 있습니다.

    • 예를 들어, 가격이 존재하지 않는 객체가 있을 수도 있는데, 이 객체는 getPrice가 굳이 필요하지 않지만 가방에 넣으려면 Component를 상속받아야 하기 때문에 지나친 일반화가 발생하는 경우라고 할 수 있습니다.

컴포짓 패턴을 적용하다가 억지로 일반화해야하는 경우가 발생한다면, 해당 구조가 컴포짓 패턴으로 구현하는 게 맞는지 다시 한 번 생각해봐야 합니다.

3. 컴포짓 패턴을 사용하는 Swing 라이브러리

스윙(Swing)은 자바 언어에서 GUI의 구현하기 위해 제공되는 라이브러리입니다. 자바에서 추구하는 WORE(Wirte Once, Run Everywhere)을 구현하기 위해 JDK 1.2 버전부터 사용되었습니다.

import javax.swing.*;

public class SwingExample {

    public static void main(String[] args) {
      	// 프레임을 만듬
        JFrame frame = new JFrame();

      	// 텍스트 필드 박스를 만들고 프레임에 추가
        JTextField textField = new JTextField();
        textField.setBounds(200, 200, 200, 40);
        frame.add(textField);

      	// 버튼을 만들고 프레임에 추가
        JButton button = new JButton("click");
        button.setBounds(200, 100, 60, 40);
        button.addActionListener(e -> textField.setText("Hello Swing"));
        frame.add(button);

      	// 프레임 크기 설정 후 보여주기
        frame.setSize(600, 400);
        frame.setLayout(null);
        frame.setVisible(true);
    }
}

여기서 JFrame, JTextField, JButton은 컴포짓 패턴으로 이루어져 있습니다. 이 3개의 객체는 전부 Component 라는 추상 클래스를 상속받고 있습니다.

프레임의 add 메서드는 아래 처럼 되어 있는데, 위 예시의 Bag(Composite) 같은 Component를 상속하는 객체들을 리스트로 가지고 있는 것을 알 수 있습니다.

public Component add(Component comp) {
        addImpl(comp, null, -1);
        return comp;
}

protected void addImpl(Component comp, Object constraints, int index) {
        synchronized (getTreeLock()) {
          
          	.... 생략
              
            // component라는 리스트에 파라미터로 받은 comp를 넣습니다.
            if (index == -1) {
                component.add(comp);
            } else {
                component.add(index, comp);
            }
            
          	.... 생략
              
        }
    }

4. 컴포짓 패턴의 방식

컴포짓 패턴에서 Composite 클래스는 자식들을 관리하기 위한 추가적인 메서드가 필요합니다. 이러한 메서드의 설계 방식에 따라 2가지 형태의 방식으로 나눌 수 있습니다.

안정성을 추구하는 방식

안정성을 추구하는 방식은 자식을 다루는 add(), remove() 와 같은 메소드들은 오직 Composite 만 정의되었다. 그로 인해, Client는 Leaf와 Composite을 다르게 취급하고 있습니다. 하지만 Client에서 Leaf객체가 자식을 다루는 메소드를 호출할 수 없기 때문에, 타입에 대한 안정성을 얻게 됩니다.

먼저 예시로 들었던, BagItem을 생각하면 됩니다.

일관성을 추구하는 방식

일관성을 추구하는 방식은 자식을 다루는 메소드들을 Composite가 아닌 Component에 정의하는 방식입니다. 그로 인해, ClientLeafComposite를 일관되게 취급할 수 있습니다. 하지만 ClientLeaf 객체가 자식을 다루는 메소드를 호출할 수 있기 때문에, 타입의 안정성을 잃게 됩니다.

Swing라이브러리가 일관성을 추구하는 방식으로 되어있습니다.


참고 : 코딩으로 학습하는 GoF의 디자인 패턴

컴포지트 패턴(Composite Pattern) :: 마이구미

Last updated