2022. 2. 10. 17:13ㆍETC/Design Patterns
Object Behavioral Pattern
Iterator Pattern
----------------- INDEX -----------------
Iterator Pattern ?
Structure
Sample Code: Java
java.util.Iterator
관련 패턴
----------------------------------------------
Provide a way to access the elements of an aggregate object sequentially without exposing its underlying representation.
- GoF Design Patterns
반복자 패턴은 내부 구조를 노출하지 않고 집한체를 통해 원소 객체에 순차적으로 접근할 수 있는 방법을 제공합니다.
이미 많은 언어에서 iterator를 지원하고 있습니다.
C++, Java, Python 등 이미 구현된 객체가 존재합니다.
그만큼 많이 활용되고 있는 주제입니다.
먼저, Iterator Pattern이 발생할 상황을 같이 살펴볼게요.
Problems
카페 메뉴를 예시로 들어보자면, 아래와 같이 두 종류의 메뉴 카테고리가 있습니다.
하나는 카페인 음료 메뉴들이 담겨있고, 하나는 논카페인 메뉴들이 있습니다.
public class CoffeeMenu {
ArrayList<MenuItem> menuItems;
// ...
public ArrayList<MenuItem> getMenuItems() {
return menuItems;
}
}
public class NonCoffeeMenu {
int numberOfItems = 0;
MenuItem[] menuItems;
// ...
public MenuItem[] getMenuItems() {
return menuItems;
}
}
이 둘은 MenuItem 객체로 메뉴를 관리하고 있고, 아래와 같이 생성할 수 있습니다.
CoffeeMenu coffeeMenu = new CoffeeMenu();
NonCoffeeMenu nonCoffeeMenu = new NonCoffeeMenu();
ArrayList<MenuItem> coffee = coffeeMenu.getMenuItems();
MenuItem[] nonCoffee = nonCoffeeMenu.getMenuItems();
이제 두 메뉴를 하나의 메뉴판에 출력하려고 합니다.
어떻게 순회할 수 있을까요?
두 메뉴 모두 getMenuItems 라는 메서드로 메뉴를 가져오지만,
타입이 다르다는 걸 눈치채셨나요?
하나는 기본 자바 배열이고, 하나는 ArrayList 로 구성되어 있습니다.
이 둘을 한번에 출력할 수 없기 때문에 아래와 같이 각각 순회해야합니다.
for (int i = 0; i < coffee.size(); i++) {
System.out.print(coffee.get(i).getName() + " - ");
System.out.println("$" + coffee.get(i).getPrice());
}
for (int i = 0; i < nonCoffee.length; i++) {
System.out.print(nonCoffee[i].getName() + " - ");
System.out.println("$" + nonCoffee[i].getPrice());
}
위의 코드에서는 두 가지의 문제점이 보입니다.
✔️ 순회를 위해 내부 배열의 정의(CoffeeMenu - ArrayList, NonCoffeeMenu - Array)를 알아야합니다.
✔️ 순회를 위해 각각 다른 로직으로 짜여질 수 밖에 없습니다.
이 두 문제를 Iterator를 통해 해결해 봅시다.
접근법은 Aggregate(Collection) 객체를 정의하여 관리할 객체를 묶으며,
Iterator 인터페이스를 정의한 후 각각의 객체 전용 Iterator를 구현합니다.
Aggregate
객체의 효율적인 집합 관리를 위해 별도의 집합체Aggregate를 갖고 있습니다.
Aggregate는 사전적으로 ‘집합’, ‘모으다'라는 의미가 있습니다.
집합체는 단순 배열과는 다르게, 복수의 객체를 가진 복합 객체으로, Collection이라고도 합니다.
반복자 패턴은 배열을 사용하지 않고 별도의 컬렉션 객체를 생성합니다.
컬렉션 객체로 설계하는 이유는 객체를 효율적으로 관리하기 위해서입니다.
새로운 객체를 추가하거나 삭제하는 행위를 쉽게 처리할 수 있다.
일부 프로그래밍 언어는 배열의 크기를 제한하는 경우가 있는데, 컬렉션을 응용하면 보다 유연하게 확장할 수도 있습니다.
먼저, 구조를 확인하고 Java 코드를 확인해보도록 할게요.
Structure
Iterator
- 요소에 접근하고 순회하기 위한 인터페이스를 정의합니다.
Concretelterator
- Iterator 인터페이스를 구현합니다.
- Aggregate의 순회를 위해 현재 위치를 추적합니다.
Aggregate
- Iterator 객체를 생성하기 위한 인터페이스를 정의합니다.
ConcreteAggregate
- Aggregate 인터페이스의 createIterator 메서드를 구현해 적절한 Concretelterator 인스턴스를 반환합니다.
Sample Code: Java
이제 구조를 살펴보았으니, 실제로 구현해보도록 할게요.
먼저, 기존의 CoffeeMenu와 NonCoffeeMenu를 Aggregate(Collection)으로 만들기 위해
Aggregate 인터페이스를 작성합니다.
Aggregate
- Menu
public interface Menu {
public Iterator<MenuItem> createIterator();
}
간단하죠?
이제 위의 인터페이스를 받는 CoffeeMenu와 NonCoffeeMenu 클래스를 구현합니다.
이 예제에서는 위에서 작성한 코드를 수정해보도록 할게요.
ConcreteAggregate
- CoffeeMenu, NonCoffeeMenu
public class CoffeeMenu implements Menu{
ArrayList<MenuItem> menuItems;
public CoffeeMenu() {
this.menuItems = new ArrayList();
additem("Espresso", 4.5);
additem("Long Black", 5);
additem("Latte", 5.5);
additem("Flat White", 5.5);
}
public void additem(String name, double price) {
MenuItem menuItem = new MenuItem(name, price);
this.menuItems.add(menuItem);
}
@Override
public Iterator<MenuItem> createIterator() {
return new CoffeeMenuIterator(this.menuItems);
}
}
public class NonCoffeeMenu implements Menu {
static final int MAX_ITEMS = 4;
int numberOfItems = 0;
MenuItem[] menuItems;
public NonCoffeeMenu() {
this.menuItems = new MenuItem[MAX_ITEMS];
additem("Lime Passion Tea", 4.5);
additem("Grapefruit Honey Tea", 5);
additem("Vin Chaud", 5.5);
additem("Milk Shake", 5.5);
}
public void additem(String name, double price) {
MenuItem menuItem = new MenuItem(name, price);
if(this.numberOfItems >= MAX_ITEMS){
System.err.println("죄송합니다, 메뉴가 꽉 찼습니다. 더 이상 추가할 수 없습니다.");
} else {
menuItems[numberOfItems] = menuItem;
numberOfItems = numberOfItems+1;
}
}
@Override
public Iterator<MenuItem> createIterator() {
return new NonCoffeeMenuIterator(this.menuItems);
}
}
기존의 MenuItems를 반환하던 getMenuItems를 제거하고 Aggregate 인터페이스에서 정의된 createIterator를 구현합니다.
createIterator 메서드에서는 ConcreteIterator를 반환하는 것을 확인할 수 있습니다.
이 패턴이 조금 익숙하지 않나요?
바로, 팩토리 메서드 패턴을 확인할 수 있습니다.
팩토리 메서드 패턴은 객체의 생성 코드를 별도의 메서드로 분리함으로써 객체 생성의 변화에 대비하는 데 유용합니다.
반복자 객체를 생성하는데 적합한 패턴이죠.
이제, 반복자 객체를 생성해봅시다.
먼저, 아웃라인이 될 인터페이스를 정의합니다.
Iterator
public interface Iterator<T> {
public boolean hasNext();
public T next();
}
위의 인터페이스를 각각 구현합니다.
ConcreteIterator
public class CoffeeMenuIterator implements Iterator<MenuItem>{
ArrayList<MenuItem> list;
int position = 0;
public CoffeeMenuIterator (ArrayList<MenuItem> list) {
this.list = list;
}
@Override
public MenuItem next() {
MenuItem menuItem = list.get(position);
position += 1;
return menuItem;
}
@Override
public boolean hasNext() {
if (position >= list.size() || list.get(position) == null) return false;
return true;
}
}
public class NonCoffeeMenuIterator implements Iterator<MenuItem>{
MenuItem[] list;
int position = 0;
public NonCoffeeMenuIterator(MenuItem[] list) {
this.list = list;
}
@Override
public MenuItem next() {
MenuItem menuItem = list[position];
position += 1;
return menuItem;
}
@Override
public boolean hasNext() {
if (position >= list.length || list[position] == null) return false;
return true;
}
}
위의 Structure에서 말한 '현재 위치를 추적'하는 역할로 position이 정의 되었습니다.
모든 클래스를 정의했습니다!
이제, 모든 메뉴를 출력할 때 얼마나 효율적이게 변경되었는지 확인해볼게요.
Client
public class Client {
public static void main(String[] args) {
CoffeeMenu coffeeMenu = new CoffeeMenu();
NonCoffeeMenu nonCoffeeMenu = new NonCoffeeMenu();
Iterator<MenuItem> coffee = coffeeMenu.createIterator();
Iterator<MenuItem> nonCoffee= nonCoffeeMenu.createIterator();
printMenu(coffee);
printMenu(nonCoffee);
}
static void printMenu(Iterator<MenuItem> iterator) {
while(iterator.hasNext()) {
MenuItem item = iterator.next();
System.out.print(item.getName() + " - ");
System.out.println("$" + item.getPrice());
}
}
}
위와 같이 여러 루프를 생성하지 않아도, 하나로 통일해 순회할 수 있게 되었습니다.
모든 코드는 Github Repository 에서 확인할 수 있습니다.
출력은 아래와 같습니다.
Output
Espresso - $4.5
Long Black - $5.0
Latte - $5.5
Flat White - $5.5
Lime Passion Tea - $4.5
Grapefruit Honey Tea - $5.0
Vin Chaud - $5.5
Milk Shake - $5.5
java.util.Iterator
위의 코드에서 ArrayList는 이미 Iterable(Iterator로 순회가능한) Collection 인터페이스를 구현한 객체입니다.
따라서, 기존의 Iterator를 수정할 수 있는데요.
수정된 코드는 Github repository에서 확인할 수 있고, 기존 코드에서 변경 이력만 확인하고 싶다면 해당 링크를 참고하시면 됩니다.
관련 패턴
C- Creational Patterns | S - Structural Patterns | B - Behavioral Patterns
위에서 확인한 것과 같이 반복자 객체 생성 시 팩토리 메서드 패턴을 응용할 수 있습니다.
하나의 객체가 다수의 여러 객체를 가질 수 있으며 이러한 구조는 복합체 패턴과 유사합니다.
재귀적 합성 구조를 가진 복합체는 외부 반복자로 처리하기 어려운데,
그 이유는 재귀적 합성 구조가 중첩된 위치 관계를 갖고 있기 때문입니다.
반복자 패턴은 집합체 요소의 개수를 파악하고, 요소의 개수에 접근하여 함께 처리합니다.
그럼 지금까지 Iterator Pattern에 대해 알아보았습니다.
오타나 잘못된 내용이 있다면 댓글로 남겨주세요!
감사합니다 ☺️
'ETC > Design Patterns' 카테고리의 다른 글
Design Pattern, State (0) | 2022.02.16 |
---|---|
Design Pattern, Visitor (0) | 2022.02.13 |
Design Pattern, Command (0) | 2022.02.06 |
Design Pattern, Decorator (0) | 2022.02.03 |
Design Pattern, Composite (0) | 2022.02.01 |
Backend Software Engineer
𝐒𝐮𝐧 · 𝙂𝙮𝙚𝙤𝙣𝙜𝙨𝙪𝙣 𝙋𝙖𝙧𝙠