"헤더퍼스트 디자인패턴" + "면접을 위한 CS 전공 지식 노트"에 개인적인 의견과 생각들을 추가하여 작성한 포스팅이므로 틀린 내용이 있을 수 있습니다. (있다면 지적 부탁 드립니다.)
📕 목차
1. Iterator Pattern
2. Menu 관리하기
1. Iterator Pattern
📌 반복자 패턴
💡 Collection 구현 방법을 노출하지 않으면서, Iterator를 사용하여 Collection의 모든 요소에 순차적 접근(순회)을 지원한다.
- Iterator는 순회 가능(연속적인)한 Collection들의 순차적인 접근을 지원하는 인터페이스
- 배열이나 리스트는 연속적인 데이터 집합이므로 간단한 for문으로 순회 가능하다.
- Hash, Tree와 같은 Collection은 순서가 없기 때문에, 순회를 위한 판단 기준이 필요하다.
- Iterator Pattern은 순회를 위한 판단 기준 알고리즘을 정의하여, 복잡한 Collection들도 iterable하게 만든다.
- Collection 내의 모든 Element에 대한 접근 방식이 공통화되어 있어야 한다.
- 여러 가지 자료 구조와 무관하게 Iterator 인터페이스 하나로 순회가 가능해진다.
- 각 항목에 일일이 접근하는 기능을 집합체가 아닌 반복자 객체가 책임진다. (단일 책임)
- 집합체의 내부 구조를 노출하지 않아도 된다. (캡슐화)
📌 반복자 패턴 구조
- Aggregate
- Collection을 추상화하기 위한 Interface
- ConcreateIterator 인스턴스를 반환하는 메서드 제공
- ConcreateAggregate
- 여러 Element들이 이루어져 있는 데이터 Collection이 들어 있다.
- 해당 Collection을 Iterator로 반환하는 createIterator() 구현
- 모든 ConcreateAggregate는 객체 Collection을 대상으로 순회할 수 있게 해주는 ConcreteIterator의 인스턴스를 만들 수 있어야 한다.
- Iterator
- Collection 내 Element들을 순차적으로 검색하기 위한 Interface
- 모든 반복자가 구현해야 하는 Interface 제공
- ConcreateIterator
- ConcreateAggregate의 Collection을 참조하여 순회한다.
- Client
- 구체 클래스 신경쓸 필요 없이, Aggregate와 Iterator만 알면 된다.
- 모두 추상화되어 있으므로 DIP, OCP 원칙에 다형성, 캡슐화까지 아주 예술적으로 두루두루 갖추고 있다.
더보기
✒️ 단일 책임 원칙(SRP)와 반복자 패턴
💡 어떤 클래스가 바뀌는 이유는 오직 하나뿐이어야 한다.
- 반복자 패턴을 사용하지 않으면, 내부 Collection 관련 기능과 Iterator method 관련 기능을 전부 구현해야만 한다.
- 이렇게 되면 원래 해당 클래스의 역할(집합체 관리) 외에 다른 역할(반복자)을 처리할 때 2가지 이유로 클래스가 바뀔 수 있다.
- 코드를 수정해야 할 이유가 2가지나 있으면, 잠재적인 오류가 발생할 확률이 증가한다.
- 따라서 하나의 클래스는 하나의 역할만을 갖도록 디자인하라
📌 Iterator와 Collection
🟡 Iterable interface
- 어떤 클래스에서 Iterable을 구현하면, 그 클래스는 iterator() 메서드를 구현한다.
- iterator(): Iterator 인터페이스를 구현하는 메서드
- forEach() 메서드를 기본으로 포함하기 때문에 for-each 구문도 사용 가능하다.
- ArrayList를 비롯한 모든 Collection 클래스는 Iterable을 상속하는 Collection 인터페이스를 구현한다.
- 따라서 모든 Collection 클래스는 Iterable하다
- 단, 배열은 Iterable하지 않기 때문에 forEach()를 사용할 수 없다. (for-each 구문은 사용할 수 있는 이유가 자동 Casting 되기 때문)
🟡 자바 컬렉션 프레임워크(JCF, Java Collections Framework)
- 자바에서 기본적으로 제공하는 Collection (ex. ArrayList, Vector, LinkedList, Stack, PriorityQueue 등)
- JCF는 단순히 클래스와 인터페이스를 모아 놓은 것에 불과하다.
- 다만 모두 java.util.Collection 인터페이스를 구현하므로 iterable하다
- JCF에서 각종 Collection을 무리없이 순회 가능한 것도 내부에 미리 Iterator Pattern이 적용되어 있기 때문이다.
📌 반복자 패턴 코드 패턴
interface Aggregate {
Iterator createIterator();
}
class ConcreteAggregate implements Aggregate {
private Object[] items;
int index = -1;
public ConcreteAggregate(int size) {
this.items = new Object[size];
}
public void add(Object item) {
if (index >= items.length)
throw new ArrayIndexOutOfBoundsException();
items[++index] = item;
}
@Override
public Iterator createIterator() {
return new ConcreteIterator(this);
}
public Object[] getItems() {
return items;
}
}
class ConcreteIterator implements Iterator {
private final ConcreteAggregate aggregate;
private int nextIndex = -1;
public ConcreteIterator(ConcreteAggregate aggregate) {
this.aggregate = aggregate;
}
@Override
public boolean hasNext() {
return nextIndex < aggregate.getItems().length - 1;
}
@Override
public Object next() {
return aggregate.getItems()[++nextIndex];
}
}
public class DefaultIteratorPattern {
public static void main(String[] args) {
ConcreteAggregate aggregate = new ConcreteAggregate(3);
aggregate.add("A");
aggregate.add("B");
aggregate.add("C");
Iterator iterator = aggregate.createIterator();
while (iterator.hasNext()) {
System.out.print(iterator.next() + " -> ");
}
}
}
A -> B -> C ->
📌 반복자 패턴 특징
- 사용 시기
- Collection 상관 없이 객체 접근 순회 방식 통일
- Collection을 순회하기 위한 다양한 전략을 지원
- Collection 내부 구조를 Client로부터 숨기고 싶은 경우
- 데이터 저장 Collection 종류에 변경 가능성이 있는 경우 (외부로 공개된 코드는 Client가 사용하고 있기 때문에 함부로 변경할 수 없다.)
- 장점
- 일관된 Iterator interface로 여러 형태의 Collection에 대해 동일한 순회 방법 제공
- Collection 내부 구조 및 순회 방식을 감출 수 있다.
- ConcreteAggregate 내에 수정 사항이 생겨도, Client는 Iterator로 접근하므로 iterator가 문제 없다면 똑같이 동작한다.
- SRP, OCP를 준수한다.
- 단점
- 클래스가 늘어나고 복잡도가 증가한다. (SRP를 깨고 Composite Pattern을 고려해볼 수도 있다.)
2. Menu 관리하기
📌 Use case
- 서로 다른 두 식당이 있고, A 식당은 아침 메뉴, B 식당은 점심 메뉴를 판매한다.
- A 식당은 메뉴를 ArrayList에 관리하고, B 식당은 배열로 관리한다.
- 단, 메뉴 항목을 나타내는 방법은 통일되어 있다.
- 멤버 변수: name, description, vegetarian, price
- 메서드: 모든 멤버 변수에 대한 getter
- 종업원 조건
- printMenu(): 메뉴의 모든 항목 출력
- printBreakfastMenu(): 아침 식사 메뉴 출력
- printLunchMenu(): 점심 식사 메뉴 출력
- printVegetarianMenu(): 채식주의자용 메뉴 출력
- isItemVegetarian(): 해당 항목이 채식주의자 용이면 true, 아니면 false 출력
📌 Legacy Code
public class PancakeHouseMenu {
List<MenuItem> menuItems;
public PancakeHouseMenu() {
menuItems = new ArrayList<>();
Stream.of(PancakeHouseItem.values()).forEach(item -> {
menuItems.add(new MenuItem(item.getName(), item.getDescription(), item.isVegetarian(), item.getPrice()));
});
}
public List<MenuItem> getMenuItems() {
return menuItems;
}
public void addItem(String name, String description, boolean vegetarian, double price) {
menuItems.add(new MenuItem(name, description, vegetarian, price));
}
}
public class DinerMenu {
static final int MAX_ITEMS = 6;
int numberOfItems = 0;
MenuItem[] menuItems;
public DinerMenu() {
menuItems = new MenuItem[MAX_ITEMS];
Stream.of(DinerItem.values()).forEach(item -> {
addItem(item.getName(), item.getDescription(), item.isVegetarian(), item.getPrice());
});
}
public void addItem(String name, String description, boolean vegetarian, double price) {
if (numberOfItems >= MAX_ITEMS) {
System.err.println("Sorry, menu is full! Can't add item to menu");
} else {
menuItems[numberOfItems++] = new MenuItem(name, description, vegetarian, price);
}
}
public MenuItem[] getMenuItems() {
return menuItems;
}
}
위 방식의 가장 큰 문제점은 Collection을 다루는 자료 구조가 다르다는 점이다.
자료 구조가 다루기 때문에 순회를 하는 데도 어려움이 생긴다.
❌ 객체 생성 : 다형성 위배
public class Waitress {
public static void main(String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
ArrayList<MenuItem> breakfastItems = pancakeHouseMenu.getMenuItems();
DinerMenu dinerMenu = new DinerMenu();
MenuItem[] lunchItems = dinerMenu.getMenuItems();
}
}
- 종업원 코드는 매번 다른 타입의 메뉴를 사용해서 메뉴를 불러와야 한다.
- 메뉴 항목이 다르므로 서로 다른 반복문을 구현해주어야 한다. (다른 가게가 추가될 때마다 반복문도 추가)
- 종업원 코드에서 모든 가게의 메뉴를 관리하는 것이 점점 어려워진다.
📌 Design
💡 바뀌는 부분은 캡슐화하라!
for (int i = 0; i < breakfastItems.size(); i++) {
MenuItem menuItem = breakfastItems.get(i);
}
for (int i = 0; i < lunchItems.length; i++) {
MenuItem menuItem = lunchItems[i];
}
- 현재 바뀌는 부분은 반복 처리 방법에 해당한다.
- ArrayList 타입의 get()과 배열 타입의 index 접근 방법을 하나로 묶어야 한다.
Iterator breakfastIterator = breakfastItems.iterator();
while (breakfastIterator.hasNext()) {
MenuItem menuItem = (MenuItem) breakfastIterator.next();
}
- 위 코드처럼 Iterator로 캡슐화할 수 있다면, 모든 Collection에 대해 공통적인 순회를 보장할 수 있다.
📌 Implementation
1️⃣ Collection 별 반복자 구현하기
public class DinerMenuIterator implements Iterator {
private final MenuItem[] items;
private int position = 0;
public DinerMenuIterator(MenuItem[] items) {
this.items = items;
}
@Override
public boolean hasNext() {
return position < items.length && items[position] != null;
}
@Override
public Object next() {
MenuItem item = items[position++];
return item;
}
}
2️⃣ Menu 클래스에서 Iterator 반환 메서드 구현
public class DinerMenu {
...
// public MenuItem[] getMenuItems() {
// return menuItems;
// }
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
public class PancakeHouseMenu {
...
// public ArrayList<MenuItem> getMenuItems() {
// return menuItems;
// }
public Iterator createIterator() {
return menuItems.iterator();
}
}
- java.util.Collection의 ArrayList는 이미 iterator()가 정의되어 있기 때문에, 그대로 사용해주면 된다.
3️⃣ Waitress 코드 수정하기
public class Waitress {
private final PancakeHouseMenu pancakeHouseMenu;
private final 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("MENU\n----\nBREAKFAST");
printMenu(pancakeIterator);
System.out.println("\nLUNCH");
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
while (iterator.hasNext()) {
MenuItem menuItem = (MenuItem) iterator.next();
System.out.printf("%s, ", menuItem.getName());
System.out.printf("%.2f -- ", menuItem.getPrice());
System.out.println(menuItem.getDescription());
}
}
}
- 두 가게 모두 공통된 인터페이스인 Iterator을 반환하므로, 순환문에 구분을 두지 않아도 된다.
- 반복자만 구현한다면 다형성을 활용해 어떤 Collection이든 1개의 순환문으로 처리할 수 있다.
4️⃣ 실행 결과 확인
public static void main(String[] args) {
PancakeHouseMenu pancakeHouseMenu = new PancakeHouseMenu();
DinerMenu dinerMenu = new DinerMenu();
Waitress waitress = new Waitress(pancakeHouseMenu, dinerMenu);
waitress.printMenu();
}
MENU
----
BREAKFAST
Pancake1, 2.99 -- Pancake1 description
Pancake2, 2.99 -- Pancake2 description
Pancake3, 3.49 -- Pancake3 description
Pancake4, 3.59 -- Pancake4 description
Pancake5, 3.69 -- Pancake5 description
LUNCH
Diner1, 2.99 -- Diner1 description
Diner2, 2.99 -- Diner2 description
Diner3, 3.49 -- Diner3 description
Diner4, 3.59 -- Diner4 description
Diner5, 3.69 -- Diner5 description
📌 구상 클래스 추상화하기
여전히 Waitress 클래스는 PancakeHouseMenu와 DinerMenu라는 구상 클래스에 얽매여 있다.
이것도 다형성을 활용하여 수정할 수 있다.
1️⃣ Menu 클래스 추상화
public interface Menu {
Iterator<MenuItem> createIterator();
}
- 어떤 Menu든지 Iterator을 반환하는 createIterator()를 정의하도록 한다.
- 이 외에도 addItem()와 같은 메서드도 공통이지만, Inteface에 등록한 메서드는 공개 메서드가 된다.
- 무엇이든 공개가 되면, 나중에 수정하는 것이 굉장히 까다로워진다.
- 이유가 없다면 전부 막아버리는 것이 좋다.
2️⃣ Menu 구현
public class DinerMenu implements Menu {
...
@Override
public Iterator createIterator() {
return new DinerMenuIterator(menuItems);
}
}
public class PancakeHouseMenu implements Menu {
...
@Override
public Iterator createIterator() {
return menuItems.iterator();
}
}
- Menu를 구현하게 함으로써, 해당 인터페이스를 구현하는 모든 클래스는 Iterator를 반환하는 것을 보장한다.
3️⃣ Waitress 코드 수정하기
public class Waitress {
private final Menu pancakeHouseMenu;
private final Menu dinerMenu;
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}
public void printMenu() {
Iterator<MenuItem> pancakeIterator = pancakeHouseMenu.createIterator();
Iterator<MenuItem> dinerIterator = dinerMenu.createIterator();
...
}
...
}
- 종업원은 Iterator만 사용하면 된다. (반복 작업을 수행할 객체 그룹 종류 대상은 Iterator가 판단)
- 메뉴가 추가되거나 자료구조가 변경되어도 종업원은 이 변화를 감지하지 못 한다.
📌 구조