← Posts

GoF · Behavioral · Iterator

Iterator

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

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

diagramdiagram

컬렉션이 반복자를 준다

반복자는 컬렉션이 만들어 준다. 컬렉션이 자기 구조를 가장 잘 아니, 그 구조에 맞는 반복자를 자기가 내주는 게 맞다. 자바에서는 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

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

관련 글