이터레이터(iterator)와 컴포지트(composite) 패턴

Head First - Design Patterns
(에릭 프리먼, 엘리자베스 프리먼, 케이시 시에라, 버트 베이츠 저 | 서환수 역)
을 읽고 정리한 내용입니다.


이터레이터 패턴(iterator pattern)

객체를 저장하는 방식은 보여주지 않으면서도
클라이언트가 객체들에게 일일이 접근할 수 있게 해주는 방법인
이터레이터 패턴(iterator pattern)에 대해 알아보자.

이터레이터 패턴을 사용하면 모든 항목에 일일이 접근하는 작업을
컬렉션 객체가 아닌 반복자 객체에서 맡게 된다.


반복을 캡슐화하자.

바뀌는 부분은 캡슐화한다.
반복 작업을 캡슐화해서 Iterator라는 객체를 만든다.
이터레이터(Iterator) 패턴에서 중요한 것은 Iterator라는 인터페이스에 의존한다는 것이다.
IterableIterator를 잘 구분하자.


java.util.Iterator 인터페이스

객체 컬렉션에서 어떤 항목을 제거하는 기능을 제공하고 싶지 않을 때

remove() 메서드를 반드시 제공해야 하는 건 아니다.
대신 remove()를 쓸 수 없도록 만들고 싶다면,
런타임java.lang.UnsupportedOperationException을 던지도록 하면 된다.
컴파일 타임에 제어하는 방법은 없고, 위와 같이 런타임에 제어하는 방법은 많이 사용된다.


반복자 코드를 웨이트리스에 적용시키기

1
2
3
public interface Menu {
public Iterator createIterator();
}

Iterator를 인자로 받아들이는 printMenu() 메서드를 만든다.
각 메뉴의 getIterator() 메서드를 사용해서 Iterator를 받은 후,
새로운 메소드를 넘긴다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import java.util.Iterator;

public class Waitress {
// 타입이 Menu로 같은 값이 2개 이상이다.
// -> 변화에 유연하기 위해 List로 만드는 게 좋다.
Menu pancakeHouseMenu;
Menu dinerMenu;

// 생성자에서 두 메뉴를 인자로 받아온다.
public Waitress(Menu pancakeHouseMenu, Menu dinerMenu) {
this.pancakeHouseMenu = pancakeHouseMenu;
this.dinerMenu = dinerMenu;
}

public void printMenu() {
// printMenu()에서 두 개의 반복자를 생성한다.
// (메뉴마다 하나씩 만들어야 하므로)
Iterator pancakeIterator = pancakeHouseMenu.createIterator();
Iterator dinerIterator = dinewMenu.createIterator();
System.out.println("메뉴\n---\n아침 식사");
System.out.println("\n점심 식사");
// 각 반복자를 가지고 오버로드된 printMenu()를 호출한다.
printMenu(dinerIterator);
}
private void printMenu(Iterator iterator) {
// 항목이 더 남았는지 확인
while (iterator.hasNext()) {
// 다음 항목을 가져온다.
MenuItem menuItem = (MenuItem) iterator.next();
System.out.println(menuItem.getName() + ",");
System.out.println(menuItem.getPrice( + " -- "));
System.out.println(menuItem.getDescription());
}
}
// 기타 메서드
}

불변 리스트 만들기

자바 컬렉션에서 리스트를 더 이상 추가, 삭제를 막기 위해서
java.util.Collections.unmodifiableList를 사용한다.

읽기 전용으로 List를 생성하며,메서드를 막을 때 많이 사용한다.
인터페이스로 만들면 반드시 오버라이딩해야만 하므로 이를 피하면서,
지금은 구현하고 싶지 않을 때 주로 사용한다.


Enhanced For Loop

향상된 For문이라는 의미로, Java 5 이상에서 지원하는 for문
기존의 For문과는 달리, Index가 없다는 단점이 있다.

1
2
3
for (대입받을 변수 정의 : 배열) {
// 실행할 문장
}

ex)

1
2
3
4
String[] strArr = {"a", "b", "c", "d"}
for (String arr : strArr) {
System.out.println(arr);
}

자바8 이상에서는 stream이나 Enhanced For Loop를 사용한다.


이터레이터 패턴의 정의

이터레이터 패턴은 컬렉션 구현 방법을 노출시키지 않으면서도 그 집합체 안에 들어있는
모든 항목에 접근할 수 있게 해주는 방법을 제공해 준다.

이터레이터 패턴의 클래스 다이어그램

iterator-pattern

  • Aggregate: 공통적인 인터페이스
  • Iterator
    • 모든 반복자에서 구현해야 하는 인터페이스를 제공한다.
    • 컬렉션에 들어있는 원소들에 돌아가면서 접근할 수 있게 해 주는 메서드들을 제공한다.
  • ConcreteAggregate
    • 객체 컬렉션이 들어있고, 그 안에 들어있는 컬렉션에 대한 Iterator를 리턴하는 메서드를 구현한다.
    • 모든 ConcreteAggregate는 그 안에 있는 객체 컬렉션에 대해 돌아가면서 반복 작업을 처리할 수 있게 해주는 ConcreteIterator의 인스턴스를 만들 수 있어야 한다.
  • ConcreteIterator: 반복작업 중에 현재 위치를 관리한다.

내부(iternal) 반복자와 외부(external) 반복자

