목차
내부 구조를 알아야 순회한다
여러 컬렉션을 훑는다고 하자. 어떤 건 배열이고, 어떤 건 연결 리스트고, 어떤 건 트리다. 그런데 순회하는 코드가 그 내부 구조를 알아야 한다.
// 배열이면 인덱스로
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와 인덱스를 알아야 하고, 연결 리스트는 head와 next를 알아야 한다. 순회 코드가 컬렉션의 속살에 묶여 있는 것이다. 그래서 컬렉션의 내부 구조가 바뀌면(배열을 리스트로 교체) 그걸 훑던 코드도 다 고쳐야 한다. 게다가 컬렉션이 head 같은 내부를 밖에 드러내야 해서, 감춰야 할 구조가 새어 나온다.
반복자 = 순회를 캡슐화한다
반복자 패턴이 이걸 푼다.
컬렉션의 내부 구조를 노출하지 않고, 원소를 순서대로 훑는 방법을 별도 객체(반복자)에 캡슐화한다.
“다음 원소가 있는가”, “다음 원소를 달라” - 순회에 필요한 건 이 두 물음뿐이다. 이 둘만 할 줄 아는 객체를 컬렉션이 내주면, 순회 코드는 컬렉션이 배열인지 리스트인지 트리인지 몰라도 된다. 어떻게 훑는지는 반복자 안에 숨고, 순회 코드는 그 반복자에게 묻기만 한다.
Iterator 인터페이스
순회의 두 물음을 인터페이스로 정한다. 자바에 이미 Iterator<E>가 있다.
public interface Iterator<E> {
boolean hasNext(); // 다음 원소가 있는가
E next(); // 다음 원소를 달라 (그리고 한 칸 전진)
}hasNext()와 next() 둘뿐이다. 이 인터페이스만 상대하면 순회가 된다. 컬렉션의 실제 모양이 무엇이든, 그 컬렉션에 맞는 반복자가 이 두 메서드 뒤에서 알아서 다음 원소를 찾아 준다.
컬렉션이 반복자를 준다
반복자는 컬렉션이 만들어 준다. 컬렉션이 자기 구조를 가장 잘 아니, 그 구조에 맞는 반복자를 자기가 내주는 게 맞다. 자바에서는 Iterable<E> 인터페이스가 이 역할이다.
public interface Iterable<E> {
Iterator<E> iterator(); // 나를 훑을 반복자를 내준다
}직접 만든 컬렉션에 반복자를 붙여 보자. 내부는 배열이지만, 밖에는 배열임을 드러내지 않는다.
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 문은 사실 이 반복자 위에서 돈다.
Bag<String> bag = new Bag<>();
// ...
for (String s : bag) {
System.out.println(s);
}컴파일러가 이 for-each를 아래와 사실상 같은 코드로 바꾼다.
Iterator<String> it = bag.iterator();
while (it.hasNext()) {
String s = it.next();
System.out.println(s);
}즉 for-each에 무언가를 넣으려면 그것이 Iterable이어야 한다. iterator()를 내줄 수 있어야 컴파일러가 위 코드로 풀어낼 수 있기 때문이다. 우리가 만든 Bag이 for-each에 그냥 들어간 이유가 이것이다. Iterable을 구현했으니 for-each가 뒤에서 반복자를 꺼내 돌린 것이다. 평소 무심코 쓰던 for-each가 실은 반복자 패턴을 부르고 있었던 셈이다.
실무: 자바 컬렉션 프레임워크
반복자 패턴은 자바 컬렉션 프레임워크 전체의 바탕이다. List, Set, Queue, Map의 키·값 뷰 - 이 모두가 Iterable이라 for-each로 훑을 수 있다.
우리가 ArrayList든 LinkedList든 HashSet이든 똑같은 for-each로 훑는 이유가 반복자 덕이다. ArrayList는 인덱스로, LinkedList는 노드 링크로 순회하지만, 그 차이는 각자의 반복자 안에 숨는다. 쓰는 쪽은 구현체를 바꿔 끼워도 순회 코드를 손대지 않는다. 내부가 배열이든 링크든 트리든, 밖에서는 “순서대로 훑는다”는 한 가지 방식으로 통일되는 것 - 이게 반복자가 실무에서 매일 하는 일이다.
정리
| 반복자란 | 내부 구조를 노출하지 않고 원소를 순서대로 훑는다 |
| 문제 | 순회 코드가 컬렉션의 내부 구조(인덱스·노드)에 묶인다 |
| 고침 | hasNext/next를 가진 반복자에 순회를 캡슐화 |
| 독립성 | 순회 상태를 반복자가 들어 여러 순회가 서로 방해 없음 |
| for-each | 사실 Iterable의 반복자를 꺼내 도는 문법 설탕 |
| 실무 | 자바 컬렉션 프레임워크 전체, for-each |
컬렉션의 속을 열지 않고 훑는 법만 반복자에 담아, 순회 코드와 내부 구조를 떼어 놓는 것 - 그게 반복자다.