본문으로 건너뛰기

안동민 개발노트

본문 시작

반복자 기반 데이터 처리

Iterator의 next와 지연 평가를 이해하고 소비자·어댑터 메서드와 클로저를 조합해 컬렉션 처리를 구성합니다.

반복자 패턴은 일련의 아이템들에 대해 순서대로 어떤 작업을 수행할 수 있도록 해줍니다.

반복자는 각 아이템을 순회하고 언제 시퀀스가 종료될지 결정하는 로직을 담당합니다.

반복자를 사용하면, 그런 로직을 다시 구현할 필요가 없습니다.

러스트에서의 반복자는 게으른데, 이는 반복자를 사용하는 메서드를 호출하여 반복자를 소비하기 전까지는 동작을 하지 않는다는 의미입니다.

예를 들면, 예제 12-10의 코드는 Vec<T> 에 정의된 iter 메서드를 호출함으로써 벡터 v1에 있는 아이템들에 대한 반복자를 생성합니다.

이 코드 자체로는 어떤 유용한 동작도 하지 않습니다.

반복자 소스·상태·Item 소유권

STATE · ITERATOR NEXT

반복자 소스·상태·Item 소유권

반복자를 만드는 일은 아직 원소를 진행시키지 않는다. 소비자가 next(&mut self)를 요청할 때 위치가 변하고 Some(Item) 또는 현재의 None이 돌아오며, Item 소유권은 생성 방식에서 정해진다.

반복자 소스·상태·Item 소유권 공유 빌림, 가변 빌림, 소유 컬렉션이 준비된 반복자 상태로 들어가고 next 호출이 Some Item을 산출해 다시 준비 상태로 돌아가거나 None으로 현재 소진을 알린다. start / 생성 경로 선택 iter() [shared] / Item=&T iter_mut() [exclusive] / Item=&mut T into_iter() [owned] / Item=T create [iter] / pos=0 create [iter_mut] / ready create [into_iter] / ready next() [READY] / &mut self return [item 있음] / Some(Item) return [현재 없음] / None next() [계속] / 위치 진행 stop / 종료 생성 API 택 1 iter() Item = &T iter_mut() Item = &mut T into_iter() Item = T READY · 위치 보존 next() &mut self Some(Item) 한 원소 산출 · 상태 진행 None 현재 소진 · fused 별도
  1. 생성

    iter(), iter_mut(), into_iter()는 반복자 상태를 만들지만 그 자체로 원소를 꺼내지 않습니다.

  2. Item 계약

    일반적으로 iter&T, iter_mut&mut T, 소유 컬렉션의 into_iterT를 냅니다.

  3. 진행

    next(&mut self)가 내부 위치를 바꾸고 다음 원소가 있으면 Some(Item)을 반환합니다.

  4. 현재 소진

    지금 원소가 없으면 None이다. 기본 Iterator만으로 이후 모든 호출도 계속 None이라고 보장하지는 않습니다.

  5. 강한 종료 계약

    그 보장이 필요하면 FusedIterator 또는 fuse() 경계를 사용합니다.

forIntoIterator를 통해 동작한다. 소유 값·공유 빌림·가변 빌림 중 무엇을 반복하느냐가 컬렉션 보존과 Item 타입을 결정한다.

예제 12-10: 반복자 생성하기
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

반복자는 v1_iter 변수에 저장됩니다.

일단 반복자를 만들면, 다양한 방법으로 사용할 수 있습니다.

2장의 예제 2-5에서는 각 아이템에 대해 어떤 코드를 실행하기 위해 for 루프를 사용하여 어떤 배열에 대한 반복을 수행했습니다.

내부적으로는 암묵적으로 반복자를 생성한 다음 소비하는 것이었지만, 지금까지는 이게 정확히 어떻게 동작하는지에 대해서 대충 넘겼습니다.

예제 12-11의 예제에서는 for 루프에서 반복자를 사용하는 부분으로부터 반복자 생성을 분리했습니다.

v1_iter에 있는 반복자를 사용하여 for 루프가 호출되면, 반복자의 각 요소가 루프의 한 순번마다 사용되는데, 여기서는 각각의 값을 출력합니다.

예제 12-11: for 루프에서 반복자 사용하기
    let v1 = vec![1, 2, 3];

    let v1_iter = v1.iter();

    for val in v1_iter {
        println!("Got: {}", val);
    }

표준 라이브러리에서 반복자를 제공하지 않는 언어에서는, 아마도 변수를 인덱스 0으로 시작하고, 그 변수를 인덱스로 사용하여 벡터에서 값을 꺼내오고, 루프 안에서 벡터가 가진 아이템의 전체 개수에 다다를 때까지 그 변숫값을 증가시키는 것으로 동일한 기능을 작성할 것입니다.

