[Rust 시리즈 15편] 이터레이터와 클로저로 데이터 흐름 다듬기

English version

컬렉션을 다루다 보면 반복문이 길어지기 쉽습니다. Rust의 이터레이터클로저를 사용하면 동일한 로직을 더 짧고 선언적으로 표현할 수 있습니다. 이번 편에서는 Iterator 트레이트의 핵심 메서드, 클로저 문법, 그리고 둘을 조합한 데이터 처리 예제를 다룹니다.

클로저 문법부터 익히기

클로저는 주변 스코프의 값을 캡처할 수 있는 익명 함수입니다.

fn main() {
    let threshold = 50;
    let is_high = |score: i32| score > threshold;

    println!("70점은? {}", is_high(70));
}
  • |score: i32| score > threshold가 클로저입니다.
  • 타입을 생략하면 컴파일러가 추론해 줍니다.
  • 캡처 방식은 클로저 본문이 값을 어떻게 쓰는지에 따라 move, 불변 참조, 가변 참조 중 하나가 자동으로 선택됩니다. 필요하면 move 키워드를 붙여 소유권을 옮길 수 있습니다.

처음에는 아래처럼 이해하면 충분합니다.

  • 읽기만 하면 보통 불변 참조로 캡처
  • 수정하면 가변 참조로 캡처
  • 클로저 밖으로 값을 넘겨야 하면 move가 필요할 수 있음

Iterator 트레이트의 핵심

  • 모든 이터레이터는 next 메서드를 구현합니다.
  • 표준 컬렉션(Vec, HashMap, Range 등)은 iter, iter_mut, into_iter 메서드로 이터레이터를 만듭니다.
  • 체이닝 가능한 어댑터 메서드(map, filter, take, collect 등)를 제공해 파이프라인을 구성합니다.
fn main() {
    let scores = vec![45, 67, 88, 52];
    let high_scores: Vec<_> = scores
        .iter()
        .filter(|score| **score >= 60)
        .map(|score| format!("합격: {}", score))
        .collect();

    println!("{:?}", high_scores);
}
  • iter()는 불변 참조 이터레이터를, into_iter()는 값을 소비하는 이터레이터를 만듭니다.
  • collect()는 원하는 컬렉션 타입으로 결과를 모읍니다. 위 예제에서는 Vec<String>으로 추론됩니다.

다만 collect()는 결과 타입이 모호하면 컴파일러가 추론하지 못할 수 있습니다. 그럴 때는 let result: Vec<_> = ...처럼 결과 타입을 명시해 주면 됩니다.

소유권과 이터레이터

  • iter()&T, iter_mut()&mut T, into_iter()T를 반환합니다.
  • 가변 이터레이터에서 값을 수정하면 원본 컬렉션에 바로 반영됩니다.
fn main() {
    let mut names = vec!["yuna".to_string(), "min".to_string()];
    names.iter_mut().for_each(|name| name.make_ascii_uppercase());
    println!("{:?}", names);
}

사용자 정의 이터레이터 만들기

struct Counter {
    current: u32,
    max: u32,
}

impl Counter {
    fn new(max: u32) -> Self {
        Self { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = u32;

    fn next(&mut self) -> Option<Self::Item> {
        if self.current >= self.max {
            None
        } else {
            self.current += 1;
            Some(self.current)
        }
    }
}

fn main() {
    let sum: u32 = Counter::new(5).map(|n| n * 2).sum();
    println!("sum = {}", sum);
}
  • type Item으로 이터레이터가 내놓는 값의 타입을 정의합니다.
  • sum, product 같은 소비자 메서드는 이터레이터를 끝까지 순회하며 결과를 반환합니다.

클로저와 에러 처리 결합하기

  • filter_map, map_while 등을 사용하면 ResultOption을 섞은 로직을 부드럽게 표현할 수 있습니다.
fn parse_numbers(lines: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError> {
    lines
        .iter()
        .map(|line| line.trim().parse::<i32>())
        .collect()
}
  • collect()Result<Vec<_>, E> 형태를 자동으로 추론합니다. 실패하면 즉시 Err를 반환하고, 성공하면 모든 값을 모읍니다.

실습 과제

  1. Vec<User>에서 활성 사용자만 골라 name 목록을 만드는 파이프라인을 작성해 보세요.
  2. into_iter()iter() 차이를 확인하기 위해, 소유권을 이동시킨 뒤 원본 벡터를 출력해 보세요.
  3. Iterator를 직접 구현해 Fibonacci 수열을 만들어 보고, take(10).collect::<Vec<_>>()로 앞쪽 항목만 모아 보세요.

완료 기준

  • 클로저 문법과 캡처 규칙을 이해했다.
  • map, filter, collect, sum 같은 이터레이터 체인 메서드를 직접 사용해 봤다.
  • 사용자 정의 이터레이터를 선언하고 next 메서드를 구현해 봤다.

15편까지 마치면 컬렉션, 에러 처리, 모듈, 제네릭, lifetime, 이터레이터가 하나의 그림으로 연결됩니다. 이제 16편부터는 스마트 포인터와 힙 데이터로 Rust의 메모리 모델을 더 깊게 살펴봅니다.

💬 댓글

이 글에 대한 의견을 남겨주세요