이미지 로딩 중...
AI Generated
2025. 11. 13. · 2 Views
Rust 입문 가이드 6 벡터 Vec<T> 생성과 요소 추가
Rust의 동적 배열인 Vec<T>의 기본 개념부터 실무 활용까지 완벽 정리. 벡터 생성, 요소 추가, 매크로 활용법을 상세한 예제와 함께 학습합니다. 초급 개발자도 쉽게 따라할 수 있는 친절한 가이드입니다.
목차
- Vec<T> 기본 개념 - 동적 크기의 배열 이해하기
- push() 메서드 - 벡터에 요소 추가하기
- vec! 매크로 - 초기값으로 벡터 생성하기
- capacity와 len의 차이 - 벡터 크기 이해하기
- reserve() 메서드 - 용량 미리 확보하기
- pop() 메서드 - 벡터의 마지막 요소 제거하기
- 인덱스 접근과 get() - 안전한 요소 읽기
- iter()와 반복 - 벡터 순회의 모범 사례
- clear()와 truncate() - 벡터 비우기와 크기 조정
- remove()와 swap_remove() - 중간 요소 제거하기
1. Vec<T> 기본 개념 - 동적 크기의 배열 이해하기
시작하며
여러분이 사용자 입력을 받아서 리스트에 저장해야 하는데, 몇 개의 입력이 들어올지 모르는 상황을 겪어본 적 있나요? 배열의 크기를 미리 정해놓으면 너무 작을 수도, 너무 클 수도 있어서 난감했던 경험 말이죠.
이런 문제는 실제 개발 현장에서 자주 발생합니다. 고정된 크기의 배열을 사용하면 메모리 낭비가 생기거나, 반대로 공간이 부족해서 데이터를 저장하지 못하는 상황이 발생할 수 있습니다.
특히 채팅 메시지, 검색 결과, 실시간 로그처럼 동적으로 변하는 데이터를 다룰 때는 더욱 그렇습니다. 바로 이럴 때 필요한 것이 Vec<T>입니다.
Vec는 Vector의 약자로, 필요에 따라 자동으로 크기가 늘어나거나 줄어드는 똑똑한 배열입니다. 데이터를 추가할 때마다 알아서 공간을 확보하고, 필요 없어지면 메모리를 반환하여 효율적인 리소스 관리를 가능하게 합니다.
개요
간단히 말해서, Vec<T>는 Rust의 동적 배열로, 런타임에 크기를 자유롭게 변경할 수 있는 컬렉션입니다. 여기서 T는 제네릭 타입으로, 어떤 타입의 데이터든 저장할 수 있다는 의미입니다.
Vec는 실무에서 가장 많이 사용되는 컬렉션 타입 중 하나입니다. API 응답 데이터를 저장하거나, 파일에서 읽어온 여러 줄의 텍스트를 다루거나, 게임에서 적 캐릭터들의 목록을 관리하는 등 다양한 상황에서 필수적입니다.
크기를 미리 알 수 없는 데이터를 다룰 때마다 Vec가 여러분의 강력한 도구가 되어줄 것입니다. 기존 C나 Java에서 동적 배열을 사용하려면 ArrayList나 동적 메모리 할당을 직접 관리해야 했습니다.
하지만 Rust의 Vec는 소유권 시스템과 결합되어 메모리 안전성을 보장하면서도 사용하기 쉬운 인터페이스를 제공합니다. Vec의 핵심 특징은 세 가지입니다.
첫째, 힙(heap) 메모리에 데이터를 저장하여 동적으로 크기를 조절할 수 있습니다. 둘째, 같은 타입의 데이터만 저장할 수 있어 타입 안전성이 보장됩니다.
셋째, 연속된 메모리 공간에 데이터를 저장하여 캐시 효율성이 높고 인덱스 접근이 빠릅니다. 이러한 특징들이 Vec를 성능과 안전성을 동시에 만족시키는 완벽한 선택으로 만들어줍니다.
코드 예제
// Vec 생성 방법 1: new() 메서드로 빈 벡터 생성
let mut numbers: Vec<i32> = Vec::new();
// Vec 생성 방법 2: 타입 추론 활용
let mut names = Vec::new(); // 타입은 나중에 추론됨
names.push("Alice"); // 여기서 Vec<&str>로 추론
// Vec 생성 방법 3: 초기 용량 지정으로 성능 최적화
let mut buffer: Vec<u8> = Vec::with_capacity(100);
println!("용량: {}, 길이: {}", buffer.capacity(), buffer.len());
// Vec 생성 방법 4: vec! 매크로로 초기값과 함께 생성
let scores = vec![95, 87, 92, 100, 88];
println!("첫 번째 점수: {}", scores[0]);
설명
Vec<T>가 하는 일은 간단합니다. 여러분이 필요한 만큼의 데이터를 저장할 수 있는 공간을 동적으로 제공하는 것입니다.
스택이 아닌 힙 메모리를 사용하기 때문에 프로그램이 실행되는 동안 언제든지 크기를 변경할 수 있습니다. 첫 번째 생성 방법인 Vec::new()는 가장 기본적인 방식입니다.
이 방법은 완전히 비어있는 벡터를 만들며, 타입을 명시적으로 지정해야 합니다. Vec<i32>처럼 제네릭 타입 파라미터를 사용하여 어떤 타입의 데이터를 저장할지 컴파일러에게 알려줍니다.
mut 키워드를 붙여야 나중에 데이터를 추가할 수 있다는 점을 기억하세요. 두 번째 방법은 타입 추론을 활용합니다.
Rust의 강력한 타입 추론 시스템 덕분에 처음에는 타입을 명시하지 않아도 됩니다. names.push("Alice")를 호출하는 순간, 컴파일러는 "아, 이 벡터는 문자열 슬라이스를 저장하는구나"라고 스스로 판단하여 Vec<&str> 타입으로 결정합니다.
이 방식은 코드를 더 간결하게 만들어주지만, 타입이 명확하지 않은 경우에는 에러가 발생할 수 있으니 주의가 필요합니다. 세 번째 방법인 with_capacity()는 성능 최적화에 중요합니다.
Vec는 요소가 추가될 때마다 내부적으로 메모리 재할당이 필요할 수 있습니다. 만약 100개의 요소를 저장할 것을 미리 안다면, with_capacity(100)으로 생성하면 처음부터 충분한 공간을 확보하여 재할당 오버헤드를 피할 수 있습니다.
capacity()는 할당된 총 공간을, len()은 실제로 저장된 요소의 개수를 반환합니다. 네 번째 방법인 vec!
매크로는 실무에서 가장 자주 사용됩니다. 초기값을 대괄호 안에 나열하기만 하면 자동으로 적절한 타입의 Vec를 생성해줍니다.
코드가 매우 간결하고 읽기 쉬워서, 테스트 데이터를 만들거나 고정된 초기값이 있는 경우에 특히 유용합니다. 인덱스 접근도 배열처럼 scores[0] 형태로 가능하며, 이는 O(1) 시간 복잡도를 가집니다.
여러분이 이 코드를 사용하면 메모리 안전성을 보장받으면서도 유연한 데이터 관리가 가능합니다. 소유권 시스템 덕분에 메모리 누수나 댕글링 포인터 걱정 없이 안전하게 동적 배열을 다룰 수 있고, 타입 시스템이 런타임 에러를 컴파일 타임에 잡아주어 더욱 견고한 프로그램을 작성할 수 있습니다.
실전 팁
💡 mut 키워드를 빼먹으면 push나 다른 수정 작업을 할 수 없습니다. 벡터를 수정할 계획이라면 항상 let mut를 사용하세요.
💡 대량의 데이터를 추가할 예정이라면 with_capacity()를 사용하여 초기 용량을 지정하세요. 메모리 재할당 횟수를 줄여 성능을 크게 향상시킬 수 있습니다.
💡 타입을 명시하지 않으면 컴파일러가 추론할 수 없어 에러가 발생할 수 있습니다. 특히 빈 벡터를 만들 때는 타입을 명시하는 습관을 들이세요.
💡 Vec는 힙 메모리를 사용하므로 스택보다 할당 속도가 느립니다. 크기가 고정되어 있고 작은 데이터라면 배열 [T; N]을 고려해보세요.
2. push() 메서드 - 벡터에 요소 추가하기
시작하며
여러분이 쇼핑 카트에 상품을 하나씩 담는 기능을 구현한다고 상상해보세요. 사용자가 "장바구니에 추가" 버튼을 클릭할 때마다 새로운 상품이 리스트 끝에 추가되어야 합니다.
이런 패턴은 프로그래밍에서 정말 흔합니다. 로그 메시지를 차례대로 기록하거나, 사용자가 입력한 태그를 모으거나, 게임에서 획득한 아이템을 인벤토리에 추가하는 등 끝에 요소를 추가하는 작업은 끊임없이 발생합니다.
바로 이럴 때 필요한 것이 push() 메서드입니다. push는 벡터의 끝에 새로운 요소를 추가하는 가장 기본적이고 중요한 메서드로, 여러분의 동적 데이터 관리를 가능하게 해줍니다.
개요
간단히 말해서, push()는 벡터의 맨 끝에 새로운 요소를 추가하는 메서드입니다. 배열의 길이를 1 증가시키고, 필요하다면 자동으로 메모리를 재할당합니다.
push() 메서드는 실무에서 데이터를 수집할 때 가장 자주 사용됩니다. 예를 들어, 파일의 각 줄을 읽어서 벡터에 저장하거나, API 호출 결과를 누적하거나, 사용자 액션을 추적하는 경우에 매우 유용합니다.
시간 복잡도는 평균적으로 O(1)이지만, 용량이 부족할 때는 재할당으로 인해 O(n)이 될 수 있습니다. 기존 방식에서는 배열에 요소를 추가하려면 인덱스를 직접 관리하고 크기를 체크해야 했습니다.
하지만 push()를 사용하면 이 모든 과정이 자동화됩니다. 크기 검사, 메모리 재할당, 인덱스 관리를 컴파일러가 대신 처리해주어 여러분은 비즈니스 로직에만 집중할 수 있습니다.
push()의 핵심 특징은 세 가지입니다. 첫째, 소유권을 이동시킵니다.
push한 값은 벡터가 소유하게 되어 원래 변수에서는 더 이상 사용할 수 없습니다. 둘째, 자동 확장 기능이 있어 용량이 부족하면 내부적으로 더 큰 메모리를 할당합니다.
셋째, 타입 안전성이 보장되어 벡터의 타입과 일치하는 값만 추가할 수 있습니다. 이러한 특징들이 push()를 안전하고 편리한 메서드로 만들어줍니다.
코드 예제
// 정수 벡터에 요소 추가
let mut scores = Vec::new();
scores.push(85); // 첫 번째 요소 추가
scores.push(92); // 두 번째 요소 추가
scores.push(78); // 세 번째 요소 추가
println!("점수들: {:?}", scores); // [85, 92, 78]
// 문자열 벡터에 요소 추가
let mut todos = vec!["코드 리뷰", "테스트 작성"];
todos.push("문서 업데이트");
println!("할 일: {:?}", todos);
// 루프에서 연속으로 추가
let mut even_numbers = Vec::new();
for i in 0..10 {
if i % 2 == 0 {
even_numbers.push(i);
}
}
println!("짝수들: {:?}", even_numbers); // [0, 2, 4, 6, 8]
설명
push()가 하는 일은 매우 직관적입니다. 여러분이 제공한 값을 벡터의 맨 마지막 위치에 추가하고, 벡터의 길이를 1 증가시킵니다.
내부적으로는 현재 용량을 확인하고, 필요하면 메모리를 확장하는 복잡한 작업을 수행하지만, 여러분은 그저 push()만 호출하면 됩니다. 첫 번째 예제에서 scores 벡터는 처음에 비어있습니다.
scores.push(85)를 호출하면 85가 첫 번째 위치(인덱스 0)에 추가됩니다. 이어서 92, 78을 차례로 push하면 벡터는 [85, 92, 78]이 됩니다.
각 push 호출은 독립적이며, 이전 작업의 영향을 받지 않습니다. {:?}는 디버그 출력 포맷으로, 벡터의 모든 요소를 보기 좋게 출력해줍니다.
두 번째 예제는 이미 초기값이 있는 벡터에 요소를 추가하는 경우입니다. vec!
매크로로 두 개의 할 일을 가진 벡터를 만들고, 나중에 "문서 업데이트"를 추가합니다. 이는 실무에서 매우 흔한 패턴으로, 기본 설정이나 필수 항목을 미리 넣어두고 사용자 입력이나 상황에 따라 추가 항목을 push하는 방식입니다.
세 번째 예제는 루프에서 push를 사용하는 전형적인 패턴입니다. 0부터 9까지 반복하면서 짝수만 필터링하여 벡터에 추가합니다.
이런 방식은 데이터 처리에서 매우 유용합니다. 예를 들어, 파일의 각 줄을 읽어서 특정 조건을 만족하는 줄만 저장하거나, API 응답에서 필요한 데이터만 추출하여 벡터에 모을 때 사용합니다.
여러분이 이 코드를 사용하면 동적 데이터 수집이 매우 간단해집니다. push()는 메모리 관리를 자동화하여 버퍼 오버플로우나 메모리 누수 같은 저수준 문제로부터 여러분을 보호합니다.
또한 타입 시스템이 잘못된 타입의 값을 추가하는 실수를 컴파일 타임에 잡아주어, 런타임 에러를 미리 방지할 수 있습니다.
실전 팁
💡 push는 소유권을 이동시킵니다. push한 후에는 원래 변수를 사용할 수 없으니, 값을 복사하거나 참조가 필요한 경우 clone()을 고려하세요.
💡 대량의 데이터를 push할 때는 reserve() 메서드로 미리 용량을 확보하세요. 반복적인 재할당을 피해 성능을 크게 개선할 수 있습니다.
💡 루프에서 push를 사용할 때는 초기 용량을 예상하여 Vec::with_capacity()로 생성하는 것이 좋습니다. 예: let mut v = Vec::with_capacity(1000);
💡 push 대신 collect()를 사용하면 iterator를 더 간결하게 벡터로 변환할 수 있습니다. 예: (0..10).filter(|x| x % 2 == 0).collect()
3. vec! 매크로 - 초기값으로 벡터 생성하기
시작하며
여러분이 테스트 데이터를 준비하거나, 설정값 목록을 초기화하거나, 샘플 데이터로 프로토타입을 만들어야 할 때가 있죠? 매번 Vec::new()를 호출하고 push를 여러 번 반복하는 것은 정말 번거롭습니다.
이런 반복적인 패턴은 코드를 길고 읽기 어렵게 만듭니다. 특히 초기값이 많을수록 push 호출이 늘어나고, 실수로 하나를 빼먹거나 순서를 바꾸는 실수가 발생하기 쉽습니다.
코드 리뷰할 때도 초기값들이 눈에 잘 들어오지 않아 불편합니다. 바로 이럴 때 필요한 것이 vec!
매크로입니다. vec!는 초기값을 가진 벡터를 한 줄로 간결하게 만들 수 있는 강력한 도구로, 코드의 가독성과 생산성을 크게 향상시켜줍니다.
개요
간단히 말해서, vec! 매크로는 초기값을 가진 벡터를 편리하게 생성하는 문법입니다.
배열 리터럴과 비슷한 문법으로 여러 값을 나열하거나, 같은 값을 반복하여 벡터를 만들 수 있습니다. vec!
매크로는 실무에서 가장 많이 사용되는 벡터 생성 방법입니다. 설정 파일의 기본값, 테스트용 더미 데이터, UI 컴포넌트의 초기 상태, 게임의 맵 데이터 등 미리 정해진 값들로 벡터를 초기화할 때 필수적입니다.
코드를 짧고 명확하게 만들어 유지보수성을 높여줍니다. 기존에는 let mut v = Vec::new(); v.push(1); v.push(2); v.push(3); 처럼 여러 줄이 필요했습니다.
하지만 vec!를 사용하면 let v = vec![1, 2, 3]; 한 줄로 끝납니다. 타이핑 양이 줄어들 뿐만 아니라, 초기값들이 한눈에 들어와 코드의 의도가 명확해집니다.
vec! 매크로의 핵심 특징은 세 가지입니다.
첫째, 두 가지 형태를 지원합니다 - 값을 나열하는 형태와 같은 값을 반복하는 형태입니다. 둘째, 타입 추론이 자동으로 작동하여 대부분의 경우 타입을 명시하지 않아도 됩니다.
셋째, 컴파일 타임에 최적화되어 런타임 오버헤드가 거의 없습니다. 이러한 특징들이 vec!를 Rust 프로그래머들이 가장 선호하는 벡터 생성 방법으로 만들어줍니다.
코드 예제
// 방법 1: 값을 나열하여 생성
let numbers = vec![1, 2, 3, 4, 5];
let names = vec!["Alice", "Bob", "Charlie"];
println!("숫자: {:?}, 이름: {:?}", numbers, names);
// 방법 2: 같은 값을 반복하여 생성 (값; 개수)
let zeros = vec![0; 10]; // 0을 10개 가진 벡터
let default_buffer = vec![255u8; 100]; // 255를 100개 (u8 타입)
println!("0의 개수: {}", zeros.len());
// 방법 3: 복잡한 타입도 가능
let points = vec![(0, 0), (10, 20), (30, 40)];
let messages = vec![
String::from("안녕하세요"),
String::from("환영합니다"),
];
println!("첫 번째 좌표: {:?}", points[0]);
설명
vec! 매크로가 하는 일은 여러분이 제공한 초기값들로 벡터를 만들고, 그 값들로 미리 채워진 상태로 반환하는 것입니다.
내부적으로는 적절한 용량을 계산하고, 메모리를 할당하고, 값들을 복사하는 작업을 수행하지만, 여러분은 그저 값들을 나열하기만 하면 됩니다. 첫 번째 생성 방법은 값을 직접 나열하는 형태입니다.
vec![1, 2, 3, 4, 5]는 다섯 개의 정수로 이루어진 벡터를 만듭니다. 쉼표로 구분된 각 값은 벡터의 요소가 되며, 순서대로 인덱스 0, 1, 2, 3, 4에 배치됩니다.
문자열 슬라이스를 저장하는 vec!["Alice", "Bob", "Charlie"]도 같은 방식으로 동작합니다. 타입은 자동으로 추론되어 각각 Vec<i32>와 Vec<&str>이 됩니다.
두 번째 방법은 같은 값을 여러 번 반복하는 형태입니다. vec![0; 10]은 "0을 10번 반복하라"는 의미로, [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]과 같은 벡터를 만듭니다.
이는 버퍼를 초기화하거나 기본값으로 채워진 배열이 필요할 때 매우 유용합니다. vec![255u8; 100]처럼 타입을 명시할 수도 있는데, 여기서 255u8은 255를 u8 타입으로 해석하라는 의미입니다.
세 번째 예제는 복잡한 타입도 vec!로 만들 수 있음을 보여줍니다. 튜플을 저장하는 vec![(0, 0), (10, 20), (30, 40)]은 좌표 데이터를 표현하는 데 유용합니다.
String 타입을 저장하는 벡터도 만들 수 있지만, 이 경우 String::from()으로 명시적으로 String을 생성해야 합니다. &str과 String의 차이를 이해하는 것이 중요합니다.
여러분이 이 코드를 사용하면 초기화 코드가 매우 간결해집니다. vec!
매크로는 컴파일 타임에 최적화되어 성능 손실이 없으며, 가독성이 크게 향상되어 코드 리뷰나 유지보수가 쉬워집니다. 특히 초기값이 많은 경우, 한눈에 데이터 구조를 파악할 수 있어 디버깅 시간을 단축시켜줍니다.
실전 팁
💡 반복 문법(vec![값; 개수])을 사용할 때는 값이 Copy 트레이트를 구현해야 합니다. String처럼 Clone만 구현된 타입은 이 방법을 쓸 수 없습니다.
💡 큰 벡터를 만들 때는 vec![0; 1000000]처럼 반복 문법이 훨씬 효율적입니다. 모든 값을 나열하는 것보다 메모리와 타이핑 측면에서 유리합니다.
💡 vec! 매크로는 기본적으로 불변(immutable)입니다. 나중에 수정하려면 let mut v = vec![...]; 형태로 선언하세요.
💡 복잡한 초기값은 여러 줄로 나눠서 작성하면 가독성이 좋습니다. Rust는 쉼표 뒤 줄바꿈을 허용하므로 코드 포맷팅을 자유롭게 할 수 있습니다.
4. capacity와 len의 차이 - 벡터 크기 이해하기
시작하며
여러분이 벡터에 요소를 추가하다가 성능 문제를 겪어본 적 있나요? 특히 루프에서 수천, 수만 개의 요소를 push할 때 프로그램이 느려지는 경험 말이죠.
이런 문제의 원인을 이해하려면 벡터의 내부 동작 방식을 알아야 합니다. 이런 성능 이슈는 메모리 재할당과 관련이 있습니다.
벡터가 가득 차면 더 큰 메모리를 할당하고 기존 데이터를 복사하는 비용이 발생합니다. 이 과정이 반복되면 프로그램 전체의 속도가 느려지고, 최악의 경우 메모리 단편화까지 발생할 수 있습니다.
바로 이럴 때 필요한 것이 capacity와 len의 이해입니다. 이 두 개념의 차이를 명확히 알면 벡터를 효율적으로 사용할 수 있고, 성능 문제를 사전에 예방할 수 있습니다.
개요
간단히 말해서, len()은 벡터에 실제로 저장된 요소의 개수이고, capacity()는 재할당 없이 저장할 수 있는 최대 요소 개수입니다. len은 논리적 크기, capacity는 물리적 크기라고 생각하면 됩니다.
이 두 개념은 실무에서 성능 최적화의 핵심입니다. 예를 들어, 1000개의 로그 메시지를 저장할 계획이라면 처음부터 capacity를 1000으로 설정하면 재할당이 일어나지 않아 훨씬 빠릅니다.
데이터베이스 쿼리 결과나 파일 파싱 결과를 저장할 때 특히 중요합니다. 기존에는 이런 세밀한 제어가 어려웠습니다.
하지만 Rust의 Vec는 len과 capacity를 명확히 구분하여 메모리 사용량과 성능을 정밀하게 제어할 수 있게 해줍니다. 여러분은 언제 메모리를 할당할지, 얼마나 할당할지를 직접 결정할 수 있습니다.
이 개념의 핵심 특징은 세 가지입니다. 첫째, len은 push/pop에 따라 변하지만 capacity는 필요할 때만 증가합니다.
둘째, capacity는 항상 len보다 크거나 같습니다. 셋째, reserve()나 shrink_to_fit() 같은 메서드로 capacity를 수동으로 조절할 수 있습니다.
이러한 특징들이 벡터를 메모리 효율적이고 성능이 좋은 자료구조로 만들어줍니다.
코드 예제
// 빈 벡터로 시작
let mut v: Vec<i32> = Vec::new();
println!("초기 - len: {}, capacity: {}", v.len(), v.capacity());
// 요소 추가하면서 관찰
v.push(1);
println!("1개 추가 - len: {}, capacity: {}", v.len(), v.capacity());
v.push(2);
v.push(3);
v.push(4);
v.push(5); // capacity 증가가 일어날 수 있음
println!("5개 추가 - len: {}, capacity: {}", v.len(), v.capacity());
// 용량 미리 확보
let mut optimized = Vec::with_capacity(100);
println!("최적화 - len: {}, capacity: {}", optimized.len(), optimized.capacity());
// len은 0이지만 capacity는 100
설명
capacity와 len의 동작 원리를 이해하는 것은 벡터를 효율적으로 사용하는 첫걸음입니다. 벡터는 내부적으로 힙에 연속된 메모리 블록을 할당받아 데이터를 저장하는데, len은 이 중에서 실제로 사용 중인 슬롯의 개수이고, capacity는 할당된 전체 슬롯의 개수입니다.
처음에 Vec::new()로 만든 벡터는 len도 0이고 capacity도 0입니다. 아직 힙 메모리를 전혀 할당받지 않은 상태입니다.
첫 번째 push(1)을 호출하면, Rust는 작은 초기 용량(보통 4 정도)을 할당하고 첫 번째 슬롯에 1을 저장합니다. 이제 len은 1, capacity는 4가 됩니다.
계속해서 2, 3, 4를 push하면 len은 2, 3, 4로 증가하지만 capacity는 여전히 4입니다. 아직 할당된 공간이 충분하기 때문입니다.
하지만 다섯 번째 요소인 5를 push하는 순간, capacity가 부족하여 재할당이 발생합니다. Rust는 보통 기존 capacity의 2배 크기로 새 메모리를 할당하고, 기존 데이터를 모두 복사한 후 5를 추가합니다.
이제 len은 5, capacity는 8이 됩니다. with_capacity(100)으로 생성한 벡터는 처음부터 100개의 슬롯을 확보합니다.
len은 0이지만 capacity는 100입니다. 이는 마치 100석짜리 영화관을 예약했지만 아직 관객이 없는 상태와 같습니다.
100개까지는 아무리 push해도 재할당이 일어나지 않아 매우 빠릅니다. 여러분이 이 개념을 이해하면 성능 병목을 정확히 진단할 수 있습니다.
프로파일링 결과 메모리 할당이 많다면, with_capacity()나 reserve()로 미리 공간을 확보하는 것만으로도 큰 성능 향상을 얻을 수 있습니다. 반대로 메모리 사용량을 줄이고 싶다면 shrink_to_fit()으로 불필요한 capacity를 해제할 수 있습니다.
실전 팁
💡 대량의 데이터를 처리할 때는 예상 크기로 with_capacity()를 사용하세요. 재할당 횟수를 0으로 만들어 성능을 대폭 개선할 수 있습니다.
💡 벡터가 더 이상 커지지 않는다면 shrink_to_fit()으로 여분의 메모리를 해제하세요. 장기 실행 프로그램에서 메모리 절약에 도움됩니다.
💡 capacity는 힌트일 뿐 보장되지 않습니다. Rust 구현에 따라 요청한 것보다 약간 더 큰 값이 할당될 수 있습니다.
💡 성능이 중요한 코드에서는 벤치마크를 통해 최적의 초기 capacity를 실험적으로 찾으세요. 너무 크면 메모리 낭비, 너무 작으면 재할당 비용이 발생합니다.
5. reserve() 메서드 - 용량 미리 확보하기
시작하며
여러분이 파일에서 10만 줄을 읽어서 벡터에 저장하는 프로그램을 작성한다고 상상해보세요. 줄을 하나씩 push하면서 계속해서 재할당이 발생하면 성능이 심각하게 저하됩니다.
이런 문제는 대용량 데이터를 다루는 실무에서 매우 흔합니다. CSV 파싱, 로그 분석, 대량 API 호출 결과 저장 등 데이터가 많을수록 재할당 비용이 누적되어 프로그램이 느려집니다.
사용자는 기다리는 시간이 길어지고, 서버는 불필요한 CPU를 소비합니다. 바로 이럴 때 필요한 것이 reserve() 메서드입니다.
reserve는 추가로 필요한 용량을 미리 확보하여 재할당을 최소화하는 강력한 최적화 도구입니다.
개요
간단히 말해서, reserve(n)은 현재 len에서 추가로 n개의 요소를 더 저장할 수 있도록 capacity를 확장하는 메서드입니다. 재할당이 필요하면 한 번에 충분한 메모리를 할당하여 이후 push가 빠르게 동작하도록 합니다.
reserve()는 실무에서 성능 최적화의 핵심 기법입니다. 특히 반복문에서 대량의 데이터를 추가할 때, 예상되는 최종 크기를 알고 있다면 reserve()를 먼저 호출하는 것만으로 실행 시간을 절반 이상 줄일 수 있습니다.
데이터 처리 파이프라인, 배치 작업, 실시간 스트리밍에서 필수적입니다. 기존에는 재할당을 제어하기 어려웠고, 자동 증가에만 의존해야 했습니다.
하지만 reserve()를 사용하면 재할당 시점과 크기를 명시적으로 제어할 수 있습니다. 메모리 사용 패턴을 예측 가능하게 만들어 디버깅과 프로파일링도 쉬워집니다.
reserve()의 핵심 특징은 세 가지입니다. 첫째, 추가 용량을 지정하는 방식이라 현재 len을 고려합니다.
둘째, 이미 충분한 capacity가 있으면 아무 일도 하지 않아 안전하게 호출할 수 있습니다. 셋째, reserve_exact()라는 변형도 있어 정확한 크기만 할당할 수 있습니다.
이러한 특징들이 reserve()를 유연하고 효율적인 메서드로 만들어줍니다.
코드 예제
// 시나리오: 1000개의 로그를 읽어서 필터링
let mut logs = Vec::new();
logs.reserve(1000); // 1000개 공간 미리 확보
println!("예약 후 capacity: {}", logs.capacity()); // 최소 1000
// 실제 데이터 추가 (재할당 없이 빠르게 진행)
for i in 0..1000 {
logs.push(format!("Log entry #{}", i));
}
println!("추가 후 len: {}, capacity: {}", logs.len(), logs.capacity());
// 추가 데이터가 더 필요할 때
logs.reserve(500); // 현재 len 기준으로 500개 더 확보
println!("추가 예약 후 capacity: {}", logs.capacity());
// reserve_exact: 정확히 필요한 만큼만
let mut buffer = Vec::new();
buffer.reserve_exact(100); // 딱 100개만 (메모리 절약)
설명
reserve() 메서드가 하는 일은 미래를 대비한 공간 확보입니다. 현재 벡터의 len이 10이고 reserve(90)을 호출하면, 총 100개까지 저장할 수 있도록 capacity를 확장합니다.
이미 capacity가 100 이상이면 아무 작업도 하지 않아 불필요한 할당을 피합니다. 첫 번째 예제는 전형적인 사용 패턴입니다.
1000개의 로그를 읽어야 하는 상황에서, 처음에 logs.reserve(1000)을 호출하여 충분한 공간을 확보합니다. 이제 반복문에서 1000번 push를 해도 단 한 번의 재할당도 일어나지 않습니다.
재할당 없이 진행되므로 데이터 복사 비용이 전혀 없어 매우 빠릅니다. 두 번째 부분에서 logs.reserve(500)을 다시 호출합니다.
현재 len이 1000이므로, 이는 "1000 + 500 = 1500개까지 저장할 수 있게 해달라"는 의미입니다. 만약 현재 capacity가 이미 1500 이상이면 아무 일도 일어나지 않고, 부족하면 최소 1500까지 확장됩니다.
이런 점진적 확장 패턴은 데이터가 여러 단계로 추가되는 상황에서 유용합니다. reserve_exact()는 reserve()의 변형으로, 정확히 요청한 만큼만 할당합니다.
reserve()는 성장 전략상 조금 더 할당할 수 있지만, reserve_exact(100)은 정확히 100개 공간만 확보합니다. 메모리가 제한적인 임베디드 시스템이나, 최종 크기가 정확히 알려진 경우에 적합합니다.
여러분이 이 메서드를 사용하면 성능 최적화가 매우 쉬워집니다. 특히 벤치마크 결과, 재할당이 병목이라면 reserve() 한 줄 추가만으로 극적인 개선을 볼 수 있습니다.
또한 메모리 할당 패턴이 예측 가능해져서 메모리 프로파일러로 분석하기도 쉬워지고, 메모리 단편화도 줄어듭니다.
실전 팁
💡 예상 크기를 정확히 모르더라도 대략적인 상한선으로 reserve()하는 것이 좋습니다. 과다 할당해도 나중에 shrink_to_fit()으로 정리할 수 있습니다.
💡 reserve()는 멱등성(idempotent)이 있어 여러 번 호출해도 안전합니다. 이미 충분하면 아무 일도 안 하므로 방어적으로 호출할 수 있습니다.
💡 성능이 중요한 경우, reserve() 전후로 벤치마크를 돌려보세요. 재할당 비용이 큰 경우 10배 이상 빨라질 수 있습니다.
💡 reserve()는 capacity만 변경하고 len은 그대로입니다. 공간을 확보했다고 해서 인덱스 접근이 가능해지는 것은 아니니 주의하세요.
6. pop() 메서드 - 벡터의 마지막 요소 제거하기
시작하며
여러분이 실행 취소(Undo) 기능을 구현하거나, 스택 자료구조를 만들거나, 처리 완료된 작업을 제거해야 하는 상황을 생각해보세요. 마지막에 추가된 요소를 안전하게 꺼내야 하는 경우가 많습니다.
이런 패턴은 많은 알고리즘과 데이터 구조에서 필수적입니다. 깊이 우선 탐색(DFS), 역순 처리, 임시 버퍼 관리 등에서 LIFO(Last In First Out) 동작이 필요합니다.
잘못 구현하면 인덱스 에러나 패닉이 발생할 수 있어 주의가 필요합니다. 바로 이럴 때 필요한 것이 pop() 메서드입니다.
pop은 벡터의 마지막 요소를 안전하게 제거하고 반환하는 메서드로, Option 타입을 사용하여 빈 벡터에서도 안전하게 동작합니다.
개요
간단히 말해서, pop()은 벡터의 마지막 요소를 제거하고 Option<T>로 감싸서 반환하는 메서드입니다. 요소가 있으면 Some(value), 비어있으면 None을 반환하여 안전성을 보장합니다.
pop() 메서드는 실무에서 스택 기반 알고리즘이나 임시 데이터 관리에 자주 사용됩니다. 예를 들어, 작업 큐에서 가장 최근 작업을 꺼내거나, 괄호 매칭 알고리즘을 구현하거나, 트랜잭션 롤백을 위한 히스토리 관리에 유용합니다.
O(1) 시간 복잡도로 매우 효율적입니다. 기존 언어에서는 빈 배열에서 pop을 시도하면 예외가 발생하거나 undefined 동작이 나타났습니다.
하지만 Rust의 pop()은 Option을 반환하여 컴파일 타임에 모든 경우를 처리하도록 강제합니다. 이는 런타임 에러를 원천적으로 방지하는 Rust의 안전 철학을 잘 보여줍니다.
pop()의 핵심 특징은 세 가지입니다. 첫째, Option<T>를 반환하여 실패 가능성을 타입 시스템으로 표현합니다.
둘째, 요소를 제거하면서 소유권을 이동시켜 메모리 안전성을 보장합니다. 셋째, capacity는 변하지 않고 len만 감소하여 메모리 재할당이 없습니다.
이러한 특징들이 pop()을 안전하고 효율적인 메서드로 만들어줍니다.
코드 예제
// 기본 사용: 스택처럼 동작
let mut stack = vec![1, 2, 3, 4, 5];
println!("초기 스택: {:?}", stack);
// 하나씩 꺼내기
if let Some(value) = stack.pop() {
println!("꺼낸 값: {}", value); // 5
}
println!("스택: {:?}", stack); // [1, 2, 3, 4]
// 모두 꺼내기
while let Some(value) = stack.pop() {
println!("처리 중: {}", value); // 4, 3, 2, 1 순서
}
println!("최종 스택: {:?}", stack); // []
// 빈 벡터에서 pop - 안전함
let empty_result = stack.pop();
println!("빈 벡터 pop 결과: {:?}", empty_result); // None
설명
pop() 메서드가 하는 일은 매우 명확합니다. 벡터의 len을 1 감소시키고, 마지막 위치에 있던 요소의 소유권을 호출자에게 이동시킵니다.
벡터가 비어있다면 아무것도 제거할 것이 없으므로 None을 반환합니다. 첫 번째 예제는 기본적인 pop 사용법을 보여줍니다.
stack.pop()을 호출하면 마지막 요소인 5가 제거되고 Some(5)가 반환됩니다. if let Some(value) 패턴은 값이 존재하는 경우만 처리하는 안전한 방법입니다.
pop 후의 스택은 [1, 2, 3, 4]로 길이가 하나 줄어듭니다. 두 번째 예제는 while let 패턴으로 모든 요소를 꺼내는 전형적인 패턴입니다.
stack.pop()이 Some(value)를 반환하는 동안 반복하고, None이 반환되면 (즉, 벡터가 비면) 자동으로 종료됩니다. 이는 스택을 비우거나, 역순으로 처리하거나, 모든 요소를 소비해야 할 때 매우 유용합니다.
4, 3, 2, 1 순서로 출력되는 것을 주목하세요. 세 번째 예제는 pop()의 안전성을 보여줍니다.
이미 비어있는 벡터에서 pop()을 호출해도 패닉이 발생하지 않고, 단순히 None을 반환합니다. 이를 통해 여러분은 벡터가 비어있는지 별도로 확인할 필요 없이, pop의 반환값만으로 모든 경우를 처리할 수 있습니다.
여러분이 이 메서드를 사용하면 스택 기반 알고리즘을 매우 쉽게 구현할 수 있습니다. Option 타입 덕분에 컴파일러가 모든 경우의 수를 처리하도록 강제하여, 실수로 빈 벡터에 접근하는 버그를 원천적으로 방지합니다.
또한 pop은 O(1) 연산이므로 성능 걱정 없이 자주 호출할 수 있습니다.
실전 팁
💡 pop()은 Option을 반환하므로 unwrap()보다는 if let이나 match로 안전하게 처리하세요. unwrap()은 None일 때 패닉을 일으킵니다.
💡 벡터를 역순으로 처리하려면 while let Some(item) = vec.pop() 패턴이 가장 효율적입니다. 인덱스 접근보다 빠르고 간결합니다.
💡 pop은 capacity를 변경하지 않습니다. 많은 요소를 pop한 후 메모리를 해제하려면 shrink_to_fit()을 명시적으로 호출하세요.
💡 pop 대신 첫 요소를 제거하려면 remove(0)을 사용할 수 있지만, 이는 O(n) 연산입니다. 빈번한 앞쪽 제거가 필요하면 VecDeque를 고려하세요.
7. 인덱스 접근과 get() - 안전한 요소 읽기
시작하며
여러분이 사용자가 입력한 번호로 리스트의 항목에 접근하는 코드를 작성한다고 생각해보세요. 사용자가 잘못된 번호를 입력하면 프로그램이 크래시될 수 있습니다.
이런 문제는 실무에서 매우 흔합니다. 설정 파일의 인덱스, API 파라미터, 데이터베이스 쿼리 결과 등 외부에서 들어오는 인덱스는 항상 범위를 벗어날 가능성이 있습니다.
잘못 처리하면 서비스가 다운되고 사용자 경험이 나빠집니다. 바로 이럴 때 필요한 것이 안전한 인덱스 접근 방법입니다.
Rust는 [ ] 연산자와 get() 메서드라는 두 가지 방법을 제공하며, 각각 다른 상황에 적합합니다.
개요
간단히 말해서, vec[index]는 직접 접근으로 빠르지만 범위를 벗어나면 패닉이 발생하고, vec.get(index)는 Option을 반환하여 안전하게 처리할 수 있습니다. 인덱스가 확실히 유효한 경우 전자를, 유효성이 불확실한 경우 후자를 사용합니다.
이 두 방법은 실무에서 상황에 따라 선택적으로 사용됩니다. 내부 로직에서 알고리즘적으로 인덱스가 보장되는 경우 [ ]로 간결하게 작성하고, 사용자 입력이나 외부 데이터에서 오는 인덱스는 get()으로 안전하게 처리합니다.
적절한 선택이 코드의 명확성과 안전성을 모두 높여줍니다. 기존 언어에서는 배열 접근이 항상 위험했습니다.
범위 검사를 수동으로 해야 했고, 깜빡하면 버퍼 오버플로우나 세그멘테이션 폴트가 발생했습니다. Rust는 [ ] 연산자에 자동 범위 검사를 추가하고, get()으로 더 안전한 대안을 제공하여 이 문제를 해결했습니다.
핵심 특징은 세 가지입니다. 첫째, [ ] 연산자는 참조(&T)를 반환하여 소유권을 이동시키지 않습니다.
둘째, get()은 Option<&T>를 반환하여 존재하지 않는 인덱스를 타입 시스템으로 처리합니다. 셋째, get_mut()으로 가변 참조를 얻을 수도 있습니다.
이러한 특징들이 Rust의 벡터 접근을 안전하고 유연하게 만들어줍니다.
코드 예제
let numbers = vec![10, 20, 30, 40, 50];
// 방법 1: 인덱스 연산자 - 확실한 경우
let first = numbers[0]; // 10
let third = numbers[2]; // 30
println!("첫 번째: {}, 세 번째: {}", first, third);
// 방법 2: get() - 안전한 접근
match numbers.get(10) {
Some(value) => println!("값: {}", value),
None => println!("인덱스가 범위를 벗어남"),
}
// 실용적 패턴: 사용자 입력 처리
let user_index = 3; // 사용자가 입력했다고 가정
if let Some(value) = numbers.get(user_index) {
println!("선택한 값: {}", value); // 40
} else {
println!("잘못된 인덱스입니다.");
}
// get_mut()으로 수정
let mut data = vec![1, 2, 3];
if let Some(elem) = data.get_mut(1) {
*elem = 20; // 두 번째 요소를 20으로 변경
}
println!("수정된 데이터: {:?}", data); // [1, 20, 3]
설명
인덱스 접근의 두 가지 방법은 각각 명확한 용도가 있습니다. [ ] 연산자는 "이 인덱스는 확실히 유효해"라고 단언하는 것이고, get()은 "이 인덱스가 유효할 수도, 아닐 수도 있어"라고 인정하는 것입니다.
첫 번째 방법인 numbers[0]은 가장 직관적이고 다른 언어와 비슷합니다. 컴파일러는 실행 시점에 인덱스가 범위 내에 있는지 자동으로 검사하고, 벗어나면 패닉을 발생시켜 프로그램을 즉시 종료합니다.
이는 논리적 오류를 빠르게 발견하는 데 도움이 되지만, 복구 불가능한 에러이므로 조심해야 합니다. numbers[2]는 30을 반환하며, 이는 원본 값의 복사본(i32는 Copy)입니다.
두 번째 방법인 get(10)은 범위를 벗어난 인덱스입니다. numbers는 5개 요소(인덱스 0~4)만 가지고 있으므로, 10은 유효하지 않습니다.
하지만 패닉 대신 None을 반환하여, match나 if let으로 우아하게 처리할 수 있습니다. 이 방식은 사용자 입력, API 응답, 설정 파일 파싱 등 외부 데이터를 다룰 때 필수적입니다.
세 번째 예제는 실무에서 가장 흔한 패턴입니다. user_index는 사용자로부터 받은 값이므로 신뢰할 수 없습니다.
get()으로 안전하게 접근하고, if let Some으로 값이 존재하는 경우만 처리합니다. 값이 없으면 친절한 에러 메시지를 출력하여 사용자 경험을 개선합니다.
네 번째 예제의 get_mut()은 가변 참조를 반환합니다. *elem = 20은 참조를 역참조하여 실제 값을 수정하는 것입니다.
이 역시 Option을 반환하므로, 존재하지 않는 인덱스를 수정하려는 시도를 안전하게 막을 수 있습니다. 여러분이 이 방법들을 적절히 사용하면 코드가 더욱 견고해집니다.
내부 로직에서는 [ ]로 간결하게 작성하여 의도를 명확히 하고, 외부 입력에서는 get()으로 안전하게 처리하여 예상치 못한 크래시를 방지할 수 있습니다. 코드 리뷰 시에도 어떤 인덱스가 안전하고 어떤 것이 검증이 필요한지 명확히 드러납니다.
실전 팁
💡 [ ] 연산자를 사용하기 전에는 반드시 인덱스가 유효함을 확신해야 합니다. 불확실하면 get()을 사용하세요.
💡 반복문에서 인덱스로 접근하는 대신 iter()나 for item in &vec 패턴을 사용하면 더 안전하고 간결합니다.
💡 get()은 범위 검사를 두 번 하지 않습니다. [ ] 연산자와 성능 차이가 거의 없으니 안전성이 필요하면 주저 없이 사용하세요.
💡 Option을 unwrap()하지 말고 if let이나 match로 처리하세요. unwrap()은 None일 때 패닉을 일으켜 get()의 안전성을 무효화합니다.
8. iter()와 반복 - 벡터 순회의 모범 사례
시작하며
여러분이 벡터의 모든 요소를 하나씩 처리해야 할 때, 인덱스를 사용한 for 루프를 작성하다가 off-by-one 에러를 겪어본 적 있나요? 인덱스 관리는 생각보다 실수하기 쉽습니다.
이런 문제는 특히 중첩 루프나 복잡한 조건문이 있을 때 더욱 심각해집니다. 인덱스를 잘못 계산하거나, 범위를 벗어나거나, 수정 중인 벡터의 크기가 변해서 예상치 못한 버그가 발생합니다.
디버깅도 어렵고 코드도 복잡해집니다. 바로 이럴 때 필요한 것이 반복자(iterator)입니다.
Rust의 iter() 메서드는 인덱스 없이 안전하고 간결하게 벡터를 순회할 수 있는 강력한 도구입니다.
개요
간단히 말해서, iter()는 벡터의 각 요소에 대한 불변 참조를 순서대로 제공하는 반복자를 생성하는 메서드입니다. 인덱스 관리 없이 for 루프나 함수형 메서드 체이닝으로 안전하고 효율적으로 순회할 수 있습니다.
iter()는 실무에서 가장 권장되는 순회 방법입니다. 데이터 변환, 필터링, 집계 연산 등 거의 모든 컬렉션 처리에 사용됩니다.
인덱스 에러가 원천적으로 불가능하고, 컴파일러 최적화도 잘 적용되어 수동 인덱스 루프보다 종종 더 빠릅니다. 기존 C 스타일 for 루프에서는 for(int i=0; i<len; i++)처럼 인덱스를 직접 관리해야 했습니다.
Python의 for item in list는 편리하지만 타입 안전성이 부족합니다. Rust의 iter()는 두 장점을 결합하여 안전하고 표현력 높은 반복을 제공합니다.
iter()의 핵심 특징은 세 가지입니다. 첫째, 불변 참조(&T)를 제공하여 원본을 수정하지 않습니다.
둘째, iter_mut()으로 가변 참조를, into_iter()로 소유권 이동을 선택할 수 있습니다. 셋째, map, filter, collect 등 함수형 메서드와 체이닝하여 강력한 데이터 처리 파이프라인을 만들 수 있습니다.
이러한 특징들이 iter()를 현대적이고 안전한 순회 방법으로 만들어줍니다.
코드 예제
let numbers = vec![1, 2, 3, 4, 5];
// 방법 1: for 루프와 iter()
for num in numbers.iter() {
println!("숫자: {}", num); // num은 &i32 타입
}
// 방법 2: 간단한 형태 (자동으로 iter() 호출)
for num in &numbers {
println!("숫자: {}", num);
}
// 방법 3: iter_mut()으로 수정
let mut data = vec![1, 2, 3, 4, 5];
for num in data.iter_mut() {
*num *= 2; // 각 요소를 2배로
}
println!("2배: {:?}", data); // [2, 4, 6, 8, 10]
// 방법 4: 함수형 스타일 - map, filter, collect
let squares: Vec<i32> = numbers.iter()
.map(|x| x * x)
.filter(|x| x % 2 == 0)
.collect();
println!("짝수 제곱: {:?}", squares); // [4, 16]
// enumerate()로 인덱스와 함께
for (index, value) in numbers.iter().enumerate() {
println!("[{}] = {}", index, value);
}
설명
반복자가 하는 일은 컬렉션의 각 요소를 하나씩 꺼내서 제공하는 것입니다. 내부적으로 현재 위치를 추적하지만, 여러분은 그 세부사항을 신경 쓸 필요 없이 값만 받아서 처리하면 됩니다.
첫 번째 방법은 가장 명시적인 형태입니다. numbers.iter()는 Iterator<Item=&i32> 타입의 반복자를 생성하고, for 루프가 이를 자동으로 순회합니다.
각 반복에서 num은 &i32 타입의 참조이므로, 원본 벡터는 전혀 영향받지 않습니다. 이는 벡터를 읽기만 하고 수정하지 않는 대부분의 경우에 적합합니다.
두 번째 방법인 for num in &numbers는 문법적 설탕(syntactic sugar)입니다. &numbers는 자동으로 numbers.iter()로 변환되어 동일하게 동작합니다.
코드가 더 간결하고 읽기 쉬워서 실무에서 많이 사용됩니다. &를 붙였다는 것 자체가 "참조로 순회한다"는 의도를 명확히 드러냅니다.
세 번째 방법인 iter_mut()은 요소를 수정해야 할 때 사용합니다. 각 반복에서 num은 &mut i32 타입의 가변 참조이므로, *num *= 2처럼 역참조하여 실제 값을 변경할 수 있습니다.
벡터의 모든 요소를 제자리에서(in-place) 변환하는 효율적인 방법입니다. 네 번째 방법은 함수형 프로그래밍 스타일입니다.
iter()가 반환한 반복자에 map()으로 각 요소를 제곱하고, filter()로 짝수만 걸러내고, collect()로 다시 Vec로 수집합니다. 이 모든 과정은 지연 평가(lazy evaluation)되어 메모리 효율적이며, 중간 벡터를 만들지 않습니다.
복잡한 데이터 변환을 선언적으로 표현할 수 있어 가독성이 뛰어납니다. 다섯 번째 예제의 enumerate()는 인덱스가 정말 필요할 때 사용합니다.
(index, value) 튜플을 반환하여 인덱스와 값을 동시에 얻을 수 있습니다. 수동으로 인덱스 변수를 관리하는 것보다 훨씬 안전하고 명확합니다.
여러분이 이 반복자 패턴을 사용하면 코드가 더욱 안전하고 표현력이 높아집니다. 컴파일러는 참조 규칙을 강제하여 순회 중 벡터를 잘못 수정하는 실수를 방지하고, 함수형 메서드는 복잡한 데이터 처리를 간결하게 표현할 수 있게 해줍니다.
실전 팁
💡 대부분의 경우 for num in &vec 형태가 가장 간결하고 명확합니다. 명시적으로 iter()를 호출할 필요는 거의 없습니다.
💡 요소를 수정하려면 for num in &mut vec을 사용하세요. iter_mut()이 자동으로 호출됩니다.
💡 함수형 스타일(map, filter)은 체이닝이 길어질수록 강력하지만, 과도하면 가독성이 떨어질 수 있습니다. 균형을 유지하세요.
💡 into_iter()는 소유권을 이동시켜 원본 벡터를 소비합니다. 벡터를 다시 사용할 필요가 없을 때만 사용하세요.
9. clear()와 truncate() - 벡터 비우기와 크기 조정
시작하며
여러분이 캐시를 초기화하거나, 임시 버퍼를 재사용하거나, 벡터의 일부만 남기고 나머지를 제거해야 하는 상황을 생각해보세요. 매번 새 벡터를 만들면 메모리 할당 비용이 발생합니다.
이런 문제는 반복적인 작업에서 성능에 영향을 줍니다. 예를 들어, 실시간 데이터 처리에서 버퍼를 매번 새로 만들면 가비지 컬렉션 압박이나 할당 오버헤드가 누적됩니다.
기존 벡터를 효율적으로 재사용하는 방법이 필요합니다. 바로 이럴 때 필요한 것이 clear()와 truncate() 메서드입니다.
이 메서드들은 벡터의 내용을 제거하면서도 capacity를 유지하여 메모리를 재사용할 수 있게 해줍니다.
개요
간단히 말해서, clear()는 모든 요소를 제거하여 벡터를 비우고, truncate(len)은 지정한 길이 이후의 요소들을 제거합니다. 두 메서드 모두 capacity는 변경하지 않아 메모리 재할당 없이 다시 사용할 수 있습니다.
이 메서드들은 실무에서 리소스 재사용과 성능 최적화에 중요합니다. 반복적으로 데이터를 수집하고 처리하는 파이프라인, 웹 서버의 요청별 임시 버퍼, 게임의 프레임당 임시 객체 리스트 등에서 할당 비용을 줄여 성능을 개선합니다.
기존에는 벡터를 초기화하려면 새 인스턴스를 만들거나, 수동으로 모든 요소를 pop하거나, 위험한 포인터 조작이 필요했습니다. Rust는 clear()와 truncate()로 안전하고 효율적인 방법을 제공하여 메모리 관리를 단순화했습니다.
핵심 특징은 세 가지입니다. 첫째, len을 0이나 지정한 값으로 설정하지만 capacity는 유지합니다.
둘째, 제거된 요소들의 drop()이 호출되어 메모리 안전성이 보장됩니다. 셋째, O(n) 시간 복잡도이지만 재할당이 없어 빠릅니다.
이러한 특징들이 이 메서드들을 효율적인 재사용 도구로 만들어줍니다.
코드 예제
// clear(): 모든 요소 제거
let mut buffer = vec![1, 2, 3, 4, 5];
println!("초기 - len: {}, capacity: {}", buffer.len(), buffer.capacity());
buffer.clear();
println!("clear 후 - len: {}, capacity: {}", buffer.len(), buffer.capacity());
// len은 0, capacity는 유지됨
// 재사용 가능
buffer.push(10);
buffer.push(20);
println!("재사용 후: {:?}", buffer); // [10, 20]
// truncate(): 지정한 길이로 자르기
let mut data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
data.truncate(5); // 앞의 5개만 남기고 제거
println!("truncate 후: {:?}", data); // [1, 2, 3, 4, 5]
// len보다 큰 값으로 truncate - 아무 일도 안 함
data.truncate(100);
println!("변화 없음: {:?}", data); // 여전히 [1, 2, 3, 4, 5]
설명
clear()와 truncate()가 하는 일은 벡터의 논리적 크기(len)를 줄이는 것입니다. 메모리는 그대로 유지하면서 "이 벡터는 이제 비어있어" 또는 "이 벡터는 n개 요소만 가지고 있어"라고 표시하는 것이죠.
clear() 메서드는 가장 단순합니다. buffer.clear()를 호출하면 len이 0이 되어 벡터가 완전히 비워집니다.
하지만 capacity는 그대로이므로, 할당된 메모리는 여전히 벡터가 소유하고 있습니다. 제거된 요소들은 적절히 drop되어, String이나 다른 힙 할당 타입도 메모리 누수 없이 정리됩니다.
clear() 후 buffer는 마치 Vec::new()로 만든 것처럼 보이지만, 중요한 차이가 있습니다. 이미 capacity가 확보되어 있어서 다시 push할 때 재할당이 필요 없습니다.
예제에서 buffer.push(10)과 buffer.push(20)은 즉시 실행되며, 기존 메모리에 값을 쓰기만 합니다. 이는 반복적인 작업에서 큰 성능 이점을 제공합니다.
truncate(5) 메서드는 더 유연합니다. data 벡터의 처음 5개 요소만 남기고 나머지를 제거합니다.
인덱스 5 이후의 요소들(6, 7, 8, 9, 10)이 모두 drop되고, len이 5로 설정됩니다. 이는 데이터의 앞부분만 필요하고 뒷부분은 버려도 될 때 유용합니다.
truncate()에 현재 len보다 큰 값을 주면 아무 일도 일어나지 않습니다. data.truncate(100)은 이미 len이 5이므로 변화가 없습니다.
이는 안전 장치로, 실수로 큰 값을 주어도 벡터가 손상되지 않습니다. 여러분이 이 메서드들을 사용하면 메모리 할당 횟수를 크게 줄일 수 있습니다.
특히 루프 안에서 벡터를 반복적으로 사용하는 경우, 매번 Vec::new()와 drop을 반복하는 대신 clear()로 재사용하면 성능이 눈에 띄게 개선됩니다.
실전 팁
💡 반복적인 작업에서는 벡터를 재생성하지 말고 clear()로 재사용하세요. 할당 비용을 없애 성능을 크게 향상시킬 수 있습니다.
💡 truncate(0)은 clear()와 동일하지만, 의도를 명확히 하려면 clear()를 사용하는 것이 좋습니다.
💡 String도 clear()를 가지고 있습니다. String은 내부적으로 Vec<u8>이므로 같은 원리로 작동합니다.
💡 많은 메모리를 사용하는 벡터를 clear() 후 오랫동안 유지한다면, shrink_to_fit()으로 불필요한 capacity를 해제하는 것을 고려하세요.
10. remove()와 swap_remove() - 중간 요소 제거하기
시작하며
여러분이 할 일 목록에서 완료된 항목을 제거하거나, 사용자 리스트에서 특정 사용자를 삭제하는 기능을 구현한다고 생각해보세요. pop()은 마지막 요소만 제거하므로 충분하지 않습니다.
이런 상황은 실무에서 매우 자주 발생합니다. 쇼핑 카트에서 특정 상품 삭제, 플레이리스트에서 노래 제거, 채팅방에서 메시지 삭제 등 중간 위치의 요소를 제거해야 하는 경우가 많습니다.
성능도 중요한 고려사항입니다. 바로 이럴 때 필요한 것이 remove()와 swap_remove() 메서드입니다.
두 메서드는 중간 요소를 제거하지만 성능 특성이 다르며, 상황에 따라 적절한 것을 선택해야 합니다.
개요
간단히 말해서, remove(index)는 지정한 인덱스의 요소를 제거하고 뒤의 요소들을 앞으로 이동시켜 순서를 유지합니다(O(n)). swap_remove(index)는 마지막 요소와 교체한 후 제거하여 빠르지만 순서가 바뀝니다(O(1)).
이 메서드들은 실무에서 동적 리스트 관리에 필수적입니다. 순서가 중요한 경우(예: 타임라인, 순위표)에는 remove()를, 순서가 상관없는 경우(예: 집합, 객체 풀)에는 swap_remove()를 사용하여 성능을 최적화합니다.
기존 방식에서는 배열 중간 요소를 제거하려면 수동으로 요소들을 이동시키거나, 제거 표시만 하고 나중에 정리하는 복잡한 방법을 써야 했습니다. Rust는 두 가지 명확한 옵션을 제공하여 상황에 맞는 선택을 가능하게 합니다.
핵심 특징은 세 가지입니다. 첫째, remove()는 순서를 유지하지만 O(n) 비용이 듭니다.
둘째, swap_remove()는 O(1)로 빠르지만 마지막 요소가 제거된 위치로 이동합니다. 셋째, 두 메서드 모두 제거된 요소의 소유권을 반환하여 값을 재사용할 수 있습니다.
이러한 특징들이 상황에 맞는 유연한 제거를 가능하게 합니다.
코드 예제
// remove(): 순서 유지하며 제거
let mut tasks = vec!["설계", "개발", "테스트", "배포", "유지보수"];
let removed = tasks.remove(2); // "테스트" 제거
println!("제거된 항목: {}", removed); // "테스트"
println!("남은 작업: {:?}", tasks); // ["설계", "개발", "배포", "유지보수"]
// 순서가 유지됨
// swap_remove(): 빠르지만 순서 바뀜
let mut users = vec!["Alice", "Bob", "Charlie", "David", "Eve"];
let removed_user = users.swap_remove(1); // "Bob" 제거
println!("제거된 사용자: {}", removed_user); // "Bob"
println!("남은 사용자: {:?}", users);
// ["Alice", "Eve", "Charlie", "David"]
// Eve(마지막)가 Bob 자리로 이동
// 성능 비교 시나리오
let mut numbers = vec![1, 2, 3, 4, 5];
numbers.remove(0); // [2,3,4,5] - 모든 요소 이동
println!("remove: {:?}", numbers);
let mut numbers2 = vec![1, 2, 3, 4, 5];
numbers2.swap_remove(0); // [5,2,3,4] - 5가 1자리로
println!("swap_remove: {:?}", numbers2);
설명
두 제거 메서드가 하는 일은 같지만 방식이 다릅니다. remove()는 신중하게 순서를 지키며 공간을 메우고, swap_remove()는 마지막 요소를 끌어와 빈 자리를 빠르게 채웁니다.
remove(2)는 인덱스 2의 "테스트"를 제거합니다. 제거 후 인덱스 3, 4에 있던 "배포", "유지보수"가 각각 인덱스 2, 3으로 이동합니다.
이 이동 작업 때문에 O(n) 시간이 걸립니다. n이 제거 위치 뒤의 요소 개수입니다.
하지만 순서가 완벽히 유지되어 ["설계", "개발", "배포", "유지보수"]가 됩니다. swap_remove(1)는 다른 접근입니다.
"Bob"을 제거하고, 마지막 요소인 "Eve"를 인덱스 1로 이동시킵니다. 그 다음 len을 1 감소시킵니다.
이는 단 두 번의 작업만 필요하므로 O(1)입니다. 결과는 ["Alice", "Eve", "Charlie", "David"]로 순서가 바뀌었지만 훨씬 빠릅니다.
성능 비교 예제는 차이를 명확히 보여줍니다. numbers.remove(0)은 첫 요소를 제거하고 나머지 4개를 모두 앞으로 이동시킵니다.
최악의 경우(첫 요소 제거)에 가장 많은 이동이 발생합니다. 반면 numbers2.swap_remove(0)은 마지막 요소 5를 첫 번째 자리로 옮기기만 하여 즉시 완료됩니다.
언제 어떤 것을 써야 할까요? 순서가 의미를 가지는 경우 - 타임라인, 재생 목록, 순위 - 에는 remove()를 사용해야 합니다.
순서가 상관없는 경우 - 사용자 집합, 객체 풀, 활성 연결 리스트 - 에는 swap_remove()로 성능을 얻으세요. 여러분이 이 메서드들을 적절히 선택하면 기능과 성능 사이의 균형을 맞출 수 있습니다.
순서가 중요하지 않은데 remove()를 쓰면 불필요한 성능 손실이 발생하고, 순서가 중요한데 swap_remove()를 쓰면 논리적 오류가 생깁니다.
실전 팁
💡 루프에서 remove()를 호출할 때는 주의하세요. 인덱스가 변하므로 역순으로 제거하거나 retain() 메서드를 고려하세요.
💡 대량의 요소를 제거할 때는 remove()를 반복하지 말고, retain()이나 drain()을 사용하는 것이 훨씬 효율적입니다.
💡 swap_remove()는 순서를 바꾸므로, 반복 중에 사용하면 예상치 못한 동작이 발생할 수 있습니다. 제거할 인덱스를 먼저 수집한 후 역순으로 처리하세요.
💡 두 메서드 모두 범위를 벗어난 인덱스에서는 패닉이 발생합니다. 안전하게 처리하려면 먼저 len을 확인하세요.