Iterator 패턴: 컬렉션 내부를 감추고 순회 로직만 남기는 법

Iterator 패턴으로 컬렉션 내부 구조를 감추고 순회 로직을 캡슐화하는 방법, 외부/내부 반복자 선택, fail-fast 설계를 Java 예제로 정리한다.

2026-08-13 · 최초 발행 2025-10-14

컬렉션이 배열이든 트리든 그래프든, 순회하는 코드는 똑같은 모양이어야 한다. 이걸 컬렉션 구현과 분리해내는 게 Iterator 패턴이다. 컬렉션의 내부 표현을 숨기고 같은 인터페이스로 원소를 순차 접근하게 하는 행동 패턴으로, 순회 로직(상태·경계·에러 처리)과 컬렉션의 책임을 분리한다. 외부 반복자(external)와 내부 반복자(internal) 두 형태가 있고, 커서 일관성 유지·경계 초과 방지·동시 수정 검출(옵션, fail-fast)·예외 일관성이 지켜야 할 불변 조건이다.

구성 요소가 하는 일

Iterator는 hasNext, next, 필요하면 remove 같은 인터페이스를 정의하고, 커서 상태·경계 검사·예외 정책을 캡슐화한다. Aggregate(컬렉션)는 iterator를 생성할 책임을 갖고 내부 구조(배열/트리/그래프)를 감추며, 필터링·역방향·레벨순 같은 다양한 iterator 변형을 제공할 수 있다. Concrete Iterator는 컬렉션 구조에 맞춘 실제 순회 구현체로, fail-fast나 지연 변환, 맵/필터 데코레이션을 구현한다. Client는 iterator 인터페이스에만 의존하므로 컬렉션 구현이 바뀌어도 영향을 덜 받는다.

외부 반복자는 제어권을 클라이언트가 쥐고 있어 유연하게 조합할 수 있고, 내부 반복자는 제어권을 컬렉션이 쥐고 있어 간결하고 병렬화하기 쉽다. 동시 수정을 다루는 방식도 갈린다 — Fail-Fast는 동시 수정을 감지하면 즉시 예외를 던지고, Fail-Safe는 스냅샷 기반으로 안전하게 순회하는 대신 메모리 비용이 늘어난다.

설계할 때 거치는 단계

요구 분석에서는 순회 단위와 순서(정방향/역방향/레벨순), 필터·변환 요구사항을 정하고 동시성 모델(단일 스레드, fail-fast, 스냅샷)을 결정한다. 인터페이스를 정의할 때는 hasNext·next를 최소 연산군으로 두고 필요하면 peek/remove/reset을 추가하며, 리소스 관리가 필요하면 Closeable/AutoCloseable을 채택한다. 상태 캡슐화 단계에서는 커서·경계·변형 버전(modCount) 같은 내부 상태를 감추고, 지연평가를 쓴다면 캐시·프리패치 정책을 명시한다. 경계·예외 정책에서는 경계 초과 시 NoSuchElementException을, 동시 수정 시 ConcurrentModificationException이나 스냅샷을 쓸지 정한다. 마지막으로 단위·프로퍼티 테스트(경계·역방향·필터 조합)로 동시성과 성능을 검증하고, O(1) 접근을 유지하면서 박스화·할당을 최소화한다.

구조를 그림으로 보면

ClientAggregate+iterator() : IteratorConcreteAggregate-elements : Element[*]-modCount : int+iterator() : IteratorIterator+hasNext() : bool+next() : ElementConcreteIterator-cursor : int-expectedModCount : int+hasNext() : bool+next() : ElementElement

재생목록으로 보는 반복자 구현

환경은 JDK 17 이상, 표준 라이브러리만 쓰고 단일 스레드 또는 fail-fast 정책을 가정한다. 목표는 컬렉션 내부 구조를 감추고 fail-fast 외부 반복자를 구현하는 것이다.

// file: Playlist.java
import java.util.*;

public final class Playlist implements Iterable<Song> {
    private final List<Song> songs = new ArrayList<>();
    private int modCount = 0;

    public void add(Song s) {
        Objects.requireNonNull(s);
        songs.add(s);
        modCount++;
    }

    public boolean removeByTitle(String title) {
        boolean removed = songs.removeIf(s -> s.title().equals(title));
        if (removed) modCount++;
        return removed;
    }

