Rust 심화: 반복자와 어댑터

5 minute read

안녕하세요! 여러분의 코딩 구원투수, 재준봇입니다!

자, 여러분. 지금까지 러스트(Rust)라는 거대한 산을 오르느라 정말 고생 많으셨습니다. 아마 여기까지 오셨다면 “아니, 러스트는 왜 이렇게 깐깐해?”라고 생각하셨을 수도 있어요. 하지만 걱정 마세요. 오늘 배울 ‘반복자와 어댑터’ 파트까지만 마스터하면, 여러분은 러스트의 진짜 매력, 즉 “우아한 코드”를 작성하는 단계에 진입하시게 됩니다.

오늘 내용은 정말 중요합니다. 이거 모르면 러스트를 쓴다고 말할 수 없을 정도로 핵심적인 기능이거든요. 하지만 어렵게 생각하실 필요 없습니다. 제가 아주 찰떡같은 비유로 뇌에 직접 때려 박아 드릴 테니까요. 준비되셨나요? 바로 시작합니다!


28강: Rust 심화 - 반복자와 어댑터: 데이터의 고속도로를 설계하라!

여러분, 혹시 공장의 컨베이어 벨트를 상상해 보신 적 있나요? 원재료가 벨트 위에 올라가면, 중간에 기계들이 붙어서 “불량품 제거”, “색칠하기”, “포장하기” 같은 작업을 쭉쭉 진행하죠. 그리고 마지막에 박스에 담기면 끝납니다.

러스트의 반복자(Iterator)가 바로 이 컨베이어 벨트입니다! 데이터를 하나씩 꺼내서 처리하는 아주 효율적인 방법이죠.

1. 반복자(Iterator)란 무엇인가?

쉽게 말해 반복자는 “컬렉션(벡터, 배열 등)의 요소들을 순차적으로 방문하는 도구”입니다.

여기서 진짜 중요한 포인트가 있습니다. 러스트의 반복자는 게으릅니다(Lazy). “아니, 공부하는 학생이 게으르면 안 되는데 코딩 도구가 게으르다고?”라고 생각하시겠죠? 하지만 이건 엄청난 장점입니다. 실제로 결과가 필요할 때까지는 아무런 작업도 하지 않고 대기하고 있다가, 마지막에 “자, 이제 결과 줘!”라고 요청하는 순간에만 움직이거든요. 메모리와 CPU를 엄청나게 아낄 수 있는 비결이죠.

반복자를 구현하는 3가지 방법

데이터를 하나씩 꺼내는 방법은 생각보다 다양합니다. 상황에 따라 어떤 것을 쓸지 결정해야 해요.

fn main() {
    let numbers = vec![10, 20, 30];

    // 방법 1: 가장 익숙한 for 문 (내부적으로 IntoIterator를 사용합니다)
    println!("--- 방법 1: for 문 ---");
    for num in &numbers {
        println!("숫자: {}", num);
    }

    // 방법 2: .iter() 메서드 사용 (요소들을 '참조'만 합니다. 원본을 유지하고 싶을 때 사용!)
    println!("--- 방법 2: .iter() ---");
    let iter_ref = numbers.iter();
    for num in iter_ref {
        println!("참조값: {}", num);
    }

    // 방법 3: .into_iter() 메서드 사용 (소유권을 가져옵니다. 원본이 파괴되어 더 이상 못 씁니다!)
    println!("--- 방법 3: .into_iter() ---");
    let iter_own = numbers.into_iter();
    for num in iter_own {
        println!("소유권 획득: {}", num);
    }
    // 여기서 numbers를 다시 쓰려고 하면 컴파일 에러가 납니다! 소유권이 넘어갔거든요.
}

[코드 뜯어보기]

  • for num in &numbers: 가장 간단한 방법입니다. &를 붙여서 원본을 빌려오기 때문에 안전합니다.
  • .iter(): 명시적으로 반복자를 생성합니다. “나는 이 데이터들을 읽기만 하겠어!”라는 의지를 보여주는 코드입니다.
  • .into_iter(): 이건 진짜 화끈한 놈입니다. 데이터를 완전히 가져와 버립니다. 이후에 numbers 변수를 사용하려고 하면 러스트 컴파일러가 “야! 이거 이미 썼잖아!”라고 소리를 지를 겁니다.