반복자는 그러한 모든 로직을 대신 처리하여 잠재적으로 엉망이 될 수 있는 반복적인 코드를 줄여 줍니다.

반복자는 벡터처럼 인덱스를 사용할 수 있는 자료구조 뿐만 아니라, 많은 다른 종류의 시퀀스에 대해 동일한 로직을 사용할 수 있도록 더 많은 유연성을 제공합니다.

반복자가 어떻게 그런 작동을 하는지 살펴봅시다.


Iterator 트레이트와 next 메서드

모든 반복자는 표준 라이브러리에 정의된 Iterator라는 이름의 트레이트를 구현합니다.

트레이트의 정의는 아래처럼 생겼습니다.

pub trait Iterator {
    type Item;

    fn next(&mut self) -> Option<Self::Item>;

    // 기본 구현이 있는 메서드는 생략했습니다
}

이 정의에 새로운 문법 몇 가지가 사용된 것에 주목하세요: type ItemSelf::Item은 이 트레이트에 대한 연관 타입(associated type) 을 정의합니다.

연관 타입에 대해서는 19장에서 더 자세히 이야기하겠습니다.

현재로서는 이 코드에서 Iterator 트레이트를 구현하려면 Item 타입도 함께 정의되어야 하며, 이 Item 타입이 next 메서드의 반환 타입으로 사용된다는 것만 알면 됩니다.

바꿔 말하면, Item 타입은 반복자로부터 반환되는 타입이 되겠습니다.

Iterator 트레이트는 구현하려는 이에게 딱 하나의 메서드 정의를 요구합니다.

바로 next 메서드인데, 이 메서드는 Some으로 감싼 반복자의 아이템을 하나씩 반환하고, 반복자가 종료될 때는 None을 반환합니다.

반복자의 next 메서드를 직접 호출할 수 있습니다; 예제 12-12는 벡터로부터 생성된 반복자에 대하여 next를 반복적으로 호출했을 때 어떤 값들이 반환되는지 보여줍니다.

예제 12-12: 반복자의 next 메서드 호출하기
src/lib.rs
    #[test]
    fn iterator_demonstration() {
        let v1 = vec![1, 2, 3];

        let mut v1_iter = v1.iter();

        assert_eq!(v1_iter.next(), Some(&1));
        assert_eq!(v1_iter.next(), Some(&2));
        assert_eq!(v1_iter.next(), Some(&3));
        assert_eq!(v1_iter.next(), None);
    }

v1_iter를 가변으로 만들 필요가 있음을 주의하세요: 반복자에 대한 next 메서드 호출은 반복자 내부의 상태를 변경하여 반복자가 현재 시퀀스의 어디에 있는지 추적합니다.

바꿔 말하면, 이 코드는 반복자를 소비(consume), 즉 다 써 버립니다.

next에 대한 각 호출은 반복자로부터 하나의 아이템을 소비합니다.

for 루프를 사용할 때는 v1_iter를 가변으로 만들 필요가 없는데, 루프가 v1_iter의 소유권을 갖고 내부적으로 가변으로 만들기 때문입니다.

또한 next 호출로 얻어온 값들은 벡터 내의 값들에 대한 불변 참조자라는 점도 주의하세요.

iter 메서드는 불변 참조자에 대한 반복자를 생성합니다.

만약 v1의 소유권을 얻어서 소유한 값을 반환하도록 하고 싶다면, iter 대신 into_iter를 호출할 수 있습니다.

비슷하게, 가변 참조자에 대한 반복자가 필요하면, iter 대신 iter_mut을 호출할 수 있습니다.


반복자를 소비하는 메서드

Iterator 트레이트에는 표준 라이브러리에서 기본 구현을 제공하는 여러 가지 메서드가 있습니다; 이 메서드들은 표준 라이브러리 API 문서의 Iterator 트레이트에 대한 부분을 살펴보면 찾을 수 있습니다.

이 메서드들 중 일부는 정의 부분에서 next 메서드를 호출하는데, 이것이 Iterator 트레이트를 구현할 때 next 메서드를 구현해야만 하는 이유입니다.

next를 호출하는 메서드들을 소비 어댑터(consuming adaptor) 라고 하는데, 호출하면 반복자를 소비하기 때문에 그렇습니다.

한 가지 예로 sum 메서드가 있는데, 이는 반복자의 소유권을 가져온 다음 반복적으로 next를 호출하는 방식으로 순회하며, 따라서 반복자를 소비합니다.

전체를 순회하면서 현재의 합계값에 각 아이템을 더하고 순회가 완료되면 합계를 반환합니다.

예제 12-13은 sum 메서드 사용 방식을 보여주는 테스트입니다.

