270. 이터레이터 (Iterator) - 내부 표현 노출 없이 순차 접근
핵심 인사이트 (3줄 요약)
- 본질: 이터레이터(Iterator) 패턴은 컬렉션(List, Set, Tree 등)의 내부 데이터 구조(배열인지, 연결 리스트인지 등)를 외부(클라이언트)에 노출하지 않고도 그 안의 모든 요소들을 순차적으로 접근(순회)할 수 있게 해주는 행동(Behavioral) 패턴이다.
- 가치: 컬렉션 객체는 오직 '데이터 보관'이라는 본연의 책임만 지고, '순회'라는 부가적인 책임은 이터레이터 객체로 분리(SRP 준수)함으로써 서로 다른 데이터 구조라도 동일한 인터페이스(
hasNext(),next())로 일관되게 탐색할 수 있는 다형성을 제공한다.- 융합: 오늘날 거의 모든 프로그래밍 언어(Java의
Iterator, C++의IEnumerator, Python의__iter__, JS의[Symbol.iterator])와 향상된for-each문의 기반이 되는 내장 표준 기술로 융합되어, 개발자가 직접 이 패턴을 바닥부터 짜는 일은 드물어졌다.
Ⅰ. 개요 및 필요성 (Context & Necessity)
-
개념: 이터레이터 패턴은 집합체(Aggregate) 안에 들어있는 요소들을 탐색하는 방법을 '반복자(Iterator)'라는 별도의 객체로 캡슐화하는 설계 기법이다.
-
필요성: 책꽃이(컬렉션)에 책이 꽂혀있다고 치자. 어떤 책꽂이는 배열(Array) 구조이고, 어떤 것은 연결 리스트(Linked List), 어떤 것은 트리(Tree) 구조다. 만약 클라이언트가 책을 순서대로 꺼내 보려면, 각각의 책꽂이가 어떤 자료구조로 되어 있는지 내부 사정을 낱낱이 알아야만 한다. 자료구조가 바뀌면 클라이언트의 탐색 코드(for 루프)도 통째로 고쳐야 한다. 이러한 구조적 종속성을 끊어낼 독립적인 '탐색 전담반'이 필요했다.
-
💡 비유: 박물관에서 관람객(클라이언트)이 전시품(데이터)을 순서대로 볼 때, 박물관의 내부 지도(자료구조)를 직접 들고 헤매는 대신, 큐레이터(이터레이터)를 고용하는 것과 같습니다. 관람객은 그저 "다음 작품 보여주세요(
next())"라고만 하면 됩니다. -
등장 배경 및 발전 과정:
- 컬렉션과 순회 로직의 뒤엉킴: 과거 C언어 시절, 구조체를 순회하기 위해 포인터 연산과 인덱스 제어 변수(
int i)가 비즈니스 로직에 난무했고,IndexOutOfBounds에러가 빈번했다. - Iterator 패턴의 제시 (GoF): 자료구조와 순회 알고리즘을 분리하여
Iterator인터페이스를 제시했다. - 언어 레벨의 내장화 (Syntactic Sugar): 패턴이 너무나도 유용하여 Java(1.5+), C# 등 대부분의 언어가 아예
for (Item item : items)형태의 문법적 설탕(Syntactic Sugar)으로 이 패턴을 언어 표준으로 흡수해 버렸다.
- 컬렉션과 순회 로직의 뒤엉킴: 과거 C언어 시절, 구조체를 순회하기 위해 포인터 연산과 인덱스 제어 변수(
-
📢 섹션 요약 비유: 식당에서 주방(컬렉션)이 어떻게 생겼고 음식을 어떻게 보관하는지 알 필요 없이, 서빙 로봇(이터레이터)에게 "다음 요리 가져와!"라고 버튼만 누르면 되는 완벽한 역할 분담입니다.
Ⅱ. 아키텍처 및 핵심 원리 (Deep Dive)
구성 요소 (클래스 다이어그램)
| 요소명 | 역할 | 비유 |
|---|---|---|
| Iterator (반복자 인터페이스) | 컬렉션의 요소들을 순회하기 위한 공통 인터페이스. 주로 hasNext()(다음 요소가 있는가?), next()(다음 요소 반환) 메서드를 정의한다. | 큐레이터의 공통 자격증 |
| ConcreteIterator | 특정 컬렉션 구조에 맞게 순회 알고리즘을 실제로 구현한 객체. 현재 순회 중인 '커서(Cursor) 위치'를 상태로 기억한다. | '미술관 전용' 큐레이터, '공룡관 전용' 큐레이터 |
| Aggregate (집합체 인터페이스) | 이터레이터를 생성해 반환하는 팩토리 메서드 createIterator()를 정의하는 인터페이스. | 가이드 지원이 가능한 박물관 인증 마크 |
| ConcreteAggregate | 요소들을 보관하는 실제 자료구조(List, Tree 등). 클라이언트의 요청이 오면 자신에게 알맞은 ConcreteIterator를 생성해 넘겨준다. | 실제 루브르 박물관, 자연사 박물관 |
동작 메커니즘 (코드 뼈대 구조)
이터레이터 패턴의 핵심은 클라이언트가 '어떻게(How) 꺼낼 것인가'를 고민하지 않고 '꺼낸 것을 어떻게 쓸 것인가'에만 집중할 수 있게 해준다는 점이다.
┌─────────────────────────────────────────────────────────────┐
│ 이터레이터 패턴의 다형적 순회 원리 │
├─────────────────────────────────────────────────────────────┤
│ │
│ [Client (손님)] │
│ │ │
│ │ 1. "반복자 좀 줘" ( createIterator() 호출 ) │
│ ▼ │
│ [Aggregate (메뉴판)] ───(생성)──▶ [Iterator (서빙 로봇)] │
│ - ArrayList 기반 + hasNext() : boolean │
│ - Array 기반 등 + next() : Object │
│ │
│ │ 2. while (iterator.hasNext()) { │
│ │ Item i = iterator.next(); │
│ │ print(i.getName()); │
│ │ } │
│ └───────────────────────────────────────────────────▶ │
│ │
│ ※ 클라이언트는 메뉴판이 배열인지 리스트인지 전혀 모른다. │
│ 오직 서빙 로봇(Iterator)의 hasNext()와 next()만 호출할 뿐이다. │
└─────────────────────────────────────────────────────────────┘
[다이어그램 해설] 클라이언트 코드(예: printMenu)는 두 종류의 서로 다른 식당 메뉴판(하나는 Array, 하나는 ArrayList 사용)을 받아서 출력해야 한다. 과거라면 두 개의 다른 for 루프(하나는 menu[i], 하나는 menu.get(i))를 짜야 했지만, 이터레이터 패턴을 적용하면 두 메뉴판 모두 createIterator()를 통해 표준화된 Iterator 객체를 던져주므로, 클라이언트는 단 하나의 while 루프만으로 두 식당의 메뉴를 모두 출력할 수 있다. OCP를 완벽히 준수하는 구조다.
단일 책임 원칙 (SRP, Single Responsibility Principle)의 실현
집합체(컬렉션) 객체는 본질적으로 '데이터를 삽입/삭제/보관'하는 1차적 책임을 갖는다. 만약 집합체 내부에 순회 상태(예: 현재 몇 번째 인덱스를 읽고 있는지 currentIndex 변수)까지 저장하게 되면, 여러 스레드나 여러 클라이언트가 동시에 순회할 때 커서 값이 엉켜버리는 재앙이 발생한다.
이터레이터 패턴은 '순회 상태(커서 위치)'를 집합체에서 완전히 분리해 독립적인 Iterator 객체에 담아낸다. 덕분에 동일한 리스트에 대해 A 클라이언트와 B 클라이언트가 각자 서로 다른 Iterator 객체를 생성받아, 동시에 독립적인 위치부터 순회하는 것이 안전하게 가능해진다.
- 📢 섹션 요약 비유: 책꽂이(컬렉션)에 "지금 내가 몇 번째 책까지 봤지?"라는 포스트잇(커서)을 직접 붙이면 동생이 볼 때 헷갈리지만, 이터레이터(개인용 독서 노트)를 따로 만들어 각자 몇 쪽까지 읽었는지 기록하면 서로 전혀 방해받지 않는 것과 같습니다.
Ⅲ. 융합 비교 및 다각도 분석
1. 이터레이터 패턴 vs 비지터 패턴 (Visitor Pattern)
두 패턴 모두 컬렉션 내부의 요소들을 처리하기 위해 고안되었으나, '누가 주도권을 쥐는가'가 완전히 다르다.
| 비교 항목 | 이터레이터 패턴 (Iterator) | 비지터 패턴 (Visitor) |
|---|---|---|
| 주 목적 | 요소들에 **순차적으로 접근(탐색)**하는 것 자체 | 요소들에 새로운 동작(연산)을 일괄 적용하는 것 |
| 제어의 주도권 | **클라이언트(외부)**가 주도 (next()를 명시적 호출) | **데이터 구조(내부)**가 주도 (요소들을 넘기며 accept()) |
| 데이터 구조의 한계 | 단일 타입 컬렉션(List 등) 순회에 적합 | 서로 다른 타입의 객체가 섞인 복잡한 트리 구조에 적합 |
| 비유 | 손님이 뷔페를 돌며 직접 음식을 하나씩 집어오는 것 | 청소부가 일렬로 늘어선 방을 지나가며 방 타입에 맞게 청소하는 것 |
과목 융합 관점
-
소프트웨어 공학 (SE): 객체 지향 언어(Java, C# 등)에서 컬렉션 프레임워크 설계의 근간이다. Java의
java.lang.Iterable인터페이스를 상속받은 객체만이for (Item item : collection)과 같은 향상된 for문(Enhanced For-loop)을 사용할 수 있도록 언어 차원의 문법 사상으로 융합되었다. -
데이터베이스 (DB): 관계형 DBMS가 SQL 커서(Cursor)를 이용해 쿼리 결과 집합(ResultSet)을 한 행(Row)씩 애플리케이션으로 올려보내는 메커니즘(
rs.next())이 이터레이터 패턴의 가장 전형적인 엔터프라이즈 적용 사례다. 전체 데이터를 메모리에 다 올리지 않고 한 건씩 스트리밍 처리할 수 있게 해준다. -
📢 섹션 요약 비유: 이터레이터는 내가 원할 때마다 자판기 버튼(
next)을 눌러 캔 깡통을 하나씩 빼먹는 능동적인 구조이고, DB의 커서(Cursor)도 이 자판기 원리를 그대로 차용하여 메모리 과부하를 막는 것입니다.
Ⅳ. 실무 적용 및 기술사적 판단
실무 시나리오
-
시나리오 — 대용량 데이터 파일 파싱 메모리 초과 (OOM): 50GB짜리 로그 파일(
.csv)을 읽어 필터링한 후 DB에 넣어야 한다. 초급 개발자가List<String> lines = Files.readAllLines()를 사용해 50GB를 한 번에 RAM에 올리려다OutOfMemoryError(OOM)를 발생시켜 서버가 다운되었다.- 아키텍트의 해결책: 데이터 전체를 메모리에 올리는 배열 기반 처리 대신, Lazy Evaluation(지연 평가) 기반의 이터레이터 패턴을 적용해야 한다. Java의
Files.lines()나Scanner를 통해 파일 스트림을 이터레이터로 감싸면,next()가 호출될 때마다 디스크에서 딱 한 줄(1줄)만 메모리로 올려 읽고 버리는 구조가 되어, 아무리 거대한 파일이라도 OOM 없이 안정적인 처리가 보장된다.
- 아키텍트의 해결책: 데이터 전체를 메모리에 올리는 배열 기반 처리 대신, Lazy Evaluation(지연 평가) 기반의 이터레이터 패턴을 적용해야 한다. Java의
-
시나리오 — 복잡한 트리 구조(조직도)의 평탄화 순회 필요성: 회사 조직도가 부서 하위에 팀, 팀 하위에 파트가 있는 '컴포지트 패턴(Composite Pattern)' 다차원 트리로 구성되어 있다. 인사팀은 전체 직원의 연봉 합계를 구하기 위해 조직도의 구조(깊이)에 상관없이 직원을 '한 줄'로 평탄화해서 순차적으로 읽고 싶어 한다.
- 아키텍트의 해결책: 트리를 전위 순회(Pre-order Traversal)하는 로직을 내장한
TreeIterator(또는CompositeIterator)를 구현하여 조직도 루트 객체에 부착한다. 클라이언트는 트리의 깊이나 재귀 함수(Recursion) 구현의 복잡성을 전혀 신경 쓰지 않고, 그저while(iterator.hasNext())로 직원들만 평면적으로 뽑아내어 연봉을 합산할 수 있게 된다.
- 아키텍트의 해결책: 트리를 전위 순회(Pre-order Traversal)하는 로직을 내장한
도입 체크리스트
- 기술적: 다중 스레드 환경에서 컬렉션을 순회하는 도중, 다른 스레드가 요소를 추가/삭제하면 끔찍한 충돌이 발생한다. 이를 막기 위해 순회 중 변경을 감지하면 즉시
ConcurrentModificationException을 던지는 Fail-Fast (빠른 실패) 이터레이터나, 데이터를 복사해 순회하는 Fail-Safe 이터레이터 중 어떤 동시성 전략을 취할 것인가? - 설계적: 최근의 현대적인 언어(Kotlin, Python 등)는
yield키워드를 이용한 제너레이터(Generator)로 이터레이터를 코루틴처럼 아주 쉽게 생성할 수 있다. 굳이 무거운 별도의ConcreteIterator클래스를 수동으로 타이핑할 필요가 있는지 언어 스펙을 먼저 확인했는가?
안티패턴
-
내부 구조의 은연중 노출: 이터레이터 인터페이스를 쓰면서도,
iterator.getIndex()나iterator.getCurrentNode()처럼 특정 자료구조(배열이나 트리)에서만 의미가 있는 특화된 메서드를 열어버리는 행위. 이는 추상화의 장벽을 부수고 클라이언트 코드를 다시 종속시키는 최악의 실수다. -
📢 섹션 요약 비유: 이터레이터를 쓰면서 "지금 배열의 3번 인덱스인가요?"라고 묻는 것은, 서빙 로봇에게 "너 지금 부엌의 두 번째 냉장고에서 꺼내왔어?"라고 따져 묻는 것과 같습니다. 로봇은 그냥 순서대로 주기만 할 뿐이어야 완벽한 분업이 유지됩니다.
Ⅴ. 기대효과 및 결론
정량/정성 기대효과
| 구분 | 구조 노출 (for 루프 인덱스 제어) | 이터레이터 패턴 도입 후 | 개선 효과 |
|---|---|---|---|
| 정량 | 자료구조(List → Set) 변경 시 클라이언트 순회 로직 100% 수정 | 클라이언트 로직 수정 0줄 | 내부 구조 변경에 대한 유지보수 비용 제로화 (OCP 준수) |
| 정량 | 대용량 파일 Load-All 방식 사용 시 메모리 점유율 100% (OOM) | Iterator Stream 방식 사용 시 메모리 1% 미만 | 메모리 효율성 및 시스템 안정성 극대화 |
| 정성 | 개발자가 배열 길이 체크, 인덱스 증감 오류(Off-by-one) 실수 잦음 | 향상된 for-loop(내부 Iterator)로 구조적 오류 원천 차단 | 휴먼 에러 제거 및 코드 가독성 향상 |
미래 전망
- 스트림(Stream API)과 함수형 프로그래밍으로의 흡수: 단순한 이터레이터 패턴의
while-next문법조차 길고 명령형(Imperative)이라는 비판을 받아, 최근에는list.stream().filter().map()과 같은 내부 반복자(Internal Iterator) 체계로 완전히 대체되고 있다. 개발자는 루프를 직접 짜지 않고 "무엇을 할 것인가"만 함수로 넘기면, 컬렉션이 내부에서 알아서(심지어 병렬로) 이터레이팅을 수행한다. - 비동기 이터레이터 (Async Iterator): JavaScript의
for await...of처럼, 데이터를 네트워크나 DB에서 실시간 비동기로 받아오면서 순회할 수 있는 비동기 스트림 이터레이터 패러다임이 웹/서버 통신 기술의 핵심으로 떠오르고 있다.
참고 표준
- GoF (Gang of Four): Behavioral Patterns - Iterator
- Java Collection Framework:
java.util.Iterator,java.lang.Iterable - ECMAScript 6 (JS): Iteration protocols (
Symbol.iterator), Generator 함수
이터레이터 패턴은 객체 지향 역사에서 가장 성공적이고 보편적으로 채택된 패턴이다. 너무나 성공한 나머지 언어의 기본 문법 속에 완전히 녹아들어 이제는 개발자가 디자인 패턴이라는 사실조차 인지하지 못하고 쓸 정도가 되었다. 기술사적 관점에서 이 패턴의 진정한 가치는 단순히 코드를 깔끔하게 하는 것을 넘어, 방대한 데이터를 **지연 평가(Lazy Evaluation)**로 잘게 쪼개어 시스템 메모리의 한계를 극복하는 엔터프라이즈 아키텍처의 구명줄 역할에 있음을 통찰해야 한다.
- 📢 섹션 요약 비유: 이터레이터 패턴은 너무 훌륭하게 설계되어 이제는 자동차의 '자동 변속기'처럼 모든 프로그래밍 언어의 기본 사양이 되었습니다. 굳이 수동 기어(인덱스 변수
i)를 조작할 필요 없이 부드러운 드라이빙(순회)을 즐기게 해준 발명품입니다.
📌 관련 개념 맵 (Knowledge Graph)
| 개념 명칭 | 관계 및 시너지 설명 |
|---|---|
| 컴포지트 패턴 (Composite Pattern) | 복잡한 트리 구조를 가진 컴포지트 패턴의 객체들을 클라이언트가 평면 구조처럼 순회하기 위해 이터레이터 패턴을 필수적으로 부착한다. |
| 팩토리 메서드 (Factory Method) | 집합체(Aggregate)가 자신에게 맞는 구체적인 이터레이터를 생성하여 반환(createIterator())할 때 사용되는 패턴. |
| 단일 책임 원칙 (SRP) | 컬렉션에서 '데이터 보관'과 '순회 알고리즘'이라는 두 가지 책임을 분리하여 Iterator 객체를 탄생시킨 핵심 객체 지향 원칙. |
| 스트림 (Stream API) | 외부 반복자(External Iterator)인 GoF 이터레이터의 한계를 넘어, 순회와 필터링을 컬렉션 내부로 숨긴(Internal Iterator) 최신 패러다임. |
| 지연 평가 (Lazy Evaluation) | 이터레이터의 next()가 호출될 때만 데이터를 평가(로드)함으로써, 무한한 데이터나 대용량 파일을 메모리 오버플로우 없이 순회하는 기법. |
👶 어린이를 위한 3줄 비유 설명
- 서랍장(컬렉션)에 장난감이 가득 들어있는데, 어떤 건 박스에 어떤 건 그물망에 들어있어서 꺼내는 방법이 다 달라요.
- 하지만 엄마가 서랍장마다 **'자동 배출기(이터레이터)'**를 달아주셨어요. 그래서 나는 그저 "다음 장난감 줘(
next())" 버튼만 누르면 된답니다. - 장난감이 어디에 어떻게 보관되어 있는지 몰라도, 똑같은 버튼 하나로 모든 장난감을 순서대로 구경할 수 있게 해주는 마법이 바로 **'이터레이터 패턴'**이에요!