2. 반복자 어댑터(Iterator Adapters): 데이터 가공의 마법

자, 이제 컨베이어 벨트(반복자)를 깔았으니, 그 위에 기계들을 설치할 차례입니다. 이걸 러스트에서는 어댑터(Adapter)라고 부릅니다.

어댑터의 특징은 반복자를 받아서 다시 반복자를 반환한다는 점입니다. 즉, 체인처럼 계속 연결할 수 있다는 뜻이죠. “필터링하고 -> 변환하고 -> 다시 필터링하고” 이런 식으로요.

꼭 알아야 할 핵심 어댑터 3가지

가장 많이 쓰이는 map, filter, take를 살펴보겠습니다.

fn main() {
    let v = vec![1, 2, 3, 4, 5, 6];

    // map: 모든 요소를 특정 규칙에 따라 변환합니다. (1 -> 10, 2 -> 20 ...)
    // filter: 조건에 맞는 요소만 남깁니다. (짝수만 남겨라!)
    // take: 앞에서부터 지정한 개수만큼만 가져옵니다. (앞에서 2개만!)
    
    let result: Vec<i32> = v.iter()
        .map(|x| x * 10)              // 1. 모든 숫자에 10을 곱한다 (10, 20, 30, 40, 50, 60)
        .filter(|x| *x > 30)         // 2. 30보다 큰 숫자만 남긴다 (40, 50, 60)
        .take(2)                     // 3. 그중 앞의 2개만 가져온다 (40, 50)
        .collect();                  // 4. 최종적으로 벡터로 모은다 (Consumer)

    println!("최종 결과: {:?}", result); // 출력: [40, 50]
}

[코드 뜯어보기]

  • .map(|x| x * 10): 입력값을 받아 새로운 값으로 바꾸는 녀석입니다. 여기서는 10배를 곱했습니다.
  • .filter(|x| *x > 30): 불리언(true/false)을 반환하는 클로저를 넣습니다. true인 녀석만 통과시키고 나머지는 버립니다.
  • .take(2): 무한한 데이터 흐름이 있을 때 특히 유용합니다. “딱 2개만 받고 끝낼게!”라고 선언하는 것이죠.

여기서 잠깐! 위 코드에서 .collect()가 없었다면 어떻게 되었을까요? 아무 일도 일어나지 않습니다! 어댑터들은 “계획”만 세우는 녀석들이기 때문입니다. 실제로 일을 시키는 ‘소비자’가 나타나야 비로소 작동합니다.


3. 소비자(Consumers): 마침표를 찍는 사람들

어댑터가 가공 기계라면, 소비자는 최종 결과물을 박스에 담는 포장원입니다. 소비자가 호출되는 순간, 그동안 쌓아왔던 모든 어댑터 체인이 한꺼번에 실행됩니다.

대표적인 소비자 3가지

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];

    // 소비자 1: .collect() - 반복자의 결과를 다시 컬렉션(Vec, HashMap 등)으로 모읍니다.
    let collected: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("Collect 결과: {:?}", collected);

    // 소비자 2: .sum() - 모든 요소의 합계를 구합니다.
    let total: i32 = numbers.iter().sum();
    println!("Sum 결과: {}", total);

    // 소비자 3: .fold() - 초기값을 가지고 요소를 하나하나 합쳐나갑니다. (가장 강력한 도구!)
    // 0부터 시작해서- 현재까지의 합(acc)에 다음 숫자(x)를 더한다.
    let folded = numbers.iter().fold(0, |acc, x| acc + x);
    println!("Fold 결과: {}", folded);
}

[코드 뜯어보기]

  • .collect(): 가장 많이 쓰입니다. 다만, 어떤 타입으로 모을지 명시해 줘야 합니다 (Vec<i32> 처럼).
  • .sum(): 아주 간단하게 합계를 낼 때 씁니다.
  • .fold(initial, closure): sum의 확장판입니다. 단순히 더하기뿐만 아니라, 곱하기나 문자열 합치기 등 아주 복잡한 누적 계산을 할 때 사용합니다.

🚨 실무주의보: “어? 왜 타입 에러가 나죠?”

상황: .collect()를 썼는데 컴파일러가 type annotations needed라며 화를 냅니다.

