[모던 C++로 다시 쓰는 GoF 디자인 패턴 총정리 #16] 이터레이터(Iterator) 패턴: Ranges와 Concepts로 컬렉션 순회 재구성하기

이전 글에서는 인터프리터(Interpreter) 패턴을 모던 C++ 관점에서 재해석하며, 상속 기반 Expression 클래스 계층 없이도 std::variant, std::visit, Concepts, std::expected, Ranges, coroutine 등을 활용해 언어 해석 로직을 단순하고 확장성 높게 구현할 수 있음을 확인했습니다. 이번 글에서는 행동(Behavioral) 패턴 중 이터레이터(Iterator) 패턴을 다룹니다.

이터레이터 패턴은 컬렉션의 내부 표현을 노출하지 않고, 요소에 접근하는 통일된 인터페이스를 제공하는 패턴입니다. 전통적으로는 컬렉션 별로 Iterator 인터페이스를 정의하고, begin()/end() 또는 next(), hasNext() 등의 메서드를 통해 요소 순회가 가능했습니다. 그러나 C++ STL 자체가 iterator 개념을 내장하고 있고, C++20에서는 Ranges 라이브러리를 통해 훨씬 단순하고 타입 안전하며 선언적인 순회 방식을 제공합니다. 또한 Concepts, coroutine, std::expected, std::format 등의 기능을 결합해 더욱 유연한 순회 패턴을 만들 수 있습니다.

패턴 소개: 이터레이터(Iterator)

의도:

  • 컬렉션(컨테이너)을 순회하는 방법을 객체(이터레이터)로 캡슐화해, 컬렉션 내부 구조를 노출하지 않고도 요소 접근 제공.
  • 전통적 구현에서 이터레이터는 한정된 기능(다음 요소 접근, 끝 확인)을 제공.
  • STL 이터레이터, range-based for, C++20 Ranges가 이미 이터레이터 개념을 잘 반영.

전통적 구현 문제점:

  • 사용자 정의 컨테이너에 맞추어 Iterator 클래스를 정의해야 함
  • next(), hasNext() 등의 메서드 수동 구현 필요
  • 비동기 스트림, 조건부 순회, 에러 처리 등 고급 기능 구현 어려움

기존 C++ 스타일 구현 (C++11/14/17 이전, 전통적 방식)

예를 들어, 간단한 컬렉션(MyCollection)과 그에 대한 이터레이터(MyIterator)를 정의하는 전통적 방식:

#include <vector>
#include <iostream>

struct MyIterator {
    using value_type = int;
    std::vector<int>* data;
    size_t pos;
    bool hasNext() { return pos < data->size(); }
    int next() { return (*data)[pos++]; }
};

struct MyCollection {
    std::vector<int> vec;
    MyIterator createIterator() {
        return MyIterator{&vec, 0};
    }
};

int main() {
    MyCollection c{{1,2,3}};
    auto it = c.createIterator();
    while(it.hasNext()) {
        std::cout << it.next() << " ";
    }
    std::cout << "\n"; // 1 2 3
}

문제점:

  • 컬렉션마다 Iterator 클래스를 직접 정의해야 함
  • next/hasNext 수동 구현 필요
  • 에러 처리나 비동기 스트림, 조건부 필터링 등 고급 기능 구현 어려움

모던 C++20 이상의 개선: Ranges와 Concepts

C++20에서 Ranges 라이브러리는 이미 컬렉션 순회를 위한 강력한 추상화를 제공합니다. STL 컨테이너는 range-based for나 Ranges 알고리즘을 통해 쉽게 순회 가능하므로, 별도 이터레이터 패턴 구현 필요성 자체가 크게 감소했습니다.

1. Ranges를 통한 간단한 순회

#include <ranges>
#include <vector>
#include <iostream>

int main() {
    std::vector<int> vec = {1,2,3};
    for (auto v : vec | std::views::transform([](int x){return x*2;})) {
        std::cout << v << " ";
    }
    std::cout << "\n"; // 2 4 6
}

비교:

  • 전통적: Iterator 클래스를 직접 정의해야 하는 반면, STL 컨테이너는 이미 이터레이터를 제공
  • C++11/14/17: range-based for는 간단하지만 고급 변환/필터링은 STL 알고리즘 사용 필요, Ranges 미지원
  • C++20 이상: Ranges로 변환, 필터링, 결합 등 파이프라인 형태로 선언적 표현 가능

2. Concepts로 사용자 정의 Range 지원

Concepts를 사용해 사용자 정의 컬렉션이 Ranges 컨셉(such as std::ranges::range)를 만족하도록 하면, 별도 Iterator 패턴 구현 없이도 STL 알고리즘과 views를 활용할 수 있다.

// 가상의 사용자 정의 컨테이너
struct MyContainer {
    std::vector<int> data;
};

// MyContainer를 std::ranges::range 콘셉트를 만족시키려면 begin/end 함수 제공
auto begin(MyContainer& c) { return c.data.begin(); }
auto end(MyContainer& c) { return c.data.end(); }

static_assert(std::ranges::range<MyContainer>);

이제 MyContainer도 Ranges 알고리즘에 바로 활용 가능:

MyContainer c{{1,2,3}};
for(auto v : c | std::views::filter([](int x){return x>1;})) {
    std::cout << v << " "; // 2 3
}
std::cout << "\n";

비교:

  • 전통적: Iterator 클래스 정의 필요
  • C++11/14/17: begin/end로 range-based for 가능하나 고급 파이프라인 표현은 어려움
  • C++20: Ranges와 Concepts로 사용자 정의 컬렉션을 쉽게 range-compatible하게 만들어 재사용성 극대화

3. 에러 처리와 std::expected 응용

이터레이터 패턴에서 에러 상황(예: 외부 데이터 스트림에서 읽기 실패)이 발생할 수 있다면, std::expected를 반환하는 코루틴 기반 이터레이터를 구현 가능.
예를 들어, 비동기 I/O 스트림 이터레이션:

#include <coroutine>

// 코루틴 기반 이터레이터 개념 가상 예제
// co_await 로 외부 데이터 읽고, 실패 시 std::unexpected 반환

비교:

  • 전통적: 에러 처리 복잡, 예외나 bool 반환 뿐
  • C++11/14/17: Future/Promise로 비동기 처리 가능하나 코드 복잡
  • C++20: coroutine + std::expected로 비동기 스트림 이터레이션 명확하고 타입 안전하게 구현 가능

4. std::format으로 로깅

이터레이션 과정에서 로깅 필요 시 std::format:

for (auto v : vec | std::views::transform([](int x){return x*2;})) {
    std::cout << std::format("Value: {}\n", v);
}

비교:

  • 전통적: 로깅 시 별도 데코레이터 필요
  • C++11/14/17: printf 또는 iostream 직렬화
  • C++20: std::format으로 깔끔하고 타입 안전한 형식 지정

5. 파이프라인 조합, 조건부 이터레이션

Ranges로 파이프라인 조합, 필터, transform, take_while 등 다양한 조건부 이터레이션 가능:

for (auto v : c
     | std::views::filter([](int x){return x%2==0;})
     | std::views::take_while([](int x){return x<10;})) {
    std::cout << v << " ";
}

비교:

  • 전통적: 조건부 순회 시 if문 사용, 명령형 코드 증가
  • C++11/14/17: STL 알고리즘 조합 가능하지만 다소 장황
  • C++20: Ranges로 선언적 파이프라인, 유지보수성 탁월

전통적 구현 vs C++11/14/17 vs C++20 이상 비교

전통적 (C++98/03):

  • Iterator 인터페이스나 클래스 정의 필요
  • 컬렉션마다 전용 이터레이터 구현 → 클래스 증가
  • 조건부 순회, 변환, 비동기 스트림 처리 어려움

C++11/14/17 시대:

  • range-based for 도입으로 단순 순회 개선
  • 여전히 Ranges 부재로 고급 파이프라인 표현 어려움
  • 비동기 처리, 에러 처리 시 별도 메커니즘 필요

C++20 이상(모던 C++):

  • Ranges, Concepts로 컬렉션 순회 단순화, 상속 없이도 범위 호환 컬렉션 정의 가능
  • std::expected, std::format, coroutine 등과 결합해 에러 처리, 로깅, 비동기 순회, 조건부 파이프라인 등 고급 기능 선언적으로 구현 가능
  • 유지보수성, 확장성, 코드 가독성 모두 향상
  • 전통적 이터레이터 패턴의 필요성 자체가 감소, Ranges로 대부분의 요구 충족

결론

이터레이터 패턴은 컬렉션 순회를 캡슐화하는 중요한 패턴이지만, 전통적 구현은 별도의 이터레이터 클래스 정의와 상속 기반 구조로 번거로웠습니다. C++11/14/17로 넘어오면서 range-based for로 단순 순회는 쉬워졌으나, 고급 요구사항(조건부 필터, 변환, 비동기 순회, 에러 처리 등)에 대응하는 데 한계가 있었습니다.

C++20 이상에서는 Ranges, Concepts, std::expected, coroutine, std::format 등을 활용해 상속 없이 선언적이고 확장성 높은 이터레이션을 구현할 수 있습니다. 파이프라인 형태의 Ranges 표현으로 조건부/필터링/변환된 순회를 간단히 정의하고, coroutine으로 비동기 순회를, std::expected로 에러 처리, std::format으로 로깅 등 다양한 기능을 적은 코드로 구현 가능합니다.

결국 모던 C++에서는 이터레이터 패턴 자체를 GoF 시절보다 훨씬 더 강력하고 직관적으로 재구성할 수 있으며, 전통적 구현 대비 유지보수성과 확장성이 크게 향상됩니다.

반응형