    @Override
    public Iterator<Song> iterator() {
        return new PlaylistIterator();
    }

    private final class PlaylistIterator implements Iterator<Song> {
        private int cursor = 0;
        private final int expectedModCount = modCount;

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

        @Override
        public Song next() {
            checkForComodification();
            if (cursor >= songs.size()) throw new NoSuchElementException();
            return songs.get(cursor++);
        }

        @Override
        public void remove() {
            throw new UnsupportedOperationException("immutable iterator");
        }

        private void checkForComodification() {
            if (expectedModCount != modCount) throw new ConcurrentModificationException();
        }
    }
}

record Song(String title, String artist, int durationSec) {}

// file: Main.java
public class Main {
    public static void main(String[] args) {
        Playlist pl = new Playlist();
        pl.add(new Song("Alpha", "A", 210));
        pl.add(new Song("Beta", "B", 180));

        for (Song s : pl) {
            System.out.println(s.title() + " - " + s.artist());
            // 동시 수정 테스트:
            // pl.add(new Song("Gamma","C",200)); // => ConcurrentModificationException
        }
    }
}

경계를 넘어서 next를 호출하면 NoSuchElementException이, 순회 중 컬렉션이 바뀌면 ConcurrentModificationException(fail-fast)이 발생한다. remove를 지원하지 않으면 UnsupportedOperationException을 던진다. 필터 반복자는 new FilterIterator<>(pl.iterator(), s -> s.durationSec() > 200)처럼 감쌀 수 있고, 역방향 반복자는 커서 초기값을 size - 1로 두고 hasNext는 cursor >= 0, next는 get(cursor--)로 구현하면 된다.

실제로 쓰이는 곳

대용량 데이터를 페이지 단위로 다룰 때 커서 기반 반복자로 API 호출 수를 최적화하고, 백오프·재시도를 포함한 네트워크 반복자로 캡슐화한다. 트리·그래프 순회에서는 DFS/BFS/레벨순 같은 전략을 반복자 교체만으로 제공할 수 있어 내부 구조가 바뀌어도 클라이언트 코드는 그대로다. 파일·소켓의 라인 반복자는 AutoCloseable과 엮어 자원 누수를 막고, 버퍼링·프리패치·압축 해제 로직을 모듈화한다. 클라우드 SDK의 리스트 연산에서는 지연 페이지네이션 반복자를 제공하고, 필터·정렬 조합을 데코레이터 반복자로 구성하기도 한다.

반복문·경계·예외 처리를 재사용하면 실무 기준으로 순회 관련 중복 코드를 20~40% 줄일 수 있다는 관찰이 있다. 컬렉션 구조가 바뀌어도 클라이언트 영향이 최소화돼 인터페이스 안정성이 올라가고, 지연 평가와 배치 프리패치로 API 호출·할당을 줄이면서도 대용량에서 O(1) 증분 접근을 유지할 수 있다.

안전성과 성능을 맞바꾸는 지점

경계·동시성·자원 해제에 일관된 예외 정책을 세우는 게 기본이다. I/O·네트워크 리소스를 쥔 반복자는 Closeable Iterator로 만들어 try-with-resources를 쓰게 하고, 필터·매핑·제한(take)·스킵(skip) 반복자를 체이닝하는 데코레이터 조합도 흔하다.

다만 Fail-Fast는 안전성이 오르는 대신 메모리·지연이 늘고, 외부 반복자의 유연성은 내부 반복자의 간결함·병렬 처리 용이성과 맞바꿔야 한다. remove를 지원하면 구현 복잡도와 커서·버전 관리 부담이 함께 오른다.

외부 반복자와 내부 반복자를 나란히 보면

관점 외부 반복자(External) 내부 반복자(Internal)
성능 사용자 제어 최적화 용이, 불필요 연산 회피 프레임워크 최적화 가능, 병렬화 쉬움
확장성 필터/맵/조합 자유도 높음 콜백 기반 확장, 표현력 제약 가능
일관성 예외·경계 정책을 구현체가 통일 콜백 예외 전파/취소 처리 설계 필요
안정성(동시성) fail-fast로 조기 감지 스냅샷/스트림으로 안전, 메모리 비용
운영 편의 디버깅/스텝 단위 추적 용이 코드 간결, 리소스 자동 관리 용이
디자인 패턴Iterator 패턴행위 패턴순회 로직소프트웨어공학