무엇인가

컨테이너의 내부 구조를 감춘 채 원소를 차례로 훑을 수단을 준다.

앞의 두 편(Command·Strategy)은 접으면 이득만 있었다. 이 편은 다르다. 접을 수는 있는데 잃는 것이 분명히 있다. 그래서 이 갈래에서 가장 중요한 문서다.

구조

바깥에서 도는 방식과 안에서 도는 방식이 있다.

graph TD
    subgraph 외부 반복
        U1[사용하는 쪽] -->|++, != 를 직접| C1[컨테이너]
    end
    subgraph 내부 반복
        U2[사용하는 쪽] -->|클로져를 넘긴다| C2[컨테이너]
        C2 -->|원소마다 호출| U2
    end

루프를 누가 도는가가 갈린다. 위는 사용하는 쪽, 아래는 컨테이너 쪽이다.

외부 반복 (클래스판)

반복자를 만들어 돌려주고, 도는 일은 사용하는 쪽이 한다.

#include <iostream>
#include <string>
#include <vector>
 
struct Person {
    std::string name;
    int age;
};
 
class Persons {
    std::vector<Person> persons_;
 
  public:
    void add(Person p) { persons_.push_back(std::move(p)); }
 
    using iterator = std::vector<Person>::const_iterator;
    iterator begin() const { return persons_.begin(); }
    iterator end() const { return persons_.end(); }
};
 
int main() {
    Persons ps;
    ps.add({"수원", 36});
    ps.add({"지훈", 17});
    ps.add({"민아", 42});
 
    for (const auto& p : ps) std::cout << p.name << ' ';
    std::cout << '\n';
 
    // 도는 쪽이 흐름을 쥐고 있다 — 중간에 멈출 수 있다
    for (const auto& p : ps) {
        if (p.age < 20) {
            std::cout << "미성년 발견: " << p.name << " — 중단\n";
            break;
        }
    }
}

begin()/end()만 있으면 범위 기반 for가 그냥 돈다. C++에서는 이게 관용이다.

내부 반복 (람다판)

컨테이너가 돌고, 원소마다 클로져를 부른다.

#include <functional>
#include <iostream>
#include <string>
#include <vector>
 
struct Person {
    std::string name;
    int age;
};
 
class Persons {
    std::vector<Person> persons_;
 
  public:
    void add(Person p) { persons_.push_back(std::move(p)); }
 
    void each(const std::function<void(const Person&)>& fn) const {
        for (const auto& p : persons_) fn(p);
    }
};
 
int main() {
    Persons ps;
    ps.add({"수원", 36});
    ps.add({"지훈", 17});
    ps.add({"민아", 42});
 
    ps.each([](const Person& p) { std::cout << p.name << ' '; });
    std::cout << '\n';
 
    // 중간에 멈추고 싶다면? each는 끝까지 돈다.
    // 플래그로 흉내 내는 수밖에 없다 — 순회 자체는 계속된다.
    bool done = false;
    ps.each([&done](const Person& p) {
        if (done) return;
        if (p.age < 20) {
            std::cout << "미성년 발견: " << p.name << " — 이후는 건너뛴다\n";
            done = true;
        }
    });
}

사용하는 쪽에서 루프가 사라진 것은 사실이다. 그런데 대가가 있다.

무엇을 잃는가

외부 반복내부 반복
중간에 멈추기break불가. 플래그로 흉내만
두 컨테이너를 나란히반복자 둘을 같이 전진불가
표준 알고리즘 사용std::find_if 등 그대로직접 다시 만들어야
게으른 순회필요한 만큼만전부 돈다
루프 코드사용하는 쪽에 있다사라진다

마지막 한 줄만 내부 반복이 이긴다. 위 네 줄은 전부 잃는 쪽이다. 특히 break가 안 되는 것이 결정적이다 — 위 예제에서 “찾으면 그만”이 안 되고 끝까지 돌면서 플래그를 검사한다. 원소가 백만 개면 백만 번 다 돈다.

eachbool을 반환하게 만들어 “false면 중단”으로 고칠 수는 있다.

void eachUntil(const std::function<bool(const Person&)>& fn) const {
    for (const auto& p : persons_) {
        if (!fn(p)) return;
    }
}

되긴 되는데, 이제 클로져의 반환값이 “계속할지 말지”라는 규약이 생겼다. 읽는 사람이 그 규약을 알아야 한다. break는 아무도 설명할 필요가 없었다.

그래서 C++에서는

반복자를 내주는 쪽이 기본이다. begin()/end()만 있으면 범위 기반 for도, 표준 알고리즘도, 뷰도 전부 공짜로 딸려 온다.

// begin/end만 있으면 이 전부가 그냥 된다
std::find_if(ps.begin(), ps.end(), [](const Person& p) { return p.age < 20; });
std::count_if(ps.begin(), ps.end(), [](const Person& p) { return p.age >= 40; });
for (const auto& p : ps) { /* ... */ }

여기서도 람다는 쓰인다. 다만 반복 방식이 아니라 조건으로 쓰인다 — Strategy가 접힌 자리이지 Iterator가 접힌 자리가 아니다. 이 구분이 이 문서의 핵심이다.

언제 접고 언제 접지 않나

접지 않는다 — 기본값이다. begin()/end()를 내준다.

접는다

  • 순회 중에 락을 쥐고 있어야 할 때. 반복자를 내주면 잠금 범위를 통제할 수 없지만 each는 함수 안에서 락을 잡고 놓을 수 있다.
  • 순회가 단순하지 않을 때(트리 순회, 여러 저장소를 이어 훑기). 반복자를 제대로 만드는 비용이 크면 콜백이 싸다.
  • 내부 표현을 절대 노출할 수 없을 때.

블랙보드 패턴의 제어 루프가 두 번째 경우에 가깝다. 지식원들을 훑으며 조건을 묻는 순회라 반복자로 표현할 이유가 없다.