[DesignPattern] 컴포지트 (Composite)
29 Sep 2023컴포지트 (Composite)
부분과 전체의 계층을 표현하기 위해 객체들을 모아 트리 구조로 구성합니다. 사용자로 하여금 개별 객체와 복함 객체를 모두 동일하게 다룰 수 있도록 하는 패턴 입니다.
동기
그래픽 편집기나 구조도 캡처 시스템 같은 그래픽 응용프로그램을 살펴보면, 사룔자가 간단한 그림 구성요소들을 모아서 복잡한 다이어그램을 생성할 수 있게 한다.
사용자는 더 큰그림 요소를 만들기 위해 구성요소들을 그룹으로 만들고 이 그룹이 다시 더 큰 그룹을 형성하기도 한다. 이를 구현하기 위한 간단한 방법은 텍스트, 라인 등 간단한 기본 그림 요소에 대한 클래스들을 정의하고, 이를 포함한 컨테이너로 동작하는 클래스를 추가로 정의하는 것이다.
하지만 이 접근법에는 문제가 있다. 각각의 그림에 해당하는 클래스들과 컨테이너 클래스들을 구분하기 위해 클래스의 속성으로 크기, 색깔 위치 등의 기본 속성 이외의 코드에 해당하는 속성을 정의해야한다.
이를 위해 컴포지트 패턴의 가장 중요한 요소는 기본 클래스와 이들의 컨테이너를 모두 표현할 수 있는 하나의 추상화 클래스를 정의하는 것이다.
활용성
복합체 패턴은 다음과 같은 경우에 사용합니다.
- 부분 - 전체의 객체 계통을 표현하고 싶을 때
- 사용자가 객체의 합성으로 생긴 복합 객체와 개개의 객체 사이의 차이를 알지 않고도 자기 일을 할 수 있도록 만들고 싶을 때. 사용자는 복합 구조의 모든 객체를 똑같이 취급하게 됩니다.
구조
전형적인 Composite 객체 구조는 다음과 같습니다.
- Component: 집합 관계에 정의될 모든 객체에 대한 인터페이스를 정의합니다. 모든 클래스에 해당하는 인터페이스에 대해서는 공통의 행동을 구현합니다. 전체 클래스에 속한 요소들을 관리하는 데 필요한 인터페이스를 정의합니다.
- Leaf: 가장 말단의 객체, 즉 자식이 없는 객체를 나타냅니다. 객체 합성에 가장 기본이 되는 객체의 행동을 정의합니다.
- Composite: 자식이 있는 구성요소에 대한 행동을 정의합니다. 자신이 복합하는 요소들을 저장하면서, Component 인터페이스에 정의된 자식 관련 연산을 구현합니다.
- Client: Component 인터페이스를 통해 복합 구조 내의 객체들을 조작합니다.
결과
컴포지트 패턴으로 발생하는 결과는 다음과 같다.
- 기본 객체와 복합 객체로 구성된 하나의 일관된 클래스 계통을 정의합니다. 사용자 코드는 일반화된 상위 개념의 객체를 조작하는 방식으로 프로그래밍 하면, 런타임 기본 객체와 복합 객체를 구분하지 않고 일관되게 프로그래밍할 수 있게 됩니다.
- 사용자 코드는 복합 구조이나 단일 객체와 동이하게 다루는 코드로 작성되기 때문에 코드가 단순해 집니다. ex) case 스타일의 함수가 필요가 없어지므로 코드가 단순해집니다.
- 새롭게 정의된 Composite나 Leaf의 서브클래스들은 기존에존재하는 구조들과 독립적으로 동작이 가능하게 되므로 새로운 종류의 구성요소를 쉽게 추가할 수 있다.
- 새로운 요소를 쉽게 추가할 때의 단점은 복합체의 구성요소에 제약을 가하기 힘들다 이는 설계가 지나치게 많은 범용성을 가지게 된다.
장점과 단점
장점
- 복잡한 트리 구조를 편리하게 사용할 수 있다.
- 다형성과 재귀를 활용할 수 있다.
- 클라이언트 코드를 변경하지 않고 새로운 엘리먼트 타입을 추가할 수 있다.
단점
- 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야 하기 때문에) 지나치게 일반화 해야 하는 경우도 생길 수 있다.
구현
이제 컴포짓 패턴을 구현해보자. 게임을 예시로 들어서 구현을 해보도록 하자.
아이템의 가격정보와 가방안에 있는 아이템들에 대한 가격정보를 조회하는 기능을 구현해보려고 한다.
적용전
먼저 Item
클래스는 아이템의 이름과 가격정보를 가지고 있는 클래스이다.
Item
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;
}
}
Bag
클래스의 경우 가방 안에 들어있는 Item
객체들의 총 아이템 정보를 조회하는 기능이 구현되어 있다.
Bag
public class Bag {
private List<Item> items = new ArrayList<>();
public void add(Item item) {
items.add(item);
}
public List<Item> getItems() {
return items;
}
}
만약 Client에서 아이템의 가격을 조회하는 것과 가방안에 있는 아이템들의 가격을 조회할 때는 아래와 같이 구현될 것이다.
Client
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);
}
}
차이가 보이는가? 동일하게 printPrice 기능을 써서 가격을 출력하는 것은 똑같지만, 하나는 가방 또하나는 아이템에 대한 가격정보이니 내부에서 돌아가는 로직이 다르다.
컴포짓 패턴을 사용하면, 동일한 행위에 대한 인터페이스를 구현하여 행위를 묶어줄 수 있다.
적용후
먼저 공통적인 행위에 대한 기능을 묶어놓는 Component
인터페이스를 생성한다.
여기서는 공통된 행위를 가격을 조회한다. 라는 행위를 공통으로 묶어 컴포짓 패턴을 적용해보고자 한다.
위의 경우 가격을 가져오는 행위를 동일하게 볼 수 있으므로 getPrice
를 묶은 Component
인터페이스를 생성해준다.
Component
public interface Component {
int getPrice();
}
이제 Item
에서는 기존의 가격을 가져오는 메소드를 Component
에서 선언한 공통된 행위인 getPrice
메소드로 교체해준다.
동작은 동일할 지라도, 공통된 행위인 Component
인터페이스로 묶어주는 방법이라 생각하면 된다.
Item
public class Item implements Component {
private String name;
private int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
@Override
public int getPrice() {
return this.price;
}
}
이제 Bag을 수정해줄 차례인데, Bag
에는 Item
의 데이터를 가지고 있다.
여기서 Item
클래스는 컴포짓 패턴의 Leaf
구조 나타내는데 컴포짓 패턴을 사용할 경우엔 Leaf
의 타입을 참조해주면 안되고 Component
의 타입을 참조해 주어야 한다.
public class Bag implements Component {
private List<Component> components = new ArrayList<>();
public void add(Component component) {
components.add(component);
}
public List<Component> getComponents() {
return components;
}
@Override
public int getPrice() {
return components.stream().mapToInt(Component::getPrice).sum();
}
}
기존의 Item
타입 이 Component
타입으로 교체된것을 확인할 수 있다.
그리고 Item
과 동일하게 Component
인터페이스를 구현하여 동일한 행위인 가격 조회 기능을 재정의 해준다.
그러면 이제 Client에서는 각각 Item과 Bag에 대한 가격 조회 로직을 따로 구현할 필요 없이 하나의 메소드로 다른 타입의 동일한 행위 정보를 가져올 수 있게 된다.
Client
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(Component component) {
System.out.println(component.getPrice());
}
}
여기서 printPrice
메소드는 Component라는 타입을 쓰고 있기 때문에 새로운 종류의 Component가 생겨도 클라이언트 코드는 변경되지 않는다.
이것은 OOP의 SOLID 법칙중 개방 폐쇄 원칙(Open-Closed Principle)으로 볼 수 있다.