Iterator Pattern 이터레이터 패턴
개념
예를 들어 객체들을 배열, 스택, 해시테이블 등의 컬렉션에 넣어서 보관할 수 있다. 그런데 클라이언트가 해당 객체들에게 일일이 접근하는 작업을 원할 수 있다.
이런 경우, 사용하는 것이 이터레이터 패턴이다. 객체를 저장하는 방식은 보여주지 않으면서도 클라이언트가 객체들에게 일일이 접근할 수 있게 해주는 방법이다.
이 패턴의 구현 방법은 반복 작업을 Iterator 인터페이스를 이용하여 캡슐화 하는 것이다.
예시
아침 메뉴를 파는 팬케이크 하우스와 저녁 메뉴를 판매하는 식당이 합병하려고 한다.
이 때, 서로 각자의 스타일로 메뉴를 구현해놨는데, 두 메뉴를 합쳐서 사용해야 한다. 두 메뉴를 합치는 방법에 대해 이야기 해보자.
참고로 팬케이크 하우스는 ArrayList를 이용하여 구현해놓았고, 식당은 정적 배열을 이용하여 구현해 놓았다.
두 메뉴를 합치는 것은 새로 뽑은 웨이트리스가 담당할 것이다.
웨이트리스 클래스의 업무는 아래와 같다.
웨이트리스 printMenu() - 메뉴에 있는 모든 항목을 출력 printBreakfastMenu() - 아침 식사 항목만 출력 printDinnerMenu() - 저녁 식사 항목만 출력 printVegetarianMenu() - 채식주의자용 메뉴 항목만 출력 isItemVegetarian(name) - 해당 항목이 채식주의자용이면 true를 리턴하고 그렇지 않으면 false를 리턴 |
먼저 각 음식점의 메뉴 구현 방법을 보자.
두 음식점들은 모두 menuItems 이라는 클래스를 사용하여 각 메뉴들을 만들었으나, menuItems를 저장하는 컬렉션이 다르다.
팬케이크 하우스는 arrayList에 담아놓아고, 식당은 정적 배열에 담아놓았다.
// PancakeHouseMenu
public class PancakeHouseMenu {
ArrayList menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList();
addItem("레큘러 팬케이크 세트",
"달걀 후라이와 소시지가 곁들여진 팬케이크",
true,
2.99);
addItem("블루베리 팬케이크",
"신선한 블루베리와 블루베리 시럽으로 만든 팬케이크",
true,
3.49);
...
}
public void addItem(String name, String description,
boolean vegetarian, double price)
{
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
menuItems.add(menuItem);
}
public ArrayList getMenuItems()
{
return menuItems;
}
}
// DinnerMenu
public class DinnerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinnerMenu(){
menuItems = new MenuItems[MAX_ITEMS];
addItem("BLT",
"통밀 위에 베이컨, 상추, 토마토를 얹은 메뉴", true, 2.99);
addItem("오늘의 스프",
"감자 샐러드를 곁들인 오늘의 스프", false, 3.29);
...
}
public void addItem(String name, String description,
boolean vegetarian, double price)
{
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if(numberOfItems >= MAX_ITEMS){
System.err.println("죄송합니다. 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");
} else{
menuItems[numberOfItems] = menuItem;
numberOfItems = numberOfItems + 1;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
웨이트리스 클래스에서 printMenu() 메소드를 구현하는 것을 상상해보자.
이런 경우, 웨이트리스 클래스에서는 두 음식점으로부터 각각getMenuItems를 호출하여 Items 에 접근한다.
두 음식점은 서로 다른 집합체를 사용하기 때문에 각 아이템을 접근하는 방식이 다르다. (arrayList.get(index) / array[index] )
따라서 통합된 인터페이스를 제공하여 웨이트리스 클래스(클라이언트)가 동일한 방식으로 각 아이템에 접근할 수 있게 해주기 위해 이터레이터 패턴을 적용하여 보자.
먼저 이터레이터 인터페이스는 다음과 같이 정의한다.
컬렉션에 다음 아이템이 있는지 여부 hasNext()를 확인하고, 다음 아이템을 next() 받아온다.
public interface Iterator {
boolean hasNext();
Object next();
}
저녁 메뉴를 판매하는 식당에 Iterator 적용하면 코드는 아래와 같아진다.
// DinnerMenu에서 사용할 수 있는 Iterator 클래스
public class DinnerMenuIterator implements Iterator
{
MenuItem[] items;
int position = 0;
public DinnerMenuIterator(MenuItem[] items)
{
this.items = items;
}
public Object next()
{
MenuItem menuItem = items[position];
position = position + 1;
return menuItem;
}
public boolean hasNext()
{
if( position >= items.length || items[position]==null )
{
return false;
}
else
{
return true;
}
}
}
// DinnerMenu식당에서 Iterator 사용하기
public class DinnerMenu
{
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
// 생성자
// addItem 메소드 호출
public Iterator createIterator()
{
return new DinnerMenuIterator(menuItems);
}
// 기타 메소드
}
위의 이터레이터 인터페이스를 각 식당에 적용한다. 이터레이터를 이용하여 다른 타입의 컬렉션을 순환한다.
그렇게되면 웨이트리스 클래스(client)에서는 각 식당이 배열인지, 스택인지, 리스트인지 알 필요 없이, iterator로 각 아이템에 접근하여 사용하면 된다.
아래의 그림과 같다.
위 다이어그램에서도 개선할 점이 있는데, 바로 웨이트리스 클래스(client)가 어떤 음식점들인지 각각 알 필요가 없다.
printMenu를 할 때는 이 음식점이 팬케이크집인지, 일반 식당인지 알 필요없이 단순히 음식점 중에 하나라고만 알고 메뉴들을 출력하면 된다.
따라서 PancakeHouse와 DinnerMenu는 Menu인터페이스를 상속받는다. 그리고 웨이트리스에서는 Menu만 가지고 있게 된다.
이처럼 객체지향, 디자인패턴의 원칙은 캡슐화이다. 클라이언트에서 알 필요없는 정보들은 과감하게 감싸준다.
위 다이어그램을 코드로 구현하면 아래와 같다.
코드
public class Waitress {
Menu pnacakeHouseMenu;
Menu dinnerMenu;
public Waitress (Menu pancakeHouseMenu, Menu dinnerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinnerMenu = dinnerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinnerIterator = dinnerMenu.createIterator();
System.out.println("메뉴\n---\n아침 식사");
printMenu(pancakeIterator);
System.out.println("\n저녁 식사");
printMenu(dinnerIterator);
}
private void printMenu(Iteraotr iterator) {
while ( iterator.hasNext() ){
MenuItem menuItem = (MenuItem)iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
}
반복자 코드를 웨이트리스에 적용했다.
먼저 생성자에서 두 메뉴를 인자로 받아오고, printMenu() 메소드에서 각 메뉴마다 하나씩 반복자를 생성한다.
각 반복자를 가지고 오버로드된 printMenu()메소드를 호추한다.
printMenu()메소드를 살펴보면, 반복자를 써서 모든 메뉴 항목에 접근하여 그 내용을 출력하고 있다.
반복자에서 항목이 더 남아있는지는 hasNext()를 호출하여 확인한다.
이터레이터 패턴의 장점
각 식당이 메뉴들을 어떻게 구현하였는지 웨이트리스 클래스(Client)에서는 알 필요가 없다. 즉 캡슐화가 잘 되어있다.
웨이트리스는 인터페이스(Iterator)만 알고 있으면 되고, 그 한 개의 순환문으로 어떤 컬렉션이든 처리할 수 있다.
정리하면, 이터레이터 패턴을 이용하면 내부적인 구현 방법을 외부로 노출시키지 않으면서도 집합체에 있는 모든 항목에 일일이 접근할 수 있다. 또한 각 항목에 일일이 접근할 수 있게 해주는 기능을 집합체가 아닌 반복자 객체에서 책임지게 된다는 것도 장점이다. 그러면 집합체 인터페이스 및 구현이 간단해진다.