이 글은 Claude Opus 4.5 을 이용해 초안이 작성되었으며, 이후 퇴고를 거쳤습니다.

Rust의 컬렉션은 Go와 유사한 기능을 제공하지만, 소유권 시스템과 결합되어 더 안전합니다. 특히 이터레이터 시스템은 Go의 range보다 훨씬 강력하며, 함수형 프로그래밍 스타일을 지원합니다.


7.1 표준 컬렉션 개요#

Go와 Rust 컬렉션 매핑#

Go Rust 설명
[]T (slice) Vec<T> 동적 배열
[N]T (array) [T; N] 고정 배열
string String 소유된 문자열
string (리터럴) &str 문자열 슬라이스
map[K]V HashMap<K, V> 해시 맵
- HashSet<T> 해시 셋
- BTreeMap<K, V> 정렬된 맵
- BTreeSet<T> 정렬된 셋
- VecDeque<T> 양방향 큐
container/list LinkedList<T> 이중 연결 리스트

7.2 Vec 심화#

생성 방법들#

// 빈 벡터
let v: Vec<i32> = Vec::new();

// vec! 매크로 (가장 흔함)
let v = vec![1, 2, 3, 4, 5];

// 같은 값으로 초기화
let v = vec![0; 10];  // [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]

// 용량 미리 할당
let mut v = Vec::with_capacity(100);

// 이터레이터에서 수집
let v: Vec<i32> = (1..=5).collect();

// from_iter
let v = Vec::from_iter([1, 2, 3]);

기본 연산#

let mut v = vec![1, 2, 3];

// 추가
v.push(4);           // 끝에 추가
v.insert(0, 0);      // 인덱스에 삽입: [0, 1, 2, 3, 4]

// 제거
let last = v.pop();           // 끝에서 제거, Option<T>
let removed = v.remove(0);    // 인덱스에서 제거, T
v.retain(|&x| x > 2);         // 조건에 맞는 것만 유지

// 접근
let first = v[0];             // 패닉 가능
let first = v.get(0);         // Option<&T>
let first = v.first();        // Option<&T>
let last = v.last();          // Option<&T>

// 수정
v[0] = 10;
if let Some(first) = v.first_mut() {
    *first = 20;
}

Go의 append vs Rust의 push#

// Go: append는 새 슬라이스 반환 가능
slice := []int{1, 2, 3}
slice = append(slice, 4)  // 재할당 가능

// 용량 확인
cap(slice)
// Rust: push는 in-place 수정
let mut v = vec![1, 2, 3];
v.push(4);  // v가 mut이어야 함

// 용량
v.capacity();
v.reserve(100);  // 추가 용량 확보
v.shrink_to_fit();  // 불필요한 용량 해제

소유권과 Vec#

let v = vec![String::from("a"), String::from("b")];

// 인덱스 접근은 빌림
let first: &String = &v[0];  // 불변 빌림

// for 루프는 소유권 이동 또는 빌림 선택 가능
for s in v {           // v의 소유권 이동, s: String
    println!("{}", s);
}
// v는 더 이상 사용 불가

let v = vec![String::from("a"), String::from("b")];
for s in &v {          // v 빌림, s: &String
    println!("{}", s);
}
// v 여전히 사용 가능

for s in &mut v {      // 가변 빌림, s: &mut String
    s.push_str("!");
}

슬라이싱#

let v = vec![1, 2, 3, 4, 5];

// 슬라이스 참조
let slice: &[i32] = &v[1..4];  // [2, 3, 4]
let slice: &[i32] = &v[..];    // 전체

// 가변 슬라이스
let mut v = vec![1, 2, 3, 4, 5];
let slice: &mut [i32] = &mut v[1..4];
slice[0] = 20;  // v는 [1, 20, 3, 4, 5]

7.3 문자열 처리#

String vs &str#

// String: 힙에 할당된 소유 문자열
let owned: String = String::from("hello");
let owned: String = "hello".to_string();
let owned: String = "hello".to_owned();

