지난 글에서 러스트의 소유권(Ownership), 빌림(Borrowing), 라이프타임(Lifetime) 개념을 살펴보았습니다. 이번에는 이러한 개념들을 실제로 활용해보는 실습 예제를 통해, 러스트의 컬렉션(Collection), 슬라이스(Slice), 그리고 함수형 프로그래밍 스타일을 지원하는 이터레이터(Iterator) 등을 다뤄보겠습니다.
또한 C++에서 std::vector, std::array, std::map 등을 사용하던 경험과 비교해보며, 러스트가 어떤 식으로 자료 구조를 제공하고 메모리 안전성을 유지하는지 살펴보겠습니다.
벡터(Vector): 동적 크기 배열
C++에서 std::vector로 동적 배열을 관리하던 것처럼, 러스트에서도 Vec<T> 타입을 통해 크기가 가변적인 배열을 다룰 수 있습니다. Vec<T>는 힙에 데이터를 저장하며, 소유권 규칙이 적용됩니다.
fn main() {
let mut numbers = vec![1, 2, 3];
numbers.push(4);
println!("numbers: {:?}", numbers);
let first = numbers[0];
println!("첫 번째 원소: {}", first);
// 소유권과 참조 규칙을 생각해봅시다.
let second_ref = &numbers[1];
println!("두 번째 원소 참조: {}", second_ref);
// numbers.push(5); // 가변 참조가 아니더라도, 불변 참조가 활성을 가지고 있는 동안 변경 불가. 컴파일 에러 발생
}
C++ 벡터와 비슷하지만, 중요한 점은 참조를 빌리는 동안 벡터를 변경하는 것은 불가능하다는 점입니다. 이 규칙을 통해 댕글링 포인터나 사이즈 변경에 따른 참조 무효화를 방지합니다. C++에서 벡터에 원소를 푸시하며 기존 참조가 무효화되는 상황을 줄이기 위해 신경 써야 하는 점들을 러스트가 언어 차원에서 해결한 것입니다.
슬라이스(Slice)
슬라이스는 컬렉션의 일부 구간에 대한 비소유 참조입니다. C++17 이후 std::span을 떠올릴 수 있습니다. 러스트 슬라이스는 [시작..끝] 범위로 컬렉션의 특정 부분을 가리킵니다. 슬라이스는 소유권을 갖지 않고, 단지 데이터를 빌려온 참조 형태로 존재합니다.
fn main() {
let arr = [10, 20, 30, 40, 50];
let slice = &arr[1..4]; // arr 중 인덱스 1부터 3까지 참조
println!("slice: {:?}", slice);
// slice는 arr의 일부를 참조하므로, arr보다 오래 살아서는 안됩니다.
// 라이프타임 규칙으로 인해 arr가 유효한 동안만 slice도 유효합니다.
// 가변 슬라이스도 가능합니다(가변 참조 기반):
let mut vec = vec![1, 2, 3, 4];
let slice_mut = &mut vec[1..3];
slice_mut[0] = 20;
println!("변경 후 vec: {:?}", vec);
}
슬라이스는 길이 정보까지 함께 저장하므로, 별도의 사이즈 인자가 필요하지 않습니다(C++ span과 유사). 또한 슬라이스를 사용하면 해당 범위 내에서 안전한 인덱싱이 이루어져 런타임 범위 체크가 가능해집니다. C++ 포인터와 달리 슬라이스는 항상 유효 범위를 아는 안전한 참조이므로 실수로 범위를 넘어가는 접근을 할 수 없습니다.
문자열 슬라이스 &str vs String
러스트에는 String과 &str 타입이 있습니다. String은 힙에 저장되는 가변적 문자열이며, &str는 문자열 슬라이스로서 불변 참조 형태의 문자열을 나타냅니다.
fn main() {
let greeting = String::from("Hello, Rust");
let greet_slice = &greeting[0..5]; // "Hello"
println!("{}", greet_slice);
// greeting이 살아있는 동안 greet_slice는 유효합니다.
// C++에서 문자열의 일부를 포인터로 참조할 때 생기는 수명 문제를
// 러스트에서는 안전한 슬라이스로 관리할 수 있습니다.
}
C++에서 std::string_view와 유사한 개념으로 생각하면 됩니다. String은 소유권을 가지며 가변적, &str는 비소유 불변 참조이며 슬라이스 형태로 존재합니다.
맵(Map), 해시맵(HashMap)
러스트에는 표준 라이브러리에 HashMap<K, V>가 제공됩니다. 이는 C++의 std::unordered_map와 유사합니다.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Rust", 95);
scores.insert("C++", 89);
match scores.get("Rust") {
Some(&score) => println!("Rust 점수: {}", score),
None => println!("Rust 점수 없음"),
}
for (lang, score) in &scores {
println!("언어: {}, 점수: {}", lang, score);
}
}
중요한 점은 키나 값으로 들어가는 데이터도 소유권/빌림 규칙이 적용된다는 것입니다. 해시맵에 값을 삽입할 때 소유권이 이동하거나 참조를 빌려오는 식으로 작동하여 안전한 메모리 관리를 보장합니다.
이터레이터(Iterator)와 함수형 스타일
C++20 이후 std::ranges를 통해 함수형 스타일의 이터레이션이 가능해졌다면, 러스트는 언어 초기부터 이터레이터 패턴을 지원했습니다. 이터레이터는 소유권 규칙과 결합해 안정적이며, map, filter, collect 등 함수형 패턴을 손쉽게 사용할 수 있습니다.
fn main() {
let nums = vec![1, 2, 3, 4, 5];
// 이터레이터를 생성하여 map, filter를 적용
let processed: Vec<_> = nums.iter()
.map(|x| x * 2)
.filter(|x| x % 4 == 0)
.collect();
println!("처리 결과: {:?}", processed);
}
여기서 nums.iter()는 불변 참조를 통해 이터레이터를 생성합니다. map, filter는 각각의 값에 대한 함수형 연산을 적용하며, 최종적으로 collect()를 통해 새로운 벡터를 생성합니다. C++에서도 레인지 기반 for, 람다 표현식 등으로 비슷한 패턴을 구현할 수 있지만, 러스트는 이 방식을 더 언어 차원에서 자연스럽게 지원합니다.
또한 이터레이터는 소유권 규칙과 잘 어우러져, 예를 들어 into_iter()를 사용할 경우 벡터의 소유권을 옮기며 요소를 소비(consuming)하는 패턴을 나타낼 수 있습니다. 이는 C++에서 move 이터레이터를 사용할 때와 비슷하지만, 러스트는 이 개념을 더 명확히 언어/라이브러리 수준에서 정립했습니다.
C++와의 비교 정리
- 벡터와 슬라이스: C++ std::vector와 std::span의 조합과 유사한 구조를 러스트는 Vec<T>와 슬라이스(&[T])로 제공. 슬라이스는 안전한 범위 체크와 라이프타임 기반 관리로 런타임 에러를 줄임.
- 문자열 관리: C++ std::string + std::string_view 조합을 러스트는 String과 &str로 제공하며 소유권과 불변 참조 개념을 명확히 적용.
- 해시맵: C++ std::unordered_map와 비슷한 HashMap, 소유권 규칙을 통해 메모리 안전성과 데이터 무효화 문제를 최소화.
- 이터레이터: C++20 이후의 함수형 스타일 이터레이션과 비슷하지만, 러스트는 초기부터 map/filter/collect 패턴을 지원. 소유권과 결합해 더욱 명확한 메모리 관리 및 안전성 확보.
앞으로의 학습 방향
이제 컬렉션, 슬라이스, 이터레이터를 통한 러스트 프로그래밍의 전반적 감각을 익혔습니다. 다음 글에서는 구조체, 열거형, 패턴 매칭, 그리고 모듈(Module)과 패키지(Crate) 구조를 통해 더 큰 스케일의 프로그램을 조직하는 방법을 다뤄보겠습니다. 이를 통해 C++에서 클래스, enum, 헤더/소스 구조와 어떤 점이 다른지 비교해볼 예정입니다.
유용한 링크와 리소스
- Rust Book: 컬렉션 소개: https://doc.rust-lang.org/book/ch08-00-common-collections.html
- Rust by Example - Iterator: https://doc.rust-lang.org/rust-by-example/trait/iter.html
- Slice와 &str 개념: https://doc.rust-lang.org/book/ch04-03-slices.html
- C++와 Rust 연계: https://cxx.rs/
'개발 이야기 > Rust (러스트)' 카테고리의 다른 글
러스트 언어 입문 시리즈 - 6편: 트레이트(Trait)와 제네릭(Generic)을 통한 추상화와 다형성 (0) | 2024.12.11 |
---|---|
러스트 언어 입문 시리즈 - 5편: 구조체(Struct), 열거형(Enum), 패턴 매칭(Pattern Matching), 그리고 모듈(Module) 구조 (0) | 2024.12.10 |
러스트 언어 입문 시리즈 - 3편: 소유권(Ownership)과 빌림(Borrowing), 라이프타임(Lifetime)의 이해 (0) | 2024.12.08 |
러스트 언어 입문 시리즈 - 2편: Cargo를 활용한 프로젝트 시작하기 (1) | 2024.12.07 |
러스트 언어 입문 시리즈 - 1편: 시작하기 전에 알아두면 좋은 것들 (0) | 2024.12.07 |