구조 관련 : Composite 패턴
- 디자인 패턴
- 2023. 11. 30.
들어가기 전
이 글은 인프런 백기선님의 GOF 강의를 복습하며 작성한 글입니다.
Composite 패턴
- GOF : 클라이언트가 그룹 전체 / 개별 객체를 동일하게 처리할 수 있는 패턴
- 클라이언트 입장에서는 어떤 객체가 전체 / 부분인지 모름 (계층 트리의 Root, Leap Node인지 구별 X)
- Composite 패턴은 'Tree 구조'를 구성해야 한다는 제약 사항이 있음.
- 디자인 Component
- Component : 트리 구조를 추상화 함.
- Composite : 그룹을 표현할 수 있는 객체임. (Root Node의 개념)
- 어떤 액션을 공통으로 제공할지 고려해야함.
- 내부적으로 Component 배열을 가짐. (타입은 Leaf가 아니라 반드시 Component임)
- 클라이언트는 Component 인터페이스에만 의존함. (Component 하위에 Composite, Leaf가 있음)
- Component 하위 클래스로 Composite, Leaf를 구성해야 함.
- Item, Bag 같은 것들이 Composite, Leaf 중에 하나가 될 수 있음.
디자인 패턴이 필요한 코드
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);
}
}
이 클라이언트 코드는 Composite 패턴을 이용하면 코드를 개선할 수 있는 형태다.
- Item은 가격이 얼마인지 제공해주고, Bag에는 여러 Item을 넣을 수 있음.
- 클라이언트는 Bag에 있는 Item 전체의 값이 궁금할 수도 있음.
이 경우, printPrice(Item item), printPrice(Bag bag) 같은 메서드들을 계속 추가해야 할 것이다. 즉, Item이 추가될 때 마다 기존 코드에 변경점이 발생할 것이다.
한 가지 더 고려해야할 부분은 printPrice(Bag bag) 메서드가 클라이언트에 있는 것이 맞는지다. 왜냐하면 클라이언트가 이 부분을 처리하기에는 고려해야 할 부분이 너무 많기 때문이다.
- Bag 안에 Bag이 있는 경우가 있을 수도 있음. (클라이언트가 이렇게 자세한 경우도 고려해야 할까?)
- Bag 안에 있는 전체 Item의 값을 왜 클라이언트가 구해야 하는 것일까? (대부분의 필드는 Bag 안에 있음)
이런 두 가지 문제들은 Bag의 캡슐화가 깨져서 클라이언트가 많은 부분을 알아야 하기 때문에 발생한다. Composite 패턴을 이용해서 캡슐화해서 대응해 볼 수 있다. Bag, Item에 대한 계층 구조로 만들고, 클라이언트는 최상위 인터페이스에만 의존해서 이런 문제들에서 해방되는 것이다.
디자인 패턴 적용하기
Composite 디자인 패턴을 적용하기 위해서는 Component 인터페이스, Composite, Leaf 노들을 먼저 정의해야한다.
- Component 인터페이스 : getPrice()만 제공한다.
- Composite : Bag 클래스가 된다. Component를 구현한다. Composite는 내부에 Component []을 가진다. (절대로 리프노드 타입이 아님)
- Leaf : Item 클래스가 된다. Component를 구현한다.
Component 인터페이스는 getPrice() 메서드만 제공을 하는데, 이것은 이전 코드에서 Bag + Item의 가격을 구하는데 공통적으로 getPrice() 메서드를 이용했기 때문에 추상화하는 것이다.
private void printPrice(Component component) {
System.out.println(component.getPrice());
}
클라이언트는 이전에는 구체적인 타입 (Item, Bag)에 의존했으나 여기서부터는 최상위 인터페이스인 Component에만 의존하도록 수정한다.
// 최상위 인터페이스 : Component
public interface Component {
int getPrice();
}
// Composite으로 동작
public class Bag implements Component {
private final List<Component> items = new ArrayList<>();
public void add(Component item) { this.items.add(item); }
@Override
public int getPrice() {
return this.items.stream()
.mapToInt(Component::getPrice)
.sum();
}
}
// Leaf 노드 구현
public class Item implements Component{
private final String name;
private final int price;
public Item(String name, int price) {
this.name = name;
this.price = price;
}
@Override public int getPrice() { return this.price; }
}
다음 코드로 Component 인터페이스를 최상위로 가지는 트리 구조를 만들 수 있다.
- Composite을 담당하는 Bag은 내부적으로 Component 배열을 가지고 있다. (Root 노드의 개념)
- Item, Bag은 동일하게 Component 인터페이스를 구현함.
기존에는 Bag에 있는 Item의 총합을 구할 때, 클라이언트가 직접 계산을 했어야했고 이 부분은 캡슐화가 깨진 것이다. 그러나 이렇게 코드를 구현해두면 Bag이 가진 Item 가격의 합을 구하는 로직이 클라이언트로부터 캡슐화된다. 즉, 클라이언트는 지나치게 많은 것들을 알지 못해도 괜찮아진다.
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) 메서드만 사용하는데, Bag + Item이 공통된 인터페이스인 Component를 통해서 getPrice()를 제공한다. 그리고 이 getPrice()에서 각 Component의 가격을 구하는 것이 캡슐화 되었기 때문에 클라이언트는 Component 객체에게 위임하기만 하면 된다.
Composite 패턴의 장/단점
- 장점
- 복잡한 트리 구조를 편리하게 사용할 수 있음. (클라이언트는 트리의 Head / Leaf인지 구별하지 않아도 됨)
- 다형성 + 재귀를 활용하기 좋음.
- 클라이언트 코드를 변경하지 않고 새로운 Element(Leaf Node / Composite)를 추가할 수 있음.
- 단점
- 트리를 만들어야 하기 때문에 (공통된 인터페이스를 정의해야하기 때문에, 예를 들면 getPrice()) 지나치게 억지로 일반화 해야하는 경우도 생길 수 있음.
클라이언트가 최상위 인터페이스 Component를 참조하기 때문에 클라이언트 코드를 변경하지 않고 새로운 Element(Leaf Node, Composite)을 추가할 수 있다는 장점이 있다.
반면 트리구조를 만들기 위해 지나친 일반화가 발생할 수 있는데, 이 경우는 적용하면 안되는 곳에 디자인 패턴을 적용하고 있다는 신호일 수 있다. 지나친 일반화의 냄새로는 트리 구조 내에서 런타임에 타입을 체크하는 경우가 있을텐데 이런 경우면 지나친 일반화를 하고 있다고 이해할 수 있다. 예를 들어 isInstance(Bag, Item) 같은 것들을 사용하는 순간들이다.
'디자인 패턴' 카테고리의 다른 글
행동 관련 : 중재자 패턴 (Mediator Pattern) (0) | 2023.12.02 |
---|---|
행동 관련 : 커맨드 패턴 (0) | 2023.12.02 |
행동 관련 : Strategy(전략) 패턴 (0) | 2023.11.30 |
행동 관련 : State 패턴 (0) | 2023.11.30 |
행동 관련 : 템플릿 메서드 패턴 (0) | 2023.11.30 |