소개
명시적인 for 루프는 컬렉션을 처리할 때 가장 이해하기 쉬운 방법인 경우가 많습니다. Rust는 반복자도 제공합니다. 반복자는 한 번에 하나의 항목을 생성하며, 읽기 쉬운 데이터 처리 파이프라인으로 조합할 수 있습니다.
작동하는 루프를 기준으로 유지하면서 iter와 sum으로 같은 합계를 재현하고, 간단한 클로저를 작성하고, map으로 측정값을 변환하고, filter로 잘못된 측정값을 제외한 다음, 마지막으로 이러한 연산을 조합합니다. 모든 수정은 준비된 Cargo 프로젝트에서 수행하므로 새로운 Rust 개념에 집중할 수 있습니다.
반복자로 루프의 합계 재현하기
이 단계에서는 준비된 for 루프와 같은 합계를 계산하는 반복자를 비교합니다.
프로젝트는 /home/labex/project/reading-pipeline에 있으며, 프로그램은 src/main.rs에 있습니다. 준비된 vec![12, 18, -1, 24, 30]은 해당 값이 들어 있는 벡터를 간결하게 만드는 vec! 매크로를 사용합니다. 앞에서 수행한 Vec 실습에서 빈 벡터를 만든 뒤 각 값을 추가하는 방식과 목적이 같습니다. 기존 루프는 &readings로 벡터를 빌리고, 각 &i32를 순회하며, *reading으로 참조가 가리키는 정수에 접근합니다.
프로젝트로 이동한 후 기준 프로그램을 실행합니다.
cd /home/labex/project/reading-pipeline
cargo run --quiet
Loop total: 83
이 합계에는 -1을 포함한 모든 측정값이 포함됩니다. 이제 소스 파일을 엽니다.
nano src/main.rs
Step 1 주석을 다음 코드로 바꿉니다.
let iterator_total: i32 = readings.iter().sum();
println!("Iterator total: {iterator_total}");
readings.iter()는 각 벡터 요소를 빌리는 반복자를 만듭니다. 벡터를 삭제하거나 소비하지는 않습니다. sum()은 생성된 항목을 계속 더하며, Rust 표준 라이브러리는 빌린 정수를 더하는 방법을 알고 있습니다. : i32 타입 표기는 sum이 생성해야 하는 결과 타입을 지정합니다.
Ctrl+O를 눌러 저장하고 Enter를 누른 다음 Ctrl+X로 종료합니다. 실행하기 전에 코드를 확인합니다.
cargo check
cargo run --quiet
Loop total: 83
Iterator total: 83
두 줄의 값이 같으므로 루프와 반복자가 같은 계산을 수행한다는 것을 알 수 있습니다. iter()는 벡터를 빌리기만 하므로 벡터를 이후 단계에서도 계속 사용할 수 있습니다.
클로저와 Map으로 측정값 변환하기
이 단계에서는 클로저를 정의하고 map으로 모든 측정값에 적용합니다.
클로저는 변수에 저장하거나 반복자 어댑터에 직접 전달할 수 있는 이름 없는 작은 함수입니다. |parameter: Type| expression 형식에서는 세로 막대 사이에 매개변수를 작성하고 표현식의 값을 반환합니다. 여기서는 보정 클로저가 오프셋 2를 더하도록 작성합니다.
소스 파일을 엽니다.
nano src/main.rs
Step 2 주석을 다음 코드로 바꿉니다.
let add_offset = |reading: i32| reading + 2;
let adjusted: Vec<i32> = readings.iter().map(|reading| add_offset(*reading)).collect();
println!("Adjusted all: {adjusted:?}");
map은 반복자의 각 항목마다 클로저를 한 번 호출하고, 클로저가 반환한 값을 생성합니다. 타입의 흐름은 iter()에서 나온 &i32 → *reading으로 복사한 i32 → add_offset에서 조정한 i32 → collect()로 만든 Vec<i32>의 요소입니다. :? 포맷 지정자는 벡터를 읽기 쉬운 디버그 형식으로 출력합니다.
저장하고 종료한 다음 확인하고 실행합니다.
cargo check
cargo run --quiet
새로 출력되는 마지막 줄은 다음과 같아야 합니다.
Adjusted all: [14, 20, 1, 26, 32]
잘못된 값인 -1도 1로 변환되었습니다. map은 항목을 변환하지만 어떤 항목을 남길지는 결정하지 않습니다.
Filter로 유효한 측정값만 유지하기
이 단계에서는 filter를 사용해 음수가 아닌 측정값만 유지하고 원래 벡터는 변경하지 않습니다.
반복자의 filter 어댑터는 각 후보에 대해 조건자 클로저를 호출합니다. 조건자는 항목을 유지할 때 true를 반환하고, 제외할 때 false를 반환합니다. 소스 파일을 엽니다.
nano src/main.rs
Step 3 주석을 다음 코드로 바꿉니다.
let valid: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.collect();
println!("Valid only: {valid:?}");
메서드 체인은 점 다음의 호출을 들여쓴 여러 줄에 이어서 작성할 수 있습니다. iter()는 벡터를 빌리고, copied()는 빌린 각 &i32 항목을 i32로 바꿉니다. 정수는 Copy를 구현하므로 이 작업은 비용이 적습니다. 그런 다음 filter 조건자는 각 i32 후보에 대한 임시 참조를 받아, 후보를 소비하지 않고 검사합니다. 따라서 비교할 때 *reading을 사용합니다. 타입의 흐름은 &i32 → 복사된 i32 → 유지된 i32 → Vec<i32>입니다.
저장하고 종료한 다음 실행합니다.
cargo run --quiet
마지막 줄은 다음과 같아야 합니다.
Valid only: [12, 18, 24, 30]
음수 값은 사라지고, 유지된 값은 원래 순서대로 남아 있습니다.
완전한 처리 파이프라인 조합하기
이 단계에서는 필터링과 매핑을 연결하고, 처리한 측정값을 수집한 다음 그 결과를 합산합니다.
반복자 어댑터는 지연 방식으로 동작합니다. filter와 map은 수행할 작업을 정의하지만, 소비 연산이 결과를 요청할 때만 항목이 어댑터를 통과합니다. collect와 sum이 소비 연산입니다. 이 연산들은 빌린 원본 벡터가 아니라 반복자 파이프라인을 소비합니다.
소스 파일을 엽니다.
nano src/main.rs
Step 4 주석을 다음 코드로 바꿉니다.
let valid_adjusted: Vec<i32> = readings
.iter()
.copied()
.filter(|reading| *reading >= 0)
.map(add_offset)
.collect();
let valid_total: i32 = valid_adjusted.iter().sum();
println!("Valid adjusted: {valid_adjusted:?}");
println!("Valid adjusted total: {valid_total}");
이 파이프라인은 먼저 -1을 제외한 다음, map(add_offset)으로 남은 각 i32를 앞에서 정의한 클로저에 전달합니다. 매개변수와 반환 타입이 어댑터에 맞으면 클로저 이름을 직접 전달할 수 있습니다. 두 번째 반복자는 수집된 벡터를 빌려 합계를 계산합니다.
저장하고 종료한 다음 확인하고 실행합니다.
cargo check
cargo run --quiet
마지막 두 줄은 다음과 같아야 합니다.
Valid adjusted: [14, 20, 26, 32]
Valid adjusted total: 92
연산 순서가 중요합니다. 매핑하기 전에 필터링해야 잘못된 측정값이 겉보기에는 유효한 양수로 변하는 것을 막을 수 있습니다. 원래 readings 벡터는 빌리기만 했으므로 이후에도 계속 사용할 수 있습니다.
요약
명시적인 루프와 반복자를 비교하고, 클로저를 작성해 재사용했습니다. 또한 map으로 값을 변환하고, filter로 값을 선택하고, 새 벡터를 수집하고, sum으로 합계를 계산했습니다. 각 파이프라인을 빌린 반복자로 시작해 원본 벡터도 유지했습니다.