원인: .collect()는 매우 똑똑해서 여러 가지 컬렉션(Vec, BTreeSet, LinkedList 등)으로 변환될 수 있습니다. 그래서 컴파일러 입장에서는 “도대체 어떤 바구니에 담으라는 거야?”라고 묻는 것입니다.

해결책:

  1. 변수 선언 시 타입을 명시하세요: let result: Vec<_> = ...collect();
  2. 터보피쉬(Turbofish) 문법을 사용하세요: ...collect::<Vec<i32>>(); (화살표 모양 ::<>가 물고기 같아서 터보피쉬라고 부릅니다. 진짜 신기하죠?)

❓ 초보자 폭풍 질문!

Q: for 문을 써도 되는데, 왜 굳이 이렇게 복잡하게 .iter().map().filter().collect() 이런 체인을 쓰나요?

A: 아주 좋은 질문입니다! 이유는 크게 두 가지입니다.

첫째, 가독성입니다. for 문 안에 if 문을 넣고, 그 안에 또 변수를 만들어 값을 수정하는 코드를 작성하면 금방 스파게티 코드가 됩니다. 하지만 체인 방식은 “데이터를 가져와서 -> 이렇게 바꾸고 -> 저런 건 버리고 -> 모은다”라는 흐름이 한눈에 보입니다.

둘째, 성능(효율성)입니다. 앞서 말씀드린 ‘게으른 평가’ 덕분에, 중간 단계에서 임시 벡터를 계속 만들 필요가 없습니다. 모든 처리가 단 한 번의 순회(Iteration) 과정에서 이루어지기 때문에 메모리 효율이 압도적으로 좋습니다.


🚀 종합 실습: 실제 서비스처럼 짜보기

마지막으로, 지금까지 배운 모든 것을 합쳐보겠습니다. 우리는 ‘학생들의 성적 리스트’에서 80점 이상인 학생들만 골라 5점의 가산점을 주고, 그들의 점수 총합을 구하는 기능을 구현해 보겠습니다.

fn main() {
    let scores = vec![75, 90, 60, 85, 100, 40];

    // [가공 프로세스]
    // 1. 반복자 생성 (.iter())
    // 2. 80점 이상만 필터링 (.filter())
    // 3. 5점 가산점 부여 (.map())
    // 4. 합계 계산 (.sum())
    
    let total_bonus_score: i32 = scores.iter()
        .filter(|&&s| s >= 80)      // 80점 이상인 사람만 통과! (참조의 참조라 && 사용)
        .map(|&s| s + 5)            // 통과한 사람들에게 5점 보너스!
        .sum();                     // 최종 합산!

    println!("가산점이 적용된 우수 학생들의 총점: {}", total_bonus_score);
    // 계산 과정: (90+5) + (85+5) + (100+5) = 95 + 90 + 105 = 290
}

[최종 분석]

  1. .filter(|&&s| s >= 80): 여기서 &&가 나오는 이유는 .iter()가 참조를 주고, .filter()가 또 그 참조를 받기 때문입니다. 당황하지 마세요! 그냥 “값에 접근하기 위해 껍질을 두 번 벗긴다”고 생각하시면 됩니다.
  2. .map(|&s| s + 5): 여기서도 &를 사용해 참조값을 실제 값으로 가져와 계산합니다.
  3. .sum(): 마지막 소비자 호출로 인해 모든 연산이 한꺼번에 쏟아지며 결과값이 나옵니다.

마무리하며

여러분, 오늘 배운 반복자와 어댑터는 러스트 코딩의 “꽃”이라고 할 수 있습니다. 처음에는 이 체인 방식이 낯설 수 있지만, 익숙해지면 for 문으로는 절대 느낄 수 없는 쾌감을 느끼실 거예요. 코드가 간결해지고, 의도가 명확해지며, 성능까지 챙길 수 있으니까요.

오늘 강의가 도움이 되셨나요? 혹시 이해가 안 가는 부분이 있다면 언제든 질문 주세요. 재준봇이 여러분의 뇌에 데이터가 쏙쏙 들어갈 때까지 설명해 드리겠습니다!

그럼 다음 강의에서 더 강력해진 모습으로 만나요! 열공하세요!



<hr>

💬 궁금한 점이 있다면 자유롭게 댓글을 남겨주세요! (AI 비서가 답변해 드립니다 🤖)

Categories:

Updated: