[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를 반환하고, 성공하면 모든 값을 모읍니다.

CodeSandbox로 이어서 실습하기

아래 샌드박스는 CodeSandbox의 Rust starter입니다. 이번 글의 핵심 코드를 src/main.rs에 옮기고, cargo checkcargo run 결과를 나란히 보면서 컴파일 메시지와 실행 출력을 비교해 보세요.

Live Practice

Rust Practice Sandbox

CodeSandbox

Run the starter project in CodeSandbox, compare it with the lesson code, and keep experimenting.

Rust startercargoterminal
  1. starter를 fork한 뒤 src/main.rs를 연다
  2. 본문 예제를 붙여 넣고 cargo check와 cargo run을 차례로 실행한다
  3. 타입, 값, 참조 흐름을 바꿔 컴파일 피드백과 출력 차이를 비교한다

Rust 실습은 브라우저 미리보기보다 터미널 피드백이 더 중요합니다. 여러 파일 구조나 추가 crate가 필요한 예제는 파일 배치를 조금 더 손봐야 할 수 있습니다.

실습 과제

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

완료 기준

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

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

💬 댓글

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