Head First Design Patterns 책을 읽으며 정리한 내용입니다. 문제가 될 시 글을 내리도록 하겠습니다!
Iterator Pattern 이란?
Iterator Pattern - 컬랙션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는 모든 항목에 접근할 수 있게 해주는 방법을 제공
이 패턴을 사용하면 컬랙션 내부에서 어떤 방식으로 일처리가 되는지 전혀 모르는 상태에서 그 안의 모든 원소들에 접근할 수 있게됩니다.
컬랙션 객체안에 들어있는 모든 원소들에 대한 접근방식이 공통되어있다면 어떤 종류의 컬랙션 에서도 사용할 수 있는 다형적인 코드를 만들 수 있기 때문이죠!
또한 반복자(Iterator)를 활용하면 모든 항목에 접근하는 일을 컬랙션 객체가 아닌 반복자 객체에 책임을 위임한게 됩니다.
이렇게 하면 컬랙션의 인터페이스도 간단해지면서 자신의 일에 충실하게 되는거죠! 다음 다이어 그램을 살펴볼까요?
패턴 소개
식당 2개가 합병이 이루어져 메뉴판을 합쳐야 하는 상황이라 해봅시다.
두 식당 주인 모두 메뉴항목을 구성하는 방식에 대해서는 합의가 이루어져 있습니다. 다음과 같이 말이죠!
public class MenuItem {
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;
}
public String getName() {return name;}
public String getDescription() {return description;}
public double getPrice() {return price;}
public boolean isVegetarian() {return vegetarian;}
}
위에서 볼수 있듯 메뉴항목은 이름, 설명, 체식주의, 가격 으로 구성되어 있습니다.
문제는 이 메뉴항목을 저장하는 방식이 두 가게에서 달랐다는 점 입니다!
펜케이크 가게에서는 ArrayList 를 이용하였고, 점심식사 식당에서는 배열 를 이용하였습니다. 다음과 같이 말이죠!
public class PancakeHouseMenu {
ArrayList menuItems;
public PancakeHouseMenu(){
menuItems = new ArrayList();
addItem("김치 팬케이크 세트", "김치와 에그토스트가 곁들어진 팬케이크", true, 3.49);
addItem("레귤러 팬케이크 세트", "달걀 후라이와 소시지가 들어간 팬케이크", false, 2.99);
addItem("블루베리 팬케이크", "신선한 블루베리 시럽으로 만든 팬케이크", true, 3.29);
addItem("와플", "와플 취향에 따라서 시럽추가 가능.", true, 3.59);
}
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;
}
}
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("체식주의자용 BLT", "통밀에 상추, 베이컨, 토마토", true, 2.99);
addItem("BLT", "통밀위에 베이컨, 상추, 토마토", false, 2.99);
addItem("오늘의 스프", "옥수수를 곁들인 스프", false, 3.29);
addItem("핫도그", "사워크라우트, 갖은 양념, 양파, 치즈", false, 3.05);
}
public void addItem(String name, String description, boolean vegetarian, double price){
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if(numberOfItems >= MAX_ITEMS){
System.out.println("죄송합니다. 메뉴가 꽉 찼습니다.");
}else{
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
서로다른 메뉴 표현방식이 어떤 문제점이 있는지 알아봅시다! 그럼 우선 이 메뉴들을 사용할 종업원을 만들어 봅시다!
종업원은 메뉴를 출력해주는 기능을 갖고 있으며, 추가적으로 아침식사메뉴 출력, 점심식사메뉴 출력, 채식주의자 용 메뉴 출력 등의 기능을 갖고있습니다.
우선 모든 메뉴를 출력 하는 printMenu()를 구현해 봅시다.
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList breakfastItems = PancakeHouseMenu.getMenuItems();
DinerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenu.getMenuItems();
팬케이크 메뉴는 ArrayList를 반환하지만, 점심메뉴는 배열을 반환하고 있습니다.
이 둘을 모두 출력하기 위해서는 각각에 대해서 순환문을 돌려야 할 것 입니다. 다음과 같이 말이죠!
for(int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = (MenuItem)breakfastItems.get(i);
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription());
}
for(int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
System.out.print(menuItem.getName() + " ");
System.out.println(menuItem.getPrice() + " ");
System.out.println(menuItem.getDescription());
}
두개의 서로 다른 순환문을 만들어야 하는 불상사가 발생하였습니다...
종업원이 갖고 있는 다른 메소드들 에서도 위와같이 2개의 다른 방식으로 구현해야 할 것 입니다.
위와 같은 방식은 문제가 많습니다.
만약 각 메뉴에 대한 똑같은 인터페이스를 구현할 수 있다면 정말 편할것 같지 않나요? 인터페이스를 통합시킬 수는 없을까요?
객체의 컬렉션에 대한 반복작업을 캡슐화한 Iterator를 사용해 봅시다!
Iterator iter = breakfastMenu.createIterator();
while(iterator.hasNext()){
MenuItem menuItem = (MenuItem)iterator.next();
}
Iterator iter2 = lunchMenu.createIterator();
while(iterator.hasNext()){
MenuItem menuItem = (MenuItem)iterator.next();
}
두개 모두 iterator 객체를 이용하여 원소들에 접근하고 있습니다.
위와같은 방식이 바로 이터레이터 패턴 입니다!
이터레이터 패턴은 바로 Iterator 라는 인터페이스에 의존한다는 점이 중요합니다. 인터페이스는 다음과 같을것 입니다.
public interface Iterator {
boolean hasNext();
Object next();
}
이러한 인터페이스가 있으면 배열, 리스트, 해시테이블 더 나아가 어떤 종류의 객체 컬랙션에 대해서도 반복자를 구현할 수 있습니다.
DinerMenu에서 Iterator를 구현하기로 했다고 합니다. 다음과 같이 말이죠!
public interface Iterator {
boolean hasNext();
Object next();
}
public class DinerMenuIterator implements Iterator{
MenuItem[] items;
int position = 0; // position은 배열에 대한 반복작업에 있어서 현 위치를 알려줍니다.
public DinerMenuIterator(MenuItem[] items){ // 생성자 호출시 반복잡업을 수행할 메뉴항목을 받음
this.items = items;
}
public Object next() {
MenuItem menuItem = items[position++];
return menuItem;
}
public boolean hasNext() {
if(position >= items.length || items[position] == null){
return false;
}else {
return true;
}
}
}
다 만들었으니 사용해봐야 겠죠??
DinerMenuIteraotr를 생성하고 Client 한테 리턴하는 코드만 추가하면 됩니다. 이전에 만든 코드를 조금 수정한 것 입니다!
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
addItem("체식주의자용 BLT", "통밀에 상추, 베이컨, 토마토", true, 2.99);
addItem("BLT", "통밀위에 베이컨, 상추, 토마토", false, 2.99);
addItem("오늘의 스프", "옥수수를 곁들인 스프", false, 3.29);
addItem("핫도그", "사워크라우트, 갖은 양념, 양파, 치즈", false, 3.05);
}
public void addItem(String name, String description, boolean vegetarian, double price){
MenuItem menuItem = new MenuItem(name, description, vegetarian, price);
if(numberOfItems >= MAX_ITEMS){
System.out.println("죄송합니다. 메뉴가 꽉 찼습니다.");
}else{
menuItems[numberOfItems] = menuItem;
numberOfItems++;
}
}
public Iterator createIterator() { // 수정된 부분!
return new DinerMenuIterator(menuItems);
}
}
위 코드에서 중요한 점은 다음 1줄 입니다.
public Iterator createIterator() { return new DinerMenuIterator(menuItems);}
menuItems 배열을 생성자의 인자로 받아 DinerMenuIterator를 만든다음, 클라이언트 에게 반환하고 있습니다.
Iterator를 반환하고 있기 때문에 클라이언트 입장에서는 menuItem이 어떻게 관리되는지 전혀 모릅니다!
그냥 반복자(Iterator)를 통해서 메뉴에 들어있는 항목들에 하나씩 접근할수만 있으면 됩니다!
저~기~ 위에서 만들었던 종업원 코드 또한 고쳐봅시다!
우선 Iterator를 인자로 받아들이는 printMenu()를 만들고, 각 메뉴의 createIterator()를 사용하여 반복자를 반환받아 사용하면 됩니다!
public class Waitress {
PancakeHouseMenu pancakeHouseMenu;
DinerMenu dinerMenu;
public Waitress(PancakeHouseMenu pancakeHouseMenu, DinerMenu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("메뉴\n----\n아침 메뉴");
printMenu(pancakeIterator);
System.out.println("\n점심 메뉴");
printMenu(dinerIterator);
}
private void printMenu(Iterator 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()에서 모든 메뉴 항목에 접근하여 그 내용을 출력합니다.
코드를 테스트 해봐야 겠죠??
public class MenuTestDrive {
public static void main(String args[]) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
waitress.printMenu();
}
}
실행결과는 다음과 같습니다!
펜케이크 메뉴가 출력된 후, 점심메뉴들이 출력되는 것을 볼 수 있었습니다! 원하는데로 작동하는 군요!
▶ 메뉴 구현볍이 캡슐화 되었습니다! Client 입장에서는 메뉴에서 어떤 컬랙션을 사용하는지는 전혀 알 수가 없죠!
▶ 이전에는 2개의 순환문을 사용한것에 반하여 Iterator를 통해 어떤 컬랙션이든 하나의 순환문으로 처리할 수 있게되었습니다.
▶ 사용하는 client 입장에서는 hasNext(), next() 라는 인터페이스만 알고있으면 됩니다!
다만 아직 처리하지 못한 문제가 있는데.. 바로 Waitress(종업원)은 여전히 2개의 구상클래스 메뉴에 의존하고 있습니다.
우선 다음 다이어그렘을 함께 확인해 보실까요?
Iterator 덕분에 Waitress는 컬랙션이 어떻게 생겨먹었는지? 어떤 방식으로 구현되었는지 전혀 모르는 군요! 인터페이스 정도만 알뿐이죠!
이처럼 반복자를 이용하면 컬랙션 입장에서는 자신의 내부사항을 노출시키지 않으면서도, 컬랙션에 들어있는 요소들에 접근할 수 있습니다.
문제점도 있는데... PancakeHouseMenu, DinerMenu 이 둘은 똑같은 메소드를 제공함에도 같은 인터페이스를 구현하고 있지 않습니다.
공통된 인터페이스를 사용한다면 다형성을 활용할 수 있겠죠? 이 문제를 해결하여 Waitress의 구상메뉴클래스 에 대한 의존성을 제거합시다!
아! 지금까지는 그냥 Iterator 인터페이스를 직접 만들어 사용했는데, 지금부터는 java.util.Iterator 인터페이스를 사용합니다!
메뉴 인터페이스를 만들어 통일시키고 Waitress도 고쳐봅시다~~
public interface Menu {
public Iterator createIterator();
}
클라이언트에서 메뉴에 들어있는 항목게 대한 반복자를 얻기위한 간단한 인터페이스 정도만 ~~
다음으로는 바뀐 종업원 클래스! 이전에는 구상 클래스였던 부분들을 Menu 인터페스를 활용하였습니다! 다형성이 적용되었겠죠?
import java.util.Iterator;
public class Waitress {
Menu pancakeHouseMenu;
Menu dinerMenu;
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinerMenu.createIterator();
System.out.println("메뉴\n----\n아침 메뉴");
printMenu(pancakeIterator);
System.out.println("\n점심 메뉴");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem)iterator.next();
System.out.print(menuItem.getName() + ", ");
System.out.print(menuItem.getPrice() + " -- ");
System.out.println(menuItem.getDescription());
}
}
}
1. Waitress가 두 식당(Menu)에게 각각의 Iterator를 요청한다.
2. 각 식당에서 자신의 Iterator를 생성하여 반환한다.
3. 반환받은 Iterator를 이용하여 Waitress가 menuitem들을 출력한다.
( printMenu(pancakeIterator); printMenu(dinnerIterator); )
한번 정리해 봅시다!
여기서 잠깐!!
단일 역할 원칙
컬랙션은 원래 해당 클래스의 역할(집합체 관리) 외에 다른 역할(반복자)을 처리하도록 두면 두가지 이유로 인하여 클래스가 변경될 수 있습니다.
1) 컬렉션이 어떠한 이유로 바뀔때
2) 반복자 기능이 변경되었을때
이러면 바뀔수 있는 부분이 두가지 이상이 되는데... 다음 원칙에 위배될것 같군요...
클래스를 바꾸는 이유는 한가지 뿐이어야 한다
한가지 역할은 한 클래스에서만 책임져야 합니다. 쉬운것 같으면서도 어려운 원칙입니다.
디자인을 하는데 있어 역할을 분리키시는 것은 어려운 것중 하나이거든요...
'BackEnd > Design Patterens' 카테고리의 다른 글
[Design Patterns] State Pattern : 스테이트 패턴 (0) | 2022.01.13 |
---|---|
[Design Patterns] Composite Pattern : 컴포지트 패턴 (0) | 2022.01.13 |
[Design Patterns] Template Method Pattern : 템플릿 메소드 패턴 (0) | 2022.01.13 |
[Design Patterns] Facade Pattern : 퍼사드 패턴 (0) | 2022.01.13 |
[Design Patterns] Adapter Pattern : 어댑터 패턴 (0) | 2022.01.13 |
댓글