// &str: 문자열 슬라이스 (빌림)
let borrowed: &str = "hello";  // 정적 문자열
let borrowed: &str = &owned;   // String에서 빌림
let borrowed: &str = &owned[0..3];  // 부분 슬라이스
특성 String &str
소유권 있음 없음 (빌림)
힙 할당 아니오
가변 가능 불가능
크기 가변 고정 (뷰)
함수 인자 소유권 필요시 대부분

UTF-8 인코딩#

Rust 문자열은 항상 유효한 UTF-8입니다:

let s = "Hello, 世界! 🌍";

// 길이 (바이트 수)
println!("{}", s.len());  // 20 bytes

// 문자 수
println!("{}", s.chars().count());  // 12 characters

// 인덱싱은 바이트 기준 (직접 불가)
// let c = s[0];  // 컴파일 에러!

// 슬라이싱은 가능하지만 위험
let hello = &s[0..5];  // "Hello"
// let invalid = &s[0..8];  // 패닉! UTF-8 경계 아님

문자 순회#

let s = "Hello, 世界!";

// 문자(char) 순회
for c in s.chars() {
    print!("{} ", c);  // H e l l o ,   世 界 !
}

// 바이트 순회
for b in s.bytes() {
    print!("{} ", b);  // 72 101 108 108 111 ...
}

// 인덱스와 함께
for (i, c) in s.char_indices() {
    println!("{}: {}", i, c);  // 바이트 인덱스
}

Go와 비교:

// Go: range는 rune(유니코드) 순회
s := "Hello, 世界!"
for i, r := range s {
    fmt.Printf("%d: %c\n", i, r)  // 바이트 인덱스, rune
}

// 바이트 순회
for i := 0; i < len(s); i++ {
    fmt.Printf("%d: %d\n", i, s[i])
}

문자열 연결과 조작#

// + 연산자 (첫 번째는 소유권 이동)
let s1 = String::from("Hello, ");
let s2 = String::from("world!");
let s3 = s1 + &s2;  // s1 이동, s2 빌림
// s1은 더 이상 사용 불가

// format! 매크로 (권장)
let s1 = String::from("Hello");
let s2 = String::from("world");
let s3 = format!("{}, {}!", s1, s2);  // 소유권 유지

// push / push_str
let mut s = String::from("Hello");
s.push(',');        // 문자 추가
s.push_str(" world!");  // 문자열 추가

// 여러 문자열 조인
let parts = vec!["Hello", "world", "Rust"];
let joined = parts.join(", ");  // "Hello, world, Rust"

문자열 파싱과 변환#

// 문자열 -> 숫자
let num: i32 = "42".parse().unwrap();
let num = "42".parse::<i32>().unwrap();

// 숫자 -> 문자열
let s = 42.to_string();
let s = format!("{}", 42);

// 트림
let s = "  hello  ".trim();      // "hello"
let s = "hello\n".trim_end();    // "hello"

// 분할
let parts: Vec<&str> = "a,b,c".split(',').collect();

// 검색
let contains = "hello".contains("ell");  // true
let starts = "hello".starts_with("he");  // true
let pos = "hello".find("ll");            // Some(2)

// 대소문자
let upper = "hello".to_uppercase();  // "HELLO"
let lower = "HELLO".to_lowercase();  // "hello"

// 교체
let s = "hello".replace("l", "L");  // "heLLo"

7.4 HashMap 활용#

생성과 기본 연산#

use std::collections::HashMap;

// 생성
let mut scores: HashMap<String, i32> = HashMap::new();

// 삽입
scores.insert(String::from("Blue"), 10);
scores.insert(String::from("Red"), 50);

// 매크로로 생성 (nightly 또는 maplit 크레이트)
// let scores = hashmap!{ "Blue" => 10, "Red" => 50 };

// from_iter
let scores: HashMap<_, _> = vec![
    (String::from("Blue"), 10),
    (String::from("Red"), 50),
].into_iter().collect();

// 접근
let score = scores.get("Blue");  // Option<&i32>
let score = scores["Blue"];      // 패닉 가능

// 제거
let removed = scores.remove("Blue");  // Option<i32>

// 존재 확인
if scores.contains_key("Red") {
    println!("Red exists");
}

// 순회
for (key, value) in &scores {
    println!("{}: {}", key, value);
}

