Design Pattern : Composite
컴포짓(Composite) 패턴
컴포짓 패턴은 전체 계층 구조에서 그 계층 구조를 구성하는 부분적인 객체들을 클라이언트에서 동일하게 취급할 수 있게 구조를 만드는 패턴입니다.
1. 정의
컴포짓 패턴은 클라이언트는 사용하는 객체가 계층 구조 상위의 객체인지, 하위의 객체인지에 상관없이 사용할 수 있습니다. 때문에 컴포짓 패턴은 계층 구조 즉, 트리 구조로 구성해야 한다는 제약사항이 있습니다.
2. 예시
가방에 아이템을 넣어 가격을 구하는 코드로 예시를 들어보겠습니다.
2-1. 컴포짓 패턴 적용 전 코드
컴포짓 패턴 적용 전 코드는 아래와 같습니다.
아이템 클래스
다양한 아이템들을 표현할 수 있는 아이템 클래스입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Item {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
public int getPrice() {
return this.price;
}
}
가방 클래스
다양한 아이템들을 넣어 놓는 가방 클래스 입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
public class Bag {
// 아이템들을 가지고 있음
private List<Item> items = new ArrayList<>();
public void add(Item item) {
items.add(item);
}
public List<Item> getItems() {
return items;
}
}
클라이언트
코드를 실행 시킬 수 있는 클라이언트 코드 입니다. 현재 아이템들의 가격은 클라이언트에서 구하게 됩니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Client {
public static void main(String[] args) {
// 아이템 생성 후
Item doranBlade = new Item("도란검", 450);
Item healPotion = new Item("체력 물약", 50);
// 가방에 넣음
Bag bag = new Bag();
bag.add(doranBlade);
bag.add(healPotion);
// 클라이언트 객체를 만든 후 도란 검의 가격, 가방 안에 들어있는 아이템들의 총 가격을 구함
Client client = new Client();
client.printPrice(doranBlade);
client.printPrice(bag);
}
// 아이템 가격을 구하는 메서드
private void printPrice(Item item) {
System.out.println(item.getPrice());
}
// 가방에 들어있는 아이템 총 가격을 구하는 메서드
private void printPrice(Bag bag) {
int sum = bag.getItems().stream().mapToInt(Item::getPrice).sum();
System.out.println(sum);
}
}
2-2. 컴포짓 패턴 적용 후 코드
컴포짓 패턴은 전체 계층 구조에서 그 계층 구조를 구성하는 부분적인 객체를 클라이언트 부분에서 동일하게 취급할 수 있게 구조를 만들어야 하기 때문에, Bag, Item을 Component를 상속받게 하여 부분적인 객체가 되어, 계층 구조를 만들 수 있습니다.
이것을 생각하면서 아래의 그림을 보게되면, Item은 Leaf가 되고 여러가지 Item(Leaf)을 가지고 있는 Bag은 Composite이 됩니다.
위의 그림에서 Leaf와 Composite은 Componenet를 상속받게 되기 때문에 클라이언트에서는 기존의 Item의 가격과 Bag 안의 총 가격을 구하는 메서드를 따로 구현할 필요 없이, Component에 가격을 구하는 메서드를 만들어 놓으면 Item과 Bag을 동일한 하나의 메서드로 처리할 수 있기 때문에 동일하게 취급할 수 있게 됩니다.
컴포넌트 인터페이스
아이템과 가방이 상속받게 될 컴포넌트 인터페이스를 만듭니다.
1
2
3
4
5
6
public interface Component {
// 상속 받은 클래스들이 구현해야하는 가격을 구하는 메서드
int getPrice();
}
아이템 클래스
다양한 아이템들을 표현할 수 있는 아이템 클래스입니다. 컴포넌트를 상속받았습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class Item implements Component {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
// 가격을 구하는 메서드를 Override하여 아이템의 가격을 구함
@Override
public int getPrice() {
return this.price;
}
}
가방 클래스
다양한 아이템들을 넣어 놓는 가방 클래스 입니다. 컴포넌트를 상속받았습니다. 기존과의 큰 차이점은 List에서 Item을 가지는 게 아니라 Component를 가지게 함으로써, 다양한 구현체들을 담을 수 있다는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Bag implements Component {
// 기존 처럼 Item을 가지고 있는 게 아니라, Component를 가지게 됨
private List<Component> components = new ArrayList<>();
public void add(Component component) {
components.add(component);
}
public List<Component> getComponents() {
return components;
}
// 가격을 구하는 메서드를 Override하여 가방안에 들어있는 Component 구현체들의 총 가격을 구함
@Override
public int getPrice() {
return components.stream().mapToInt(Component::getPrice).sum();
}
}
클라이언트
코드를 실행 시킬 수 있는 클라이언트 코드 입니다. 기존과의 차이점은 가방 안에 들어있는 아이템들의 가격을 구하는 메서드와 아이템의 가격을 구하는 메서드를 따로 구분하지 않고 Component의 getPrice만 호출해주면 된다는 것입니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Client {
public static void main(String[] args) {
// 아이템 생성 후
Item doranBlade = new Item("도란검", 450);
Item healPotion = new Item("체력 물약", 50);
// 가방에 넣음
Bag bag = new Bag();
bag.add(doranBlade);
bag.add(healPotion);
// 클라이언트 객체를 만든 후 도란 검의 가격, 가방 안에 들어있는 아이템들의 총 가격을 구함
Client client = new Client();
client.printPrice(doranBlade);
client.printPrice(bag);
}
// 기존의 코드와 달리 Component의 getPrice만 호출해주면 됨
private void printPrice(Component component) {
System.out.println(component.getPrice());
}
}
위와 같이 만든 계층 구조는 아래와 같은 트리 형식이 됩니다.
3. 장점과 단점
장점
- 복잡한 트리 구조를 편하게 사용할 수 있습니다.
- 클라이언트는 Component의 getPrice 메서드만 사용하면 되기 때문
- 다형성과 재귀를 활용할 수 있습니다.
- 하나의
getPrice메서드가 구현체 마다 다르게 동작하는다형성과Composite의 여러 가지 구현체를 담고 있는 리스트에 재귀(여기서는 Stream을 활용)를 활용할 수 있습니다.
- 하나의
- 클라이언트 코드를 변경하지 않고 새로운 구현체를 추가할 수 있습니다.
컴포짓 패턴을 사용함으로써 OCP(Open-Closed Principle) 즉, 개방 폐쇄 원칙을 지키면서 프로그래밍을 할 수 있다는 것을 알 수 있습니다.
단점
- 트리를 만들야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에) 지나치게 일반화 해야 하는 경우가 생길 수 있습니다.
- 예를 들어, 가격이 존재하지 않는 객체가 있을 수도 있는데, 이 객체는 getPrice가 굳이 필요하지 않지만 가방에 넣으려면 Component를 상속받아야 하기 때문에 지나친 일반화가 발생하는 경우라고 할 수 있습니다.
컴포짓 패턴을 적용하다가 억지로 일반화해야하는 경우가 발생한다면, 해당 구조가 컴포짓 패턴으로 구현하는 게 맞는지 다시 한 번 생각해봐야 합니다.
4. 컴포짓 패턴을 사용하는 Swing 라이브러리
스윙(Swing)은 자바 언어에서 GUI의 구현하기 위해 제공되는 라이브러리입니다. 자바에서 추구하는 WORE(Wirte Once, Run Everywhere)을 구현하기 위해 JDK 1.2 버전부터 사용되었습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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를 상속하는 객체들을 리스트로 가지고 있는 것을 알 수 있습니다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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);
}
.... 생략
}
}
5. 컴포짓 패턴의 방식
컴포짓 패턴에서 Composite 클래스는 자식들을 관리하기 위한 추가적인 메서드가 필요합니다. 이러한 메서드의 설계 방식에 따라 2가지 형태의 방식으로 나눌 수 있습니다.
안정성을 추구하는 방식
안정성을 추구하는 방식은 자식을 다루는 add(), remove() 와 같은 메소드들은 오직 Composite 만 정의되었다. 그로 인해, Client는 Leaf와 Composite을 다르게 취급하고 있습니다. 하지만 Client에서 Leaf객체가 자식을 다루는 메소드를 호출할 수 없기 때문에, 타입에 대한 안정성을 얻게 됩니다.
먼저 예시로 들었던, Bag과 Item을 생각하면 됩니다.
일관성을 추구하는 방식
일관성을 추구하는 방식은 자식을 다루는 메소드들을 Composite가 아닌 Component에 정의하는 방식입니다. 그로 인해, Client는 Leaf와 Composite를 일관되게 취급할 수 있습니다. 하지만 Client는 Leaf 객체가 자식을 다루는 메소드를 호출할 수 있기 때문에, 타입의 안정성을 잃게 됩니다.
Swing라이브러리가 일관성을 추구하는 방식으로 되어있습니다.

