Nelmm
Design Pattern
Search…
⌃K

컴포짓 패턴

GoF의 디자인패턴 중 구조의 컴포짓 패턴을 Java 로 정리한 글
객체들의 관계를 트리 구조로 구성하여 부분-전체 계층을 표현하는 패턴으로, 사용자가 단일 객체복합 객체 모두 동일하게 다루도록 함
Untitled
위의 다이어그램의 각각의 역할은 다음과 같다.
  • 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
Untitled
컴포짓 패턴에 매칭 시키면 다음과 같다.
  • SaraminComponent
  • DepartmentComposite (어찌보면 Composite Of Composite)
  • ITLaboratoryComposite
  • PlanDepartmentComposite
  • DeveloperLeaf
  • PlannerLeaf
위의 코드를 해석하면 부서직원은 모두 Saramin에 소속되어 있다. 그리고 직원개발자 ,기획자가 있고 개발자IT연구소 소속이고 기획자기획부서 소속이다.
Untitled
클라이언트에서는 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가지 형태의 방식으로 나눌 수 있습니다.
composite-way

안정성을 추구하는 방식

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

일관성을 추구하는 방식

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