컴포지트 (Composite) 패턴
- 그룹 전체와 개별 객체를 동일하게 처리할 수 있는 패턴이다.
- 트리 형태로 데이터를 표현할 수 있을 때 유용하다.
- 여러 오브젝트를 트리 구조로 재구성한 뒤 오브젝트들을 각각의 오브젝트처럼 이용한다.
- 트리는
Leaf
와Composite
로 이루어져 있다.
- 트리는
- 클라이언트가 복잡한 구조의 오브젝트를 이용할 때 복잡함에 영향을 받지 않고 쉽게 오브젝트를 이용할 수 있도록 도와준다.
다이어그램으로 살펴보기
Component
가 구조상 가장Primitive
한 단위가 된다.Component
를 구현한 것이Leaf
가 된다.Component
를 구현하면서 내부에Component
를 포함하는 것이Composite
가 된다.- 이렇게 중첩 구조를 가진다.
Client
입장에서는 전체나 혹은 부분 모두 동일하게Component
로 인식할 수 있게 된다.Composite
도Component
를 구현한 구현체이기 때문이다.Client
는 둘을 구분하지 않고 둘 다Component
라는 인터페이스를 구현했다는 사실만 본다.
현실 예제
- 게임에서 상점 기능을 구현하려 한다.
- 게임에 인벤토리가 존재하고 인벤토리에는 아이템을 넣을 수 있다.
- 아이템 중에는 '가방' 이라는 아이템도 있다.
- 가방은 인벤토리의 효율을 높여주기 위한 아이템으로 내부에 여러 개의 아이템을 더 보관할 수 있다.
- 가방은 일종의 미니 인벤토리 기능을 하는 아이템이다.
- 게임 내에서 '아이템 다 팔기' 기능을 사용하면, 가방 내부에 존재하는 아이템과 가방까지 전부 처분해주어야 한다.
- '인벤토리' 내부에 '가방' 이라는 것이 깊이 제한 없이 계속 중첩될 수 있는 트리 형태가 만들어진다.
Item
인터페이스 구현하기
- 아이템은 간단히 이름과 가격을 가지고 있다고 가정했다.
public interface Item {
int getPrice();
String getName();
}
DefaultItem
클래스 구현하기
price
와name
필드를 직관적으로 가지고 있는 일반 아이템 클래스이다.
public class DefaultItem implements Item {
private final int price;
private final String name;
public DefaultItem(int price, String name) {
this.price = price;
this.name = name;
}
@Override
public int getPrice() {
return price;
}
@Override
public String getName() {
return name;
}
}
ItemStorage
인터페이스 구현하기
- 인벤토리와 가방 아이템의 공통 메서드를 생각하며 구현했다.
- 아이템을 저장하거나 뺄 수 있다.
- 모든 아이템의 가격을 구할 수 있는 메서드도 넣었다.
public interface ItemStorage {
void addItem(Item item);
void removeItem(Item item);
int getAllPrice();
}
DefaultItemStorage
추상 클래스 구현하기
ItemStorage
를 상속한 추상 클래스로 공통 필드를 생각하며 구현했다.
public abstract class DefaultItemStorage implements ItemStorage{
private ArrayList<Item> items = new ArrayList<>();
@Override
public void addItem(Item item) {
items.add(item);
}
@Override
public void removeItem(Item item) {
items.remove(item);
}
@Override
public int getAllPrice() {
return items.stream().mapToInt(Item::getPrice).sum();
}
}
Inventory
클래스 구현하기
- 인벤토리는
DefaultItemStorage
에 있는 필드와 메서드로 충분하여 상속만 받았다.
public class Inventory extends DefaultItemStorage {
}
ItemBag
클래스 구현하기
ItemBag
은Item
의 속성과ItemStorage
속성이 모두 필요하다.
public class ItemBag extends DefaultItemStorage implements Item {
private final String name;
private final int price;
public ItemBag(int price, String name) {
this.name = name;
this.price = price;
}
@Override
public int getPrice() {
return this.getAllPrice() + this.price;
}
@Override
public String getName() {
return this.name;
}
}
Client
코드로 테스트하기
- 원하는 요구사항대로 잘 동작한다.
Client
에 정의한getPrice()
정적 메서드는Component
에 해당하는Item
인터페이스를 구현했다면, 무엇이든 가격을 구할 수 있다.ItemBag
과 같은Composite
와Item
과 같은Component
를 동일선상에서 보는 것이 포인트이다.
getPrice()
메서드는Leaf
인DefaultItem
과Composite
인ItemBag
을 동일선상에서 보고 있다.ItemBag
에는 많은Item
이 중첩되어 있을 수 있음에도 동일하게 볼 수 있다.- 클라이언트는 내부에 몇개의 중첩이 있는지 복잡한 사실을 알 필요 없다는 것이 중요하다.
DefaultItem
이든ItemBag
이든 팔면 얼마가 나오냐가 클라이언트의 중요한 관심사이다.
- 아무리 중첩되어 있어도 클라이언트는
inventory.getAllPrice()
메서드 한번에 인벤토리 내부 모든 아이템의 가격을 알 수 있다.
public class Client {
public static void main(String[] args) {
Inventory inventory = new Inventory();
Item longSword = new DefaultItem(350, "긴 검");
inventory.addItem(longSword);
ItemBag beginnerBag = new ItemBag(100, "모험자의 가방");
Item rareSword = new DefaultItem(400, "레어 검");
Item uniqueSword = new DefaultItem(1000, "유니크 검");
beginnerBag.addItem(rareSword);
beginnerBag.addItem(uniqueSword);
inventory.addItem(beginnerBag);
System.out.println("롱소드의 가격: " + getPrice(longSword)); // 롱소드의 가격: 350
System.out.println("모험자의 가방과 내부 아이템들의 가격: " + getPrice(beginnerBag)); // 모험자의 가방과 내부 아이템들의 가격: 1500
System.out.println("인벤토리 아이템 가격의 총 합계: " + inventory.getAllPrice()); // 인벤토리 아이템 가격의 총 합계: 1850
}
public static int getPrice(Item item) {
return item.getPrice();
}
}
최종 다이어그램 살펴보기
Item
이Component
가 되었다.ItemBag
이Composite
에 해당한다.DefaultItem
은Leaf
에 해당한다.- 클라이언트는 셋 모두
Item
인터페이스의 관점에서 바라보기 때문에 그룹 전체와 객체를 동일하게 처리할 수 있게 된다.
컴포지트 패턴의 장단점
장점
- 복잡한 트리 구조를 편리하게 사용할 수 있다.
- 다형성과 재귀를 활용할 수 있다.
- 클라이언트 코드를 변경하지 않고,
Component
의 집합인Composite
도 이용할 수 있다.
단점
- 트리 자료구조가 어울리는 경우엔 자연스럽게 사용할 수 있지만, 트리 구조가 어울리지 않는 경우엔 지나치게 일반화해야 할 수도 있다.
- 이 부작용으로 런타임에 타입을 체크해야 되는 경우도 생길 수 있다. (이 때는 한번쯤 설계가 적당한지 의심해봐야 한다.)
자바와 스프링의 컴포지트 패턴
자바의 스윙 라이브러리
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);
}
}
- 스윙에서는 모든 UI 요소를
Component
라는 인터페이스로 규정하고 있다. JFrame
은 화면 프레임으로Component
UI 를 받을 수 있다.JTextField
나JButton
모두Component
여서JFrame
에 붙일 수 있다.- UI 에서 제공하는 공통 operation 을
Component
내부 동작에 모아놓았다. JFrame
이Composite
에 해당하고,JButton
과JTextField
를Leaf
로 볼 수 있다.
스프링의 ApplicationContext
ApplicationContext
에서는BeanFactory
인스턴스를 트리 구조로 제공한다.BeanFactory
인터페이스는 각각의 빈을 접근하고 관리하는 핵심 인터페이스이다.ApplicationContext
에서는getParent()
메서드를 사용해 컨텍스트의 계층구조를 제공한다.- 이로 인해 복잡한 스프링 애플리케이션의 관리와 모듈화를 더 쉽게 만든다.
AbstractApplicationContext
클래스가 컴포지트 패턴의 핵심이다.- 처음에 로컬 컨텍스트에서 빈을 찾는다.
- 만일 빈이 발견되지 않는 경우 부모 컨텍스트에 요청을 위임한다.
- 빈이 발견되거나 최상위 컨텍스트에 닿을 때까지 계속된다.
- Sring Boot 의 Web Application 이라면,
AnnotationConfigServletWebServerApplicationContext
내부에ApplicationContext
의 인스턴스들이 계층구조를 이루고 있으며,DefaultListableBeanFactory
와 같은BeanFactory
가 각 컨텍스트의 빈을 관리하는 것을 도와준다.
반응형
'Java > 자바 디자인 패턴' 카테고리의 다른 글
퍼사드 패턴 (Facade Pattern) 이란? (0) | 2023.04.18 |
---|---|
데코레이터 패턴 (Decorator Pattern) 이란? (0) | 2023.02.25 |
브릿지 패턴 (Bridge Pattern) 이란? (0) | 2023.02.19 |
자바 디자인 패턴, 객체 생성 관련 패턴 (Object Creational Patterns) 이란? (0) | 2023.02.17 |
어댑터 패턴 (Adapter Pattern) 이란? (1) | 2023.01.29 |