GoF · Behavioral · Iterator

Iterator

컬렉션의 속을 열어 보지 않고 원소를 순서대로 훑을 때. 순회를 캡슐화해 내부 구조와 순회 코드를 떼어 놓는 행위 패턴.

·행동 패턴 7편
목차
  1. 내부 구조를 알아야 순회한다
  2. 반복자 = 순회를 캡슐화한다
  3. Iterator 인터페이스
  4. 컬렉션이 반복자를 준다
  5. 내부가 바뀌어도 순회는 그대로다
  6. for-each의 정체
  7. 실무: 자바 컬렉션 프레임워크
  8. 정리

내부 구조를 알아야 순회한다

여러 컬렉션을 훑는다고 하자. 어떤 건 배열이고, 어떤 건 연결 리스트고, 어떤 건 트리다. 그런데 순회하는 코드가 그 내부 구조를 알아야 한다.

java
// 배열이면 인덱스로
for (int i = 0; i < array.length; i++) {
    process(array[i]);
}

// 연결 리스트면 노드를 따라
Node node = list.head;
while (node != null) {
    process(node.value);
    node = node.next;
}

배열은 length와 인덱스를 알아야 하고, 연결 리스트는 headnext를 알아야 한다. 순회 코드가 컬렉션의 속살에 묶여 있는 것이다. 그래서 컬렉션의 내부 구조가 바뀌면(배열을 리스트로 교체) 그걸 훑던 코드도 다 고쳐야 한다. 게다가 컬렉션이 head 같은 내부를 밖에 드러내야 해서, 감춰야 할 구조가 새어 나온다.

반복자 = 순회를 캡슐화한다

반복자 패턴이 이걸 푼다.

컬렉션의 내부 구조를 노출하지 않고, 원소를 순서대로 훑는 방법을 별도 객체(반복자)에 캡슐화한다.

“다음 원소가 있는가”, “다음 원소를 달라” - 순회에 필요한 건 이 두 물음뿐이다. 이 둘만 할 줄 아는 객체를 컬렉션이 내주면, 순회 코드는 컬렉션이 배열인지 리스트인지 트리인지 몰라도 된다. 어떻게 훑는지는 반복자 안에 숨고, 순회 코드는 그 반복자에게 묻기만 한다.

Iterator 인터페이스

순회의 두 물음을 인터페이스로 정한다. 자바에 이미 Iterator<E>가 있다.

java
public interface Iterator<E> {
    boolean hasNext();   // 다음 원소가 있는가
    E next();            // 다음 원소를 달라 (그리고 한 칸 전진)
}

hasNext()next() 둘뿐이다. 이 인터페이스만 상대하면 순회가 된다. 컬렉션의 실제 모양이 무엇이든, 그 컬렉션에 맞는 반복자가 이 두 메서드 뒤에서 알아서 다음 원소를 찾아 준다.

순회 코드
하나뿐 while (it.hasNext())
it.next()
묻는다
Iterator - 두 물음뿐
hasNext() next()
이 두 창구 말고는
열려 있지 않다
컬렉션 속 - 밖에선 보이지 않는다
배열
연결 리스트
트리

속은 맞붙은 칸·이어진 노드·갈라진 가지로 제각각인데, 밖에서는 늘 같은 두 물음이다.

컬렉션이 반복자를 준다

반복자는 컬렉션이 만들어 준다. 컬렉션이 자기 구조를 가장 잘 아니, 그 구조에 맞는 반복자를 자기가 내주는 게 맞다. 자바에서는 Iterable<E> 인터페이스가 이 역할이다.

java
public interface Iterable<E> {
    Iterator<E> iterator();   // 나를 훑을 반복자를 내준다
}

직접 만든 컬렉션에 반복자를 붙여 보자. 내부는 배열이지만, 밖에는 배열임을 드러내지 않는다.

java
public class Bag<E> implements Iterable<E> {

    private final Object[] items;
    private int size;
    // add 등 생략

    @Override
    public Iterator<E> iterator() {
        return new Iterator<>() {
            private int cursor = 0;          // 순회 상태를 반복자가 든다

            @Override
            public boolean hasNext() {
                return cursor < size;
            }

            @Override
            @SuppressWarnings("unchecked")
            public E next() {
                return (E) items[cursor++];
            }
        };
    }
}

cursor지금 어디까지 훑었는지를 기억한다. 이 순회 상태를 컬렉션이 아니라 반복자가 들고 있다는 점이 중요하다. 그래서 같은 Bag에서 반복자를 두 개 얻으면 각자 자기 cursor를 가져, 두 순회가 서로 방해 없이 독립적으로 진행된다.

내부가 바뀌어도 순회는 그대로다

값어치는 여기서 드러난다. Bag의 내부를 배열에서 연결 리스트로 바꾼다고 하자. iterator() 안의 hasNext()·next() 구현만 새 구조에 맞게 고치면 된다.

밖에서 Bag을 훑던 코드는 한 줄도 안 바뀐다. 그 코드는 여전히 hasNext()next()만 부르기 때문이다. 내부 구조의 변화가 반복자 안에서 막히고, Iterator 인터페이스라는 경계를 넘어 밖으로 새지 않는다. 순회하는 코드와 컬렉션의 내부 구조가 이 경계로 깔끔히 갈린다.

for-each의 정체

자바의 for-each 문은 사실 이 반복자 위에서 돈다.

java
Bag<String> bag = new Bag<>();
// ...
for (String s : bag) {
    System.out.println(s);
}

컴파일러가 이 for-each를 아래와 사실상 같은 코드로 바꾼다.

java
Iterator<String> it = bag.iterator();
while (it.hasNext()) {
    String s = it.next();
    System.out.println(s);
}

for-each에 무언가를 넣으려면 그것이 Iterable이어야 한다. iterator()를 내줄 수 있어야 컴파일러가 위 코드로 풀어낼 수 있기 때문이다. 우리가 만든 Bagfor-each에 그냥 들어간 이유가 이것이다. Iterable을 구현했으니 for-each가 뒤에서 반복자를 꺼내 돌린 것이다. 평소 무심코 쓰던 for-each가 실은 반복자 패턴을 부르고 있었던 셈이다.

실무: 자바 컬렉션 프레임워크

반복자 패턴은 자바 컬렉션 프레임워크 전체의 바탕이다. List, Set, Queue, Map의 키·값 뷰 - 이 모두가 Iterable이라 for-each로 훑을 수 있다.

우리가 ArrayListLinkedListHashSet이든 똑같은 for-each로 훑는 이유가 반복자 덕이다. ArrayList는 인덱스로, LinkedList는 노드 링크로 순회하지만, 그 차이는 각자의 반복자 안에 숨는다. 쓰는 쪽은 구현체를 바꿔 끼워도 순회 코드를 손대지 않는다. 내부가 배열이든 링크든 트리든, 밖에서는 “순서대로 훑는다”는 한 가지 방식으로 통일되는 것 - 이게 반복자가 실무에서 매일 하는 일이다.

정리

반복자란내부 구조를 노출하지 않고 원소를 순서대로 훑는다
문제순회 코드가 컬렉션의 내부 구조(인덱스·노드)에 묶인다
고침hasNext/next를 가진 반복자에 순회를 캡슐화
독립성순회 상태를 반복자가 들어 여러 순회가 서로 방해 없음
for-each사실 Iterable의 반복자를 꺼내 도는 문법 설탕
실무자바 컬렉션 프레임워크 전체, for-each

컬렉션의 속을 열지 않고 훑는 법만 반복자에 담아, 순회 코드와 내부 구조를 떼어 놓는 것 - 그게 반복자다.