프로그래밍/디자인패턴

이터레이터 패턴(Iterator Pattern) 완벽 정리

Jinwookoh 2025. 12. 7. 18:04

개발을 하다 보면 리스트, 배열, 트리 등 다양한 자료구조에 들어있는 데이터를 순회(Loop)해야 할 일이 수도 없이 생깁니다.

이때 **"데이터가 어떻게 저장되어 있는지(내부 구조)는 몰라도, 순서대로 꺼낼 수 있게 해주는 것"**이 이터레이터 패턴의 핵심입니다. 하지만 이 순회를 "내가 직접 할지" 아니면 **"맡길지"**에 따라 코드는 완전히 달라집니다.

Shutterstock
 

1. 방식 1: 외부 반복자 (External Iterator) - "능동적"

가장 고전적인 방식(C++, Java의 초기 스타일)입니다. 클라이언트가 **"다음 거 내놔(next)"**라고 직접 명령하며 순서를 제어합니다. 우리가 아는 Iterator<E> 인터페이스가 바로 이것입니다.

특징

  • 제어권: 클라이언트(사용자)에게 있습니다.
  • "언제 멈출지", "건너띌지"를 클라이언트가 결정합니다.

코드 예시

Java
 
// 자료구조 (Collection)
List<String> names = Arrays.asList("James", "Emily", "John");

// 1. 반복자(Iterator) 획득
Iterator<String> iterator = names.iterator();

// 2. 클라이언트가 직접 반복을 제어 (Explicit Control)
while (iterator.hasNext()) {
    String name = iterator.next();
    
    if (name.equals("Emily")) {
        System.out.println("찾았다 에밀리!");
        break; // 클라이언트 맘대로 멈출 수 있음
    }
}

장단점

  • 장점: 세밀한 제어가 가능합니다. (중간에 멈추기, 두 개의 리스트를 동시에 비교하며 돌리기 등)
  • 단점: 코드가 길어지고(Boilerplate), 클라이언트가 반복 상태를 관리해야 합니다.

2. 방식 2: 내부 반복자 (Internal Iterator) - "수동적"

Java 8(Lambda), JavaScript 등 현대적인 언어에서 주류가 된 방식입니다. 클라이언트는 "할 일(로직)"만 던져주고, 반복 자체는 컬렉션 내부에서 알아서 처리합니다.

특징

  • 제어권: 컬렉션(Iterator 구현체)에게 있습니다.
  • 클라이언트는 "어떻게(How)" 반복할지 신경 쓰지 않고, "무엇을(What)" 할지만 정의합니다.

코드 예시

Java
 
List<String> names = Arrays.asList("James", "Emily", "John");

// 클라이언트는 '로직(Lambda)'만 전달함. 반복은 알아서 돌아감.
names.forEach(name -> {
    System.out.println("이름: " + name);
});

// Stream API도 내부 반복자의 일종
names.stream()
    .filter(name -> name.startsWith("J"))
    .forEach(System.out::println);

장단점

  • 장점:
    • 가독성: 코드가 매우 간결하고 선언적(Declarative)입니다.
    • 병렬 처리: 반복 제어권이 라이브러리에 있으므로, parallelStream()만 붙이면 알아서 멀티스레드로 반복을 쪼개서 수행할 수 있습니다.
  • 단점: break나 return으로 중간에 반복을 멈추기가 까다롭거나 불가능합니다.

3. 실무 예시: 언제 무엇을 쓰는가?

자바의 Iterator 인터페이스(External)와 Stream API(Internal)가 완벽한 비교 대상입니다.

상황 A: 복잡한 비교가 필요할 때 (External)

두 개의 리스트를 동시에 순회하면서 비교해야 한다면 내부 반복자(forEach)로는 구현이 매우 어렵습니다.

Java
// 외부 반복자가 필요한 순간
while (iter1.hasNext() && iter2.hasNext()) {
    if (iter1.next().equals(iter2.next())) { ... }
}

상황 B: 대용량 데이터 처리 및 가독성 (Internal)

데이터를 필터링하고 변환하는 파이프라인 작업에는 내부 반복자가 압도적입니다.

Java
// 내부 반복자의 승리
list.stream()
    .filter(User::isActive)
    .map(User::getEmail)
    .forEach(this::sendEmail);

요약: External vs Internal

구분 External Iterator (for, while) Internal Iterator (forEach, stream)
제어 주체 클라이언트 (개발자) 라이브러리 (컬렉션)
스타일 명령형 (Imperative) 선언형 (Functional)
제어 유연성 높음 (break, continue 자유로움) 낮음 (중단하기 어려움)
병렬 처리 개발자가 직접 구현해야 함 (어려움) 라이브러리가 지원 (쉬움)

결론

단순히 코드를 짧게 줄이고 싶거나 병렬 처리가 필요하다면 내부 반복자(Internal, Stream) 가 정답입니다. 이것이 현대적인 코딩 스타일입니다.

하지만 반복 중간에 복잡한 제어(조건부 중단, 다중 컬렉션 제어)가 필요하다면, 고전적인 외부 반복자(External, Iterator) 를 사용하는 것을 두려워하지 마세요. 구관이 명관일 때가 있습니다.