헤드퍼스트 디자인패턴: 반복자 패턴과 컴포지트 패턴

Joo Hee Paige Kim
13 min readNov 15, 2022

2022.11.15

반복자 패턴

  • 컬렉션의 구현 방법을 노출하지 않으면서 집합체 내의 모든 항목에 접근하는 방법을 제공
  • 각 항목에 일일이 접근할 수 있게 해주는 기능을 집합체가 아닌 반복자 객체가 책임진다는 장점
  • 집합체 인터페이스와 구현이 간단해지게 됨

객체 마을 식당과 팬케이크 하우스 합병

  • 팬케이크 하우스에서 파는 아침메뉴와 객체 마을 식당에서 파는 점심 메뉴를 합치는데,
  • 팬케이크 하우스는 ArrayList를 활용하고, 객체 마을 식당은 배열을 이용해서 메뉴를 저장하고 있음
  • 메뉴 구현 방식이 달라 메뉴를 출력하고, 구별하는 Waitress 클래스를 만드는데 어려움 발생
  • Solution: 메뉴에 Iterator 클래스 생성 및 적용

팬케이크하우스와 객체 마을 식당 메뉴에 대해 사용할 수 있는 구상 Iterator 클래스 생성 및 적용

// Iterator 인터페이스 정의
public interfaceIterator {
boolean hasNext();
MenuItem next();
}

// Iterator 인터페이스 구현
public class DinerMenuIterator implements Iterator {
MenuItem[] items;
int position = 0; // 반복 작업이 처리되고 있는 위치를 저장

// 생성자는 반복 잔업을 수행할 메뉴 항목 배열을 인자로 받아들임
public DinerMenuIterator(MenuItem[] items){
this.items = items;
}

// next() 메소드는 배열의 다음 원소를 리턴하고 position 변수값을 증가
public MenuItem next() {
MenuItem menuItem = items[position];
position += 1;
return menuItem;
}
// hasNext() 메소드는 배열에 있는 모든 원소를 돌았는지 확인한 다음,
// 더 돌아야할 원소가 있는 지 확인
// 객체마을 주방장이 정한 배열 끝에 있는 지 확인
public boolean hasNext() {
if(position =› items.length || items[position] = null) {
return false;
}
return true;
}
}

public class DinerMenu {
static final int MAX_ITEMS = 6;
int numOfItems = 0;
MenuItem[] menuItems;
// createIterator() 메소드는 menuItems배열을 가지고
// DinerMenuIterator를 생성한 다음 클라이언트에게 리턴
public Iterator createIterator(){
return DinerMenuIterator(menuItems);
}
}

종업원 코드 수정

public class Waitress{
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;

// 생성자에서 두 메뉴를 인자로 받아옴
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}

public void printMenu(){
// printMenu() 메소드에서 2개의 반복자를 생성
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();

// 각 반복자를 가지고 오버로드 된 printMenu() 메소드 호출
printMenu(pancakeIterator);
printMenu(dinerIterator);
}

private void printMenu(Iterator iterator){
// 오버로드 된 pirntMenu() 메소드는 반복자를 써서 모든 메뉴 항목에 접근해서 그 내용을 출력
// 기존엔 ArrayList와 배열 타입에 맞춰 반복문을 두번 돌려야했지만 iterator 인터페이스를 사용해 한 번만 순환하게 됨
while(iterator.hasNext()){
MenuItem menuItem = (MenuItem)iterator.next();
System.out.println(menuItem.getName());
...
}
}
}

인터페이스 개선하기

  • 자바에서 제공하는 Iterator(java.util.Iterator) 사용하도록 수정
  • ArrayList에 반복자를 리턴하는 iterator()메소드가 있기 때문에, 구현 필요 없음
  • 메뉴 클래스 추상화 처리해주기
// 팬케이크 하우스
public Iterator<MenuItem> createIterator(){
// 반복자를 직접 만드는 대신 menuItems ArrayList의 iterator() 메소드만 호출
return menuItems.iterator();
}

// DinerMenuIterator Class
import java.util.Iterator; // 구문 추가
public class DinerMenuIterator implements Iterator{
MenuItem[] items;
int position = 0;
...

// Iterator 인터페이스에서 remove() 메소드는 필수가 아님
public void remove(){
throw new UnsupportedOperationException("메뉴 항목은 지우면 안 됩니다.");
}
}
  • 메뉴 인터페이스를 통일하고, 종업원 코드 수정
// Menu Interface를 정의
public interface Menu{
public Iterator createIterator();
}

// Waitress 클레스도 java.util.Iterator 사용
import java.util.Iterator;
public class Waitress{
**Menu** pancakeHouseMenu;
**Menu** dinerMenu;
// 구상 메뉴 클래스를 Menu Interface로 바꿔주기
public Waitress(**Menu** pancakeHouseMenu, **Menu** dinerMenu) (
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
...

}

Conclusion

  • 각자 쓰던 코드를 수정하지 않고도 통합적인 처리가 가능해짐
  • 각 메뉴 구현법이 캡슐화됨 — Waitress입장에선 메뉴 항목의 컬렉션이 무슨 타입인지 알 수 없음
  • Iterator만 구현한다면 어떤 컬렉션이든 한 개의 순환문으로 처리가 가능
  • "특정 구현이 아닌 인터페이스에 맞춰 프로그래밍한다"는 원칙을 지켜 Waitress 클래스와 구상 메뉴 클래스 간 의존성 감소

단일 역할 원칙