내부(iternal) 반복자

  • 반복을 제어하는 주체: 반복자 자신에 의해 제어한다.
    반복자가 자신이기 때문에 다음 원소에 대해서 어떤 작업을 직접 처리한다.

  • -> 따라서 반복자에게 모든 원소에 대해서 어떤 일을 할지 직접 알려줘야 한다.

  • 내부 반복자는 클라이언트가 반복작업을 마음대로 제어할 수 없기 때문에
    외부 반복자보다 유연성이 떨어진다.

  • But, 할 일을 넘겨주기만 하면, 나머지는 전부 알아서 해준다.

  • ex) stream에서 내부적으로 for문을 돌리는 것
    내부 반복자를 사용하면, 반복문에서 조건을 잘못 줘서 루프를 1번 더 돈다거나
    멈춰야하는데 안 멈추거나 하는 실수를 방지할 수 있다.

외부(external) 반복자

  • 반복을 제어하는 주체: 클라이언트가 반복작업을 제어한다.

  • 클라이언트에서 next()를 호출해서 다음 항목을 가져온다.

단일 역할 원칙

디자인 원칙
: 클래스를 바꾸는 이유는 한 가지 뿐이어야 한다.
이 원칙에 따라 한 클래스에서는 한 가지 역할만 맡도록 해야 한다.

But, 실제로는 클래스를 바꾸는 이유 그 한 가지를 정의하기 어렵다.

ex) 식사를 예로 들자면,
개발자1: ‘밥을 먹는다’ -> 한 가지로 본다.
개발자2: ‘밥을 차린다 + 밥을 먹는다 + 정리한다’ -> 한 가지로 본다.
사람마다 그 한가지 이유의 기준이 다를 수 있기 때문이다.


컴포지트 패턴(Composite pattern)

컴포지트 패턴의 정의

컴포지트 패턴을 이용하면, 객체들을 트리 구조로 구성해서 부분과 전체를 나타내는 계층 구조를 만들 수 있다.
클라이언트에서 개별 객체와 다른 객체들로 구성된 복합 객체(compoiste)를 똑같은 방법으로 다룰 수 있다.


컴포지트 패턴의 클래스 다이어그램

composite-pattern

컴포지트 패턴을 이용하면, 복합 객체와 개별 객체를 구분할 필요가 없어진다.
복합 구조에 들어있는 것을 구성요소라고 부른다.
구성요소에는 복합 객체잎(leaf) 노드가 있다.
잎(leaf) 노드든 아니든 똑같이 구현할 수 있게 되는 것이다.


컴포지트 패턴을 이용한 메뉴 디자인


MenuComponentMenuItem(Leaf)와 Menu(Composite)에서 쓰이는 인터페이스 역할을 한다.


복합 반복자

CompositeIteratorMenuItem에 대해서 반복작업을 할 수 있게 해주는 기능을 제공한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import java.util.*;

// 다른 반복자들과 마찬가지로 java.util.Iterator 인터페이스를 구현한다.
public class CompoisteIterator implements Iterator {
Stack stack = new Stack();

// 반복작업을 처리할 대상 중에서 최상위 복합 객체의 반복자가 전달된다.
// 그 반복자는 스택에 집어넣는다.
public CompoisteIterator(Iterator iterator) {
stack.push(iterator);
}

public Object next() {
// 클라이언트에서 다음 원소를 요청하려면,
// 우선, hasNext()를 호출해서 다음 원소가 남아있는지 확인해야 한다.
// 다음 원소가 있다면, 스택에서 현재 반복자를 꺼낸 후 그 다음 원소를 구한다.
if (hasNext()) {
// stack.peek(): 읽기. stack의 top이 가리키는 데이터를 반환
Iterator iterator = (Iterator) stack.peek();
MenuComponent component = (MenuComponent) iterator.next();

// 그 원소가 메뉴일 경우(또 다른 객체가 추가된 것이므로)
if (component instanceof Menu) {
// 스택에 집어 넣는다.
stack.push(component.createIterator());
}
// 원소가 메뉴든 아니든 구성요소 자체는 리턴한다.
return component;
} else {
return null;
}
}

// public boolean hasNext() {
// if (stack.empty()) {
// return false;
// } else {
// Iterator iterator = (Iterator) stack.peek();
// if (!iterator.hasNext()) {
// stack.pop();
// return hasNext();
// } else {
// return true;
// }
// }
// }

public boolean hasNext() {
// 스택이 비어있는지를 확인하여 다음 원소가 있는지 살펴본다.
if (stack.empty()) {
return false;
}

Iterator iterator = (Iterator) stack.peek();
if (!iterator.hasNext()) {
stack.pop();
return hasNext();
}
return true;
}

public void remove() {
throw new UnsupportedOperationException();
}
}

컴포지트 패턴의 구현 문제

장점

  • 클라이언트를 단순화시킬 수 있다.
    (복합 객체를 사용하고 있는지 잎(left) 객체를 사용하고 있는지에 대해 신경쓰지 않아도 되기 때문이다.)
  • 메서드 하나만 호출하면, 전체 구조에 대해서 반복해서 작업을 처리할 수 있는 경우도 자주 있다.

1. 컴포지트 패턴에서 아무 의미가 없는 메서드가 생기는 경우

아무일도 하지 않거나 널(Null) 또는 false를 상황에 맞게 리턴하는 방법이 있다.
또는 예외를 던질 수 있다.

2. 복합 구조를 탐색하는데 너무 복잡할 경우

복합 노드를 캐싱해두는 게 효과적이다.

결론

컴포지트 패턴을 적용할 때는 여러 가지 장단점을 고려해야 한다.
상황에 따라 투명성과 안전성 사이에서 적절한 평형점을 찾아야 한다.


참고 링크