Entry API#

Go에 없는 강력한 기능:

use std::collections::HashMap;

let mut scores: HashMap<String, i32> = HashMap::new();

// 없으면 삽입
scores.entry(String::from("Blue")).or_insert(50);

// 없으면 계산해서 삽입
scores.entry(String::from("Red")).or_insert_with(|| {
    expensive_calculation()
});

// 있으면 수정
scores.entry(String::from("Blue")).and_modify(|v| *v += 10);

// 조합: 없으면 기본값, 있으면 수정
scores.entry(String::from("Blue"))
    .and_modify(|v| *v += 10)
    .or_insert(0);

// 실전 예: 단어 카운팅
let text = "hello world hello rust";
let mut word_count: HashMap<&str, u32> = HashMap::new();

for word in text.split_whitespace() {
    *word_count.entry(word).or_insert(0) += 1;
}
// {"hello": 2, "world": 1, "rust": 1}

Go 비교:

// Go: 조건부 삽입이 장황함
scores := make(map[string]int)

if _, exists := scores["Blue"]; !exists {
    scores["Blue"] = 50
}

// 단어 카운팅
wordCount := make(map[string]int)
for _, word := range strings.Fields(text) {
    wordCount[word]++  // 없으면 0으로 시작
}

커스텀 타입을 키로 사용#

use std::collections::HashMap;

// Eq + Hash 필요
#[derive(Hash, Eq, PartialEq, Debug)]
struct Point {
    x: i32,
    y: i32,
}

let mut locations: HashMap<Point, String> = HashMap::new();
locations.insert(Point { x: 0, y: 0 }, String::from("Origin"));

7.5 이터레이터 기초#

Iterator 트레이트#

trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // 수십 개의 기본 구현 메서드들...
}

이터레이터 생성#

let v = vec![1, 2, 3];

// iter(): &T 이터레이터 (빌림)
for x in v.iter() {
    println!("{}", x);  // x: &i32
}
// v 여전히 사용 가능

// iter_mut(): &mut T 이터레이터
let mut v = vec![1, 2, 3];
for x in v.iter_mut() {
    *x *= 2;  // x: &mut i32
}
// v는 [2, 4, 6]

// into_iter(): T 이터레이터 (소유권 이동)
let v = vec![1, 2, 3];
for x in v.into_iter() {
    println!("{}", x);  // x: i32
}
// v 더 이상 사용 불가

// for 루프의 암묵적 변환
for x in &v { }      // v.iter()와 동일
for x in &mut v { }  // v.iter_mut()와 동일
for x in v { }       // v.into_iter()와 동일

Go의 range와 비교#

// Go: range는 인덱스와 값을 제공
slice := []int{1, 2, 3}

for i, v := range slice {
    fmt.Println(i, v)
}

// 값만
for _, v := range slice {
    fmt.Println(v)
}

// 인덱스만
for i := range slice {
    fmt.Println(i)
}
// Rust: 더 유연한 이터레이터

let v = vec![1, 2, 3];

// 값만
for x in &v {
    println!("{}", x);
}

// 인덱스와 값
for (i, x) in v.iter().enumerate() {
    println!("{}: {}", i, x);
}

// 인덱스만
for i in 0..v.len() {
    println!("{}", i);
}

7.6 이터레이터 어댑터#

이터레이터 어댑터는 지연 평가(lazy evaluation) 됩니다:

let v = vec![1, 2, 3];

// 어댑터만으로는 아무 일도 일어나지 않음
let iter = v.iter().map(|x| {
    println!("mapping {}", x);  // 출력 안 됨!
    x * 2
});

// collect()로 소비해야 실행
let doubled: Vec<i32> = iter.collect();  // 이제 출력됨

변환 어댑터#

let v = vec![1, 2, 3, 4, 5];

// map: 각 요소 변환
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
// [2, 4, 6, 8, 10]

// filter: 조건에 맞는 요소만
let evens: Vec<i32> = v.iter().filter(|x| *x % 2 == 0).copied().collect();
// [2, 4]

// filter_map: 변환 + 필터링
let results: Vec<i32> = ["1", "two", "3"]
    .iter()
    .filter_map(|s| s.parse().ok())
    .collect();