  • 어떤 클래스가 바뀌는 이유는 하나뿐이어야 한다.
  • 어떤 클래스에서 맡고 있는 모든 역할은 나중에 코드 변화를 불러올 수 있음.
  • 역할이 2개 이상 있으면 바뀔 수 잇는 부분이 2개 이상 됨.
  • 응집도
  • 한 클래스 또는 모듈이 특정 목적이나 역할을 얼마나 일관되게 지원하는 지를 나타내는 척도
  • 높으면, 서로 연관된 기능이 묶여있다는 뜻
  • 낮으면, 서로 상관 없는 기능들이 묶여있다는 뜻

컴포지트 패턴

  • 객체를 트리구조로 구성해서 부분-전체 계층구조(part-whole hierarchy)를 구현
  • 클라이언트를 단순화시킬 수 있음

고려해야될 점

  1. 자식의 순서 ⇒ 추가 제거 할 때, 더 복잡한 관리 방법을 사용해야 됨

2. 복합 구조가 너무 복잡하거나, 복합 객체 전체를 도는 데 너무 많은 자원이 필요하다면 복합 노드를 캐싱해두면 도움이 됨

메뉴, 종업원 코드 수정

MenuItem과 Menu에 모두 적용할 수 있는 MenuComponent 추상 클래스 생성

  • 기본적으로 UnsupportedOperationException을 던지도록 하여, 자기 역할에 맞지 않은 메소드는 오버라이드 하지 않고 기본 구현을 그대로 사용 가능
// MenuComponent에서는 모든 메소드를 기본적으로 구현
public abstract class MenuComponent{
// 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();
}

// MenuItem에서 작업을 처리하는 메소드이며, 이 중 몇 개는 Menu에서 쓸 수 있음
public String getName(){
throw new UnsupportedOperationException();
}
public String getDescription(){
throw new UnsupportedOperationException();
}
public double getPrice(){
throw new UnsupportedOperationException();
}
public boolean isVegetarian(){
throw new UnsupportedOperationException();
}

// Menu와 MenuItem에서 모두 구현하는 작업용 메소드
public void print(){
throw new UnsupportedOperationException();
}
}

메뉴 항목 구현

// MenuComponent 인터페이스를 확장
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;
}

// 기존 getter 메소드 동일
public String getName(){
return name;
}
public String getDescription(){
return description;
}
public double getPrice(){
return price;
}
public boolean isVegetarian(){
return vegetarian;
}

// MenuComponent 클래스에 있는 Print() 메소드를 오버라이드
// MenuItem이 이 메소드를 호출하면 메뉴에 수록해야할 내용이 출력
public void print(){
System.out.print(" " + getName());
if(isVegetarian()){
System.out.print("(v)");
}
System.out.print(", " + getPrice());
System.out.println(" --" + getDescription());
}

메뉴(복합 객체 클래스) 구현

// MenuItem과 동일하게 MenuComponent 상속
public class Menu extends MenuComponent{
// ArrayList에 저장
List<MenuComponent> menuComponents = new ArrayList<MenuComponent>();
String name;
String description;

// 전에는 다른 클래스를 사용했지만, 이번에는 메뉴마다 이름, 설명을 붙임
public Menu(String name, String description){
this.name = name;
this.description = description;
}

public void add(MenuComponent menuComponent){
menuComponents.add(menuComponent);
}
public void remove(MenuComponent menuComponent){
menuComponents.remove(menuComponent);
}
public void MenuComponent(int i){
return (MenuComponent)menuComponents.get(i);
}

// getPrice()와 isVegetarian()은 적합하지 않으므로 구현하지 않음
public String getName(){
..
}
public String getDescription(){
..
}

// Menu 클래스에선 해당 메뉴에 속하는 모든 서브Menu와 MenuItem을 출력해줘야함
// 반복자를 사용해 각 자식의 print() 호출
public void print(){
System.out.print("\\n" + getName());
System.out.println(", " + getDescription());
System.out.println("---------------------");

for (MenuComponent menuComponent: menuComponents) {
menuComponent.print();
}
}
}

종업원코드에 컴포지트 적용

public class Waitress {
MenuComponent allMenus;

// 다른 모든 메뉴를 포함하고 있는 최상위 메뉴 구성 요소만 넘겨주면 됨
public Waitress(MenuComponent allMenus) {
this.allMenus = allMenus;
}

//전체 계층구조를 출력하고 싶다면 최상위 메뉴의 print() 호출
public void printMenu() {
allMenus.print();
}
}

이렇게 되면 컴포지트 패턴에서 ‘계층 구조를 관리와 메뉴 관리’ 두가지 역할을 한 클래스에서 처리하게 됨

  • 컴포지트 패턴은 단일 역할 원칙을 깨면서 투명성을 확보하기 위한 패턴
  • Component 인터페이스에 자식들을 관리하는 기능과 잎으로써의 기능을 전부 넣어서 클라이언트가 복합 객체와 잎을 똑같은 방식으로 처리할 수 있음
  • 두 종류의 기능이 모두 들어있다 보니 안정성은 약간 떨어짐
  • 클라이언트가 어떤 원소를 대상으로 무의미하거나 부적절한 작업을 처리하려고 할 수 있음
  • 원칙을 적절하게 사용하기

--

--