예제 12-13: sum 메서드를 호출하여 반복자의 모든 아이템에 대한 합계 구하기
src/lib.rs
    #[test]
    fn iterator_sum() {
        let v1 = vec![1, 2, 3];

        let v1_iter = v1.iter();

        let total: i32 = v1_iter.sum();

        assert_eq!(total, 6);
    }

sum은 반복자를 소유하여 호출하므로, sum을 호출한 이후에는 v1_iter의 사용이 허용되지 않습니다.


다른 반복자를 생성하는 메서드

소비자가 당겨 실행하는 지연 반복자 체인

FLOWCHART · DEMAND-DRIVEN

소비자가 당겨 실행하는 지연 반복자 체인

mapfilter는 소스 반복자를 값으로 받아 변환 규칙을 저장할 뿐 즉시 원소를 진행시키지 않는다. filter 클로저는 shoe_size 같은 주변 선택 상태를 캡처하며, collect, sum 같은 소비자가 요청할 때 필요한 원소만 체인을 통과한다.

소비자가 당겨 실행하는 지연 반복자 체인 소스 반복자, map, filter, consumer가 한 체인을 이루며 소비자의 요청은 뒤에서 앞으로 전달되고 각 원소는 map과 filter를 통과해 필요한 만큼만 결과로 물질화된다. PULL · consumer가 next 요청을 뒤로 전달 SOURCE iter() Some(Item) ADAPTER map Item → U ADAPTER filter &Item → bool captures shoe_size consumer collect · sum · find MATERIALIZE OR SHORT-CIRCUIT 필요한 결과만 확정 NO INTERMEDIATE Vec REQUIRED · FINAL collect MAY ALLOCATE · DEMAND CAN STOP EARLY
  1. 어댑터 구성

    mapfilterself를 값으로 받아 이전 반복자를 새 어댑터 안으로 이동시키고 규칙을 저장합니다.

  2. 소비자 요청

    collect, sum, find 같은 소비자가 다음 원소를 요구할 때만 뒤에서 앞으로 next 요청이 전달됩니다.

  3. 원소 평가

    소스 원소는 필요한 만큼 map을 거친다. filter 클로저는 shoe_size 같은 주변 선택 상태를 캡처하고 predicate가 참인 원소만 통과시킵니다.

  4. 조기 종료

    find, any, take는 소스를 끝까지 순회하지 않을 수 있습니다.

  5. 물질화 경계

    단순 체인에는 중간 Vec가 필요 없지만 collect는 최종 컬렉션을 할당할 수 있고 클로저 자체도 할당할 수 있습니다.

지연성은 평가 시점을 뜻할 뿐 전체 파이프라인의 무할당 보장이 아니다. filter predicate는 Item의 참조를 받고 mapItem을 값으로 받는다.

반복자 어댑터(iterator adaptor)Iterator 트레이트에 정의된 메서드로 반복자를 소비하지 않습니다.

대신 원본 반복자의 어떤 측면을 바꿔서 다른 반복자를 제공합니다.

예제 12-14는 반복자 어댑터 메서드인 map을 호출하는 예를 보여주는데, 클로저를 인수로 받아서 각 아이템에 대해 호출하여 아이템 전체를 순회합니다.

map 메서드는 수정된 아이템들을 생성하는 새로운 반복자를 반환합니다.

여기에서의 클로저는 벡터의 각 아이템에서 1이 증가한 새로운 반복자를 만듭니다.

예제 12-14: 반복자 어댑터 map을 호출하여 새로운 반복자 생성하기
src/main.rs
    let v1: Vec<i32> = vec![1, 2, 3];

    v1.iter().map(|x| x + 1);

하지만 이 코드는 다음과 같은 경고를 발생시킵니다.