// [1, 3]

// flat_map: 평탄화 + 변환
let nested = vec![vec![1, 2], vec![3, 4]];
let flat: Vec<i32> = nested.into_iter().flatten().collect();
// [1, 2, 3, 4]

let words: Vec<&str> = vec!["hello world", "foo bar"]
    .iter()
    .flat_map(|s| s.split_whitespace())
    .collect();
// ["hello", "world", "foo", "bar"]

제어 어댑터#

let v: Vec<i32> = (1..=10).collect();

// take: 처음 n개
let first_three: Vec<i32> = v.iter().take(3).copied().collect();
// [1, 2, 3]

// skip: 처음 n개 건너뜀
let after_three: Vec<i32> = v.iter().skip(3).copied().collect();
// [4, 5, 6, 7, 8, 9, 10]

// take_while: 조건이 참인 동안
let small: Vec<i32> = v.iter().take_while(|&&x| x < 5).copied().collect();
// [1, 2, 3, 4]

// skip_while: 조건이 참인 동안 건너뜀
let large: Vec<i32> = v.iter().skip_while(|&&x| x < 5).copied().collect();
// [5, 6, 7, 8, 9, 10]

// step_by: n개씩 건너뜀
let every_other: Vec<i32> = v.iter().step_by(2).copied().collect();
// [1, 3, 5, 7, 9]

조합 어댑터#

// chain: 이터레이터 연결
let a = vec![1, 2, 3];
let b = vec![4, 5, 6];
let combined: Vec<i32> = a.iter().chain(b.iter()).copied().collect();
// [1, 2, 3, 4, 5, 6]

// zip: 이터레이터 결합
let names = vec!["Alice", "Bob"];
let scores = vec![90, 85];
let pairs: Vec<_> = names.iter().zip(scores.iter()).collect();
// [("Alice", 90), ("Bob", 85)]

// enumerate: 인덱스 추가
let indexed: Vec<_> = vec!["a", "b", "c"]
    .iter()
    .enumerate()
    .collect();
// [(0, "a"), (1, "b"), (2, "c")]

7.7 이터레이터 컨슈머#

컨슈머는 이터레이터를 소비하고 결과를 반환합니다:

let v = vec![1, 2, 3, 4, 5];

// collect: 컬렉션으로 수집
let doubled: Vec<i32> = v.iter().map(|x| x * 2).collect();
let set: HashSet<i32> = v.iter().copied().collect();

// fold: 누적 연산 (초기값 있음)
let sum = v.iter().fold(0, |acc, x| acc + x);  // 15
let product = v.iter().fold(1, |acc, x| acc * x);  // 120

// reduce: fold의 초기값 없는 버전
let sum = v.iter().copied().reduce(|acc, x| acc + x);  // Some(15)

// sum, product (Sum, Product 트레이트 구현 필요)
let sum: i32 = v.iter().sum();      // 15
let product: i32 = v.iter().product();  // 120

// count
let count = v.iter().count();  // 5

// for_each: 부수 효과 실행
v.iter().for_each(|x| println!("{}", x));

// any, all
let has_even = v.iter().any(|x| x % 2 == 0);  // true
let all_positive = v.iter().all(|x| *x > 0);  // true

// find
let first_even = v.iter().find(|x| *x % 2 == 0);  // Some(&2)

// position
let pos = v.iter().position(|x| *x == 3);  // Some(2)

// max, min
let max = v.iter().max();  // Some(&5)
let min = v.iter().min();  // Some(&1)

// max_by, min_by (커스텀 비교)
let max_by_abs = vec![-10, 5, 3]
    .iter()
    .max_by(|a, b| a.abs().cmp(&b.abs()));  // Some(&-10)

7.8 클로저와 함수형 패턴#

클로저 문법#

// 기본 형태
let add = |a: i32, b: i32| -> i32 { a + b };

// 타입 추론 (가장 흔함)
let add = |a, b| a + b;

// 한 줄이면 중괄호 생략
let double = |x| x * 2;

// 복잡한 클로저
let process = |x| {
    let y = x * 2;
    let z = y + 1;
    z
};

