본문 바로가기
SWE/C++ OOP

[디자인 패턴] Iterator Pattern 이터레이터 패턴

by S나라라2 2021. 10. 30.
반응형

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)만 알고 있으면 되고, 그 한 개의 순환문으로 어떤 컬렉션이든 처리할 수 있다.

정리하면, 이터레이터 패턴을 이용하면 내부적인 구현 방법을 외부로 노출시키지 않으면서도 집합체에 있는 모든 항목에 일일이 접근할 수 있다. 또한 각 항목에 일일이 접근할 수 있게 해주는 기능을 집합체가 아닌 반복자 객체에서 책임지게 된다는 것도 장점이다. 그러면 집합체 인터페이스 및 구현이 간단해진다.

반응형