$ cargo run
   Compiling iterators v0.1.0 (file:///projects/iterators)
warning: unused `Map` that must be used
 --> src/main.rs:4:5
  |
4 |     v1.iter().map(|x| x + 1);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^
  |
  = note: iterators are lazy and do nothing unless consumed
  = note: `#[warn(unused_must_use)]` on by default

warning: `iterators` (bin "iterators") generated 1 warning
    Finished dev [unoptimized + debuginfo] target(s) in 0.47s
     Running `target/debug/iterators`

예제 12-14의 코드는 아무것도 하지 않습니다; 넘겨진 클로저는 결코 호출되지 않습니다.

위 경고는 이유가 무엇인지 상기시켜 줍니다.

반복자 어댑터는 게으르고, 반복자를 여기서 소비할 필요가 있다는 것을요.

이 경고를 수정하고 반복자를 소비하기 위해서 collect 메서드를 사용할 것인데, 12장의 예제 12-1에서 env::args와 함께 사용했었지요.

이 메서드는 반복자를 소비하고 결괏값을 모아서 컬렉션 데이터 타입으로 만들어 줍니다.

예제 12-15에서는 벡터에 map을 호출하여 얻은 반복자를 순회하면서 결과를 모읍니다.

이 벡터는 원본 벡터로부터 1씩 증가한 아이템들을 담고 있는 상태가 될 것입니다.

예제 12-15: map을 호출하여 새로운 반복자를 생성한 다음 collect 메서드를 호출하여 이 반복자를 소비하고 새로운 벡터 생성하기
src/main.rs
    let v1: Vec<i32> = vec![1, 2, 3];

    let v2: Vec<_> = v1.iter().map(|x| x + 1).collect();

    assert_eq!(v2, vec![2, 3, 4]);

map이 클로저를 인수로 받기 때문에, 각 아이템에 대해 수행하고자 하는 어떤 연산이라도 지정할 수 있습니다.

이는 Iterator 트레이트가 제공하는 반복 동작을 재사용하면서 클로저로 동작의 일부를 커스터마이징할 수 있게 해주는 방법을 보여주는 훌륭한 예입니다.

반복자 어댑터의 호출을 연결시키면 복잡한 동작을 읽기 쉬운 방식으로 수행할 수 있습니다.

하지만 모든 반복자는 게으르므로, 반복자 어댑터를 호출한 결과를 얻기 위해서는 소비 어댑터 중 하나를 호출해야만 합니다.


환경을 캡처하는 클로저 사용하기

많은 반복자 어댑터는 클로저를 인수로 사용하고, 보통 반복자 어댑터의 인수에 명시되는 클로저는 자신의 환경을 캡처하는 클로저일 것입니다.

이러한 예를 들기 위해 클로저 인수를 사용하는 filter 메서드를 사용해보겠습니다.

이 클로저는 반복자로부터 아이템을 받아서 bool을 반환합니다.

만일 클로저가 true를 반환하면, 그 값을 filter에 의해 생성된 반복자에 포함시키게 됩니다.

클로저가 false를 반환하면 해당 값은 포함시키지 않습니다.

리스트 13-13에서는 환경으로부터 shoe_size를 캡처하는 클로저를 가지고 filter를 사용하여 Shoe 구조체 인스턴스의 컬렉션을 순회합니다.

이는 지정된 크기의 신발만 반환해 줄 것입니다.

예제 12-16: shoe_size를 캡처하는 클로저로 filter 메서드 사용하기
src/lib.rs
#[derive(PartialEq, Debug)]
struct Shoe {
    size: u32,
    style: String,
}

fn shoes_in_size(shoes: Vec<Shoe>, shoe_size: u32) -> Vec<Shoe> {
    shoes.into_iter().filter(|s| s.size == shoe_size).collect()
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn filters_by_size() {
        let shoes = vec![
            Shoe {
                size: 10,
                style: String::from("sneaker"),
            },
            Shoe {
                size: 13,
                style: String::from("sandal"),
            },
            Shoe {
                size: 10,
                style: String::from("boot"),
            },
        ];

        let in_my_size = shoes_in_size(shoes, 10);

        assert_eq!(
            in_my_size,
            vec![
                Shoe {
                    size: 10,
                    style: String::from("sneaker")
                },
                Shoe {
                    size: 10,
                    style: String::from("boot")
                },
            ]
        );
    }
}

shoes_in_size 함수는 매개변수로 신발들의 벡터에 대한 소유권과 신발 크기를 받습니다.

이 함수는 지정된 크기의 신발들만을 담고 있는 벡터를 반환합니다.

shoes_in_size의 본문에서는 into_iter를 호출하여 이 벡터의 소유권을 갖는 반복자를 생성합니다.

그다음 filter를 호출하여 앞의 반복자를 새로운 반복자로 바꾸는데, 새로운 반복자에는 클로저가 true를 반환하는 요소들만 담겨있게 됩니다.

클로저는 환경에서 shoe_size 매개변수를 캡처하고 각 신발의 크기와 값을 비교하여 지정된 크기의 신발만 유지하도록 합니다.

마지막으로, collect를 호출하면 적용된 반복자에 의해 반환된 값을 벡터로 모으고, 이 벡터가 함수에 의해 반환됩니다.

이 테스트는 shoes_in_size를 호출했을 때 지정된 값과 동일한 크기인 신발들만 돌려받는다는 것을 보여 줍니다.

반복자 체인은 만들기, 변환하기, 소비하기가 분리되어 있으므로 체인의 마지막에 어떤 소비 지점이 있는지 확인하는 습관이 중요합니다.


반복자로 일련의 아이템들 처리하기 학습 판단 기준표

반복자로 일련의 아이템들 처리하기 마지막 점검은 실제 호출 모양과 실패 처리 경계가 자연스럽게 이어지는지에 맞춥니다.