캡처 방식#

// 불변 빌림 (기본)
let x = 4;
let equal_to_x = |z| z == x;  // x를 불변 빌림

// 가변 빌림
let mut count = 0;
let mut increment = || {
    count += 1;  // count를 가변 빌림
};
increment();

// 소유권 이동 (move)
let s = String::from("hello");
let consume = move || {
    println!("{}", s);  // s의 소유권 이동
};
// s는 더 이상 사용 불가

Fn, FnMut, FnOnce 트레이트#

// FnOnce: 한 번만 호출 가능 (소유권 소비)
fn call_once<F: FnOnce()>(f: F) {
    f();
    // f();  // 에러! 이미 소비됨
}

// FnMut: 여러 번 호출 가능, 환경 변경 가능
fn call_mut<F: FnMut()>(mut f: F) {
    f();
    f();  // OK
}

// Fn: 여러 번 호출 가능, 환경 불변
fn call<F: Fn()>(f: F) {
    f();
    f();  // OK
}

// 계층: Fn : FnMut : FnOnce (Fn이 가장 제한적)

이터레이터 체이닝 실전 예제#

// 성적 처리 예제
struct Student {
    name: String,
    score: u32,
}

let students = vec![
    Student { name: "Alice".into(), score: 85 },
    Student { name: "Bob".into(), score: 70 },
    Student { name: "Charlie".into(), score: 95 },
    Student { name: "David".into(), score: 60 },
];

// 80점 이상 학생의 이름을 점수 순으로 정렬
let honor_roll: Vec<&str> = students
    .iter()
    .filter(|s| s.score >= 80)
    .map(|s| s.name.as_str())
    .collect();

// 평균 점수
let avg: f64 = students.iter().map(|s| s.score).sum::<u32>() as f64 
    / students.len() as f64;

// 점수대별 그룹화
use std::collections::HashMap;
let by_grade: HashMap<&str, Vec<&Student>> = students
    .iter()
    .fold(HashMap::new(), |mut acc, s| {
        let grade = match s.score {
            90..=100 => "A",
            80..=89 => "B",
            70..=79 => "C",
            _ => "F",
        };
        acc.entry(grade).or_default().push(s);
        acc
    });

Go 스타일 vs Rust 함수형 스타일#

// Go: 명령형 스타일
var result []int
for _, x := range numbers {
    if x%2 == 0 {
        result = append(result, x*2)
    }
}
// Rust: 함수형 스타일
let result: Vec<i32> = numbers
    .iter()
    .filter(|x| *x % 2 == 0)
    .map(|x| x * 2)
    .collect();

// 또는 명령형 스타일도 가능
let mut result = Vec::new();
for x in &numbers {
    if x % 2 == 0 {
        result.push(x * 2);
    }
}

7.9 요약#

컬렉션 선택 가이드#

상황 컬렉션
순서 있는 동적 배열 Vec<T>
키-값 저장 HashMap<K, V>
정렬된 키-값 BTreeMap<K, V>
고유 값 집합 HashSet<T>
정렬된 집합 BTreeSet<T>
양방향 큐 VecDeque<T>
문자열 소유 String
문자열 참조 &str

이터레이터 체크리스트#

  • iter() vs iter_mut() vs into_iter() 구분
  • 지연 평가 이해 (어댑터는 소비 전까지 실행 안 됨)
  • 적절한 컨슈머 선택 (collect, fold, for_each 등)
  • 소유권 고려 (copied(), cloned() 필요한 경우)

연습 문제#

  1. Vec 조작: 벡터에서 중복을 제거하는 함수를 작성하세요.

  2. 문자열 처리: 문장에서 가장 긴 단어를 찾는 함수를 작성하세요.

  3. HashMap 활용: 문자열에서 각 문자의 빈도를 계산하세요.

  4. 이터레이터 체이닝: 1부터 100까지의 숫자 중 3의 배수이면서 5의 배수가 아닌 수의 합을 구하세요.

  5. Entry API: 단어 빈도 카운터를 구현하되, 대소문자를 무시하세요.

  6. 커스텀 이터레이터: 피보나치 수열을 생성하는 이터레이터를 구현하세요.