2021. 10. 15. 18:58ㆍComputer Sciences/Design Patterns
문제
이터레이터 패턴에서 사용한 예시를 다시 써보자. 두 가게를 합병한 후로 장사가 탄탄대로를 달려서 이젠 점심 메뉴에 디저트 메뉴도 넣자는 기획이 들어왔다. 즉 점심 메뉴 안에 디저트 메뉴가 포함되는 것이다. 이러한 구조는 어떤 자료구조를 사용해야 할까? 메뉴 안에 메뉴... 트리 구조가 적당할 것 같다. 그림을 그려보면 대충 다음과 같다.
기존의 이터레이터 패턴을 유지하면서 모든 메뉴를 순회하도록 변경할 수 있을까? 정답부터 말하자면 그렇다. 컴포지트 패턴을 적용하면 이를 해결할 수 있다.
컴포지트 패턴
객체들을 트리 구조로 구성하여 부분과 전체를 나타내는 계층 구조로 만드는 패턴이다. 이 패턴을 이용하면 클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체를 똑같은 방법으로 다룰 수 있다. 이번 예제에서는 Menu
를 순회하면서 MenuItem
에 접근하다가 Menu
가 나오면 그 Menu
를 다시 순회하며 MenuItem
을 접근하는 방식으로 흘러간다. 이를 일반화하여 클래스 다이어그램으로 나타내면 다음과 같다.
이를 우리 프로젝트에 적용하면 다음과 같다.
Waitress
MenuComponent
인터페이스를 이용하여Menu
와MenuItem
에 모두 접근하게 된다.
MenuComponent
MenuComponent
는MenuItem
과Menu
모두에 적용되는 인터페이스를 나타낸다. 여기에서는 기본 메서드를 정의하기 위해 인터페이스가 아닌 추상 클래스를 사용했다.
MenuItem
MenuItem
에서 쓰일 메서드들은 필요에 맞게 오버라이딩하고 나머지는 주어진 구현을 그대로 사용한다.
Menu
Menu
에서 쓰일 메서드들은 필요에 맞게 오버라이딩하고 나머지는 주어진 구현을 그대로 사용한다.
이제 코드로 옮겨보자.
public abstract class MenuComponent {
public void add(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public void remove(MenuComponent menuComponent) {
throw new UnsupportedOperationException();
}
public MenuComponent getChild(int i) {
throw new UnsupportedOperationException();
}
public String getName() {
throw new UnsupportedOperationException();
}
public String getDescription() {
throw new UnsupportedOperationException();
}
public double getPrice() {
throw new UnsupportedOperationException();
}
public boolean isVegetarian() {
throw new UnsupportedOperationException();
}
public void print() {
throw new UnsupportedOperationException();
}
}
public class MenuItem extends MenuComponent {
String name;
String description;
boolean vegetarian;
double price;
public MenuItem(String name, String description,
boolean vegetarian, double price) {
this.name = name;
this.description = description;
this.vegetarian = vegetarian;
this.price = price;
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public boolean isVegetarian() {
return vegetarian;
}
@Override
public double getPrice() {
return price;
}
@Override
public void print() {
System.out.print(" " + getName());
if (isVegetarian()) {
System.out.print("(v)");
}
System.out.println(", " + getPrice());
System.out.println(" -- " + getDescription());
}
}
public class Menu extends MenuComponent {
ArrayList<MenuComponent> menuComponents = new ArrayList<>();
String name;
String description;
public Menu(String name, String description) {
this.name = name;
this.description = description;
}
@Override
public void add(MenuComponent menuComponent) {
menuComponents.add(menuComponent);
}
@Override
public void remove(MenuComponent menuComponent) {
menuComponents.remove(menuComponent);
}
@Override
public MenuComponent getChild(int i) {
return (MenuComponent) menuComponent.get(i);
}
@Override
public String getName() {
return name;
}
@Override
public String getDescription() {
return description;
}
@Override
public void print() {
System.out.print("\n" + getName());
System.out.println(", " + getDescription());
System.out.println("--------------------------");
Iterator<MenuComponent> iterator = menuComponents.iterator();
while (iterator.hasNext()) {
MenuComponent menuComponent = iterator.next();
menuComponent.print();
}
}
}
거의 다 끝났다. 이제 마지막으로 우리 웨이트리스 코드를 만들자.
public class Waitress {
MenuComponent allMenus;
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}
public void printMenu() {
allMenus.print();
}
}
이를 동작시킬 테스트 코드를 빠르게 짜보자. 중간에 디저트 메뉴가 추가됨을 확인하기 위해 저녁 메뉴도 간단하게 넣어서 테스트하자.
public class MenuTest {
public static void main(String[] args) {
MenuComponent pancakeHouseMenu = new Menu("팬케이크 하우스 메뉴", "아침 메뉴");
MenuComponent dinerMenu = new Menu("점심 식당 메뉴", "점심 메뉴");
MenuComponent cafeMenu = new Menu("카페 메뉴", "카페 메뉴");
MenuComponent dessertMenu = new Menu("디저트 메뉴", "맛있는 디저트");
MenuComponent allMenus = new Menu("전체 메뉴", "전체 메뉴");
allMenus.add(pancakeHouseMenu);
allMenus.add(dinerMenu);
allMenus.add(cafeMenu);
pancakeHouseMenu.add(new MenuItem(
"팬케이크 세트 A", "우리 가게 대표 메뉴", true, 2.99));
pancakeHouseMenu.add(new MenuItem(
"팬케이크 세트 B", "두 번째로 출시한 팬케이크 세트 메뉴", true, 2.99));
pancakeHouseMenu.add(new MenuItem(
"레귤러 팬케이크 세트", "달걀 후라이와 소시지가 들어가 있는 팬케이크", false, 3.19));
dinerMenu.add(new MenuItem(
"점심 세트 A", "우리 가게 대표 메뉴", true, 2.95));
dinerMenu.add(new MenuItem(
"점심 세트 B", "두 번째로 출시한 점심 세트 메뉴", true, 2.95));
dinerMenu.add(new MenuItem(
"핫도그 세트", "양파, 치즈, 많은 양념이 들어간 핫도그", false, 3.15));
cafeMenu.add(new MenuItem(
"아메리카노", "원두의 향이 느껴지는 아메리카노", true, 1.29));
dinerMenu.add(dessertMenu);
dessertMenu.add(new MenuItem(
"애플 파이", "바삭바삭한 애플 파이", true, 1.59));
Waitress waitress = new Waitress(allMenus);
waitress.printMenu();
}
}
실행한 결과는 다음과 같다.
단일 책임 원칙을 위반한 것 아닌가?
맞다. 컴포지트 패턴은 단일 책임 원칙을 깨면서 투명성을 확보하기 위한 패턴이라고 할 수 있다. 투명성이란 뭘 뜻할까? Component
인터페이스에 자식들을 관리하기 위한 기능과 리프 노드로써의 기능을 전부 집어넣음으로써 클라이언트에서 복합 객체와 리프 노드를 똑같은 방식으로 처리할 수 있도록 한다. 어떤 원소가 복합 객체인지 리프 노드인지가 클라이언트 입장에서는 투명하게 느껴지는 것이다.
Component
클래스에는 두 종류의 기능이 모두 들어가 있으므로 안정성은 조금 떨어진다. 사용하지 말아야 할 기능을 호출할 수도 있기 때문이다. 이러한 문제는 설계 결정 사항에 포함된다. 안정성 측면을 고려한다면 여러 역할로 분리해서 다른 인터페이스로 구현할 수도 있다. 하지만 이렇게 설계한다면 각 인스턴스를 확인하기 위해 조건문 또는 instanceof
와 같은 코드가 추가될 것이다. 아니면 우리가 한 것처럼 예외를 던질 수도 있다.
질문으로 돌아와보면 지적한 내용은 상황에 따라 원칙을 적절하게 사용해야 한다는 것을 알 수 있다. 원칙은 지켰을 때 더 좋은 가이드라인 정도인 것이지 반드시 지켜야 하는 법칙이 아니다. 많은 경우에는 원칙을 따르는 편이 더 좋으나 항상 그 원칙이 우리가 설계하고 있는 디자인에 어떤 영향을 끼칠지 생각해보아야 한다.
'Computer Sciences > Design Patterns' 카테고리의 다른 글
13. Bridge Pattern (0) | 2021.11.14 |
---|---|
12. State Pattern (0) | 2021.10.15 |
10. Iterator Pattern (0) | 2021.10.15 |
9. Template Method Pattern (0) | 2021.10.14 |
8. Facade Pattern (0) | 2021.10.14 |