본문 바로가기

벡터와 값 목록

Vec를 생성·갱신·조회·순회하고 참조 규칙과 열거형을 이용해 길이가 변하는 동종 값 목록을 안전하게 관리합니다.

러스트의 표준 라이브러리에는 컬렉션(collection)이라고 부르는 쓸모 있는 자료 구조가 여럿 있습니다. 다른 데이터 타입은 대개 특정한 값 하나를 나타내지만, 컬렉션은 여러 값을 담습니다. 배열이나 튜플과 달리 컬렉션이 가리키는 데이터는 힙에 저장되므로, 데이터의 양을 컴파일할 때 알 필요가 없고 프로그램이 실행되는 동안 늘거나 줄 수 있습니다. 컬렉션마다 할 수 있는 일과 비용이 다르며, 상황에 맞는 컬렉션을 고르는 것은 시간을 들여 익히는 기술입니다.

7장에서는 러스트 프로그램에서 아주 자주 쓰는 세 가지 컬렉션을 다룹니다. 벡터(vector)는 여러 값을 나란히 저장하고, 문자열(string)은 문자의 모음이며, 해시 맵(hash map)은 값을 특정한 키와 연결합니다. 해시 맵은 맵(map)이라는 더 일반적인 자료 구조를 구현한 것입니다. 표준 라이브러리의 다른 컬렉션은 문서에 있습니다.

이 글에서는 첫 번째 컬렉션인 Vec<T>, 곧 벡터를 다룹니다. 벡터는 값 여러 개를 메모리에 서로 붙여 놓는 하나의 자료 구조에 담으며, 같은 타입의 값만 담을 수 있습니다. 파일의 텍스트 줄이나 장바구니에 담긴 물건의 가격처럼 항목의 목록을 저장할 때 쓸모가 있습니다.

새 벡터 만들기

빈 벡터를 만들려면 Vec::new 함수를 부릅니다. 이 글의 짧은 예제는 main 함수 안에 넣어 실행합니다.

    let v: Vec<i32> = Vec::new();

타입 표기를 붙였다는 점에 주목합니다. 이 벡터에는 아직 값을 넣지 않았으므로, 러스트는 어떤 타입의 원소를 담을지 알 수 없습니다. 벡터는 제네릭으로 구현되어 어떤 타입이든 담을 수 있으며, 제네릭으로 직접 타입을 만드는 방법은 제네릭 데이터 타입에서 다룹니다. 특정한 타입을 담을 벡터를 만들 때는 꺾쇠괄호 안에 그 타입을 적으며, 여기서는 v의 Vec이 i32 원소를 담는다고 알려 주었습니다.

대부분은 처음 값을 넣어 Vec<T>를 만들고 러스트가 그 값으로 타입을 추론하므로, 타입 표기를 할 일이 드뭅니다. 러스트는 주어진 값을 담은 새 Vec을 만드는 vec! 매크로를 제공합니다. 다음 코드는 1, 2, 3을 담은 Vec<i32>를 만듭니다. 데이터 타입에서 본 것처럼 정수의 기본 타입이 i32이므로, 처음 값으로 v의 타입을 Vec<i32>로 추론해 타입 표기가 필요 없습니다.

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

벡터 업데이트하기

벡터를 만든 뒤 원소를 더하려면 push 메서드를 씁니다. push는 원소를 벡터의 끝에 붙이므로, 아래 네 줄을 실행하면 v에는 5, 6, 7, 8이 이 순서로 들어 있습니다.

    let mut v = Vec::new();

    v.push(5);
    v.push(6);
    v.push(7);
    v.push(8);

변수의 값을 바꾸려면 mut으로 가변이어야 한다는 것은 2장에서 보았습니다. 넣은 숫자가 모두 i32이므로 러스트가 v의 타입을 추론하며, 그래서 Vec<i32>라고 적지 않아도 됩니다.

벡터 요소 읽기

벡터에 저장된 값을 가리키는 방법은 인덱스와 get 메서드 두 가지입니다. 다음 코드는 두 방법으로 세 번째 값을 읽으며, 뜻을 분명히 하려고 각 값의 타입을 적었습니다.

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

    let third: &i32 = &v[2];
    println!("The third element is {third}");

    let third: Option<&i32> = v.get(2);
    match third {
        Some(third) => println!("The third element is {third}"),
        None => println!("There is no third element."),
    }
The third element is 3
The third element is 3

벡터의 인덱스는 0부터 시작하므로 세 번째 값은 인덱스 2입니다. &와 []를 쓰면 그 인덱스의 원소를 가리키는 참조를 얻고, get에 인덱스를 넘기면 match로 다룰 수 있는 Option<&T>를 얻습니다. 러스트가 두 방법을 주는 까닭은, 벡터에 없는 인덱스를 쓰려 할 때 프로그램이 어떻게 동작할지 고를 수 있게 하려는 것입니다. 아래 그림은 get이 범위를 확인해 두 경로로 결과를 돌려주는 과정입니다.

Rust Vec 작업과 접근 실패 정책

길이 5 Vec에서 get(2)는 값 3의 참조 Some, get(100)은 None으로 갈리는 실제 조회다. 한 입력의 범위 판정·반환과 별도 입력의 부재를 표시하면 [] 실패 정책과 차이가 드러난다.

Vec 생성과 추가, 인덱싱과 get의 실패 정책, 닫힌 타입 집합과 소유권 종료를 작업별 선택 기준으로 정리한 다이어그램

CREATE · ACCESS · TYPE · DROP

Vec<T>는 작업보다 실패와 소유권 정책을 먼저 고른다

인덱싱이 위험하고 get이 무조건 안전한 것이 아닙니다. 범위 위반을 프로그램 불변식의 실패로 볼지, 정상적으로 처리할 부재로 볼지가 접근 API를 결정합니다.

get이 범위를 판정해 반환하는 두 경로길이5벡터에서usize인덱스i가범위안이면Some요소참조,밖이면None.2는값3,100은없다.인덱싱[]실패정책은원래보조설명과대조한다.예아니요v.get(i)길이 5 · i: usizei < len범위 안인가?Some(&요소)i=2이면 값 3Nonei=100이면 부재
get이 범위를 판정해 반환하는 두 경로길이5벡터에서usize인덱스i가범위안이면Some요소참조,밖이면None.2는값3,100은없다.인덱싱[]실패정책은원래보조설명과대조한다.예아니요v.get(i)길이 5 · i: usizei < len범위 안인가?Some(&요소)i=2이면 값 3Nonei=100이면 부재

보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.

작업별 선택 계약

생성·접근·타입·수명을 실패 정책과 함께 고른다

  1. 생성 · 추론할 근거를 둔다

    let v: Vec<i32> = Vec::new();처럼 다른 타입 제약이 없을 때 표기합니다. vec!로 만든 값 목록과 이후 push는 추론 근거가 됩니다. 용량 0과 ZST는 요소 버퍼를 할당하지 않습니다.

  2. 인덱싱 · 불변식 위반은 패닉

    &v[i]는 범위 안이라는 프로그램 계약을 짧게 표현하며, 위반하면 현재 스레드에서 패닉합니다.

  3. get · 정상적인 부재는 값으로

    v.get(i)는 Option<&T>를 돌려 Some과 None을 호출자가 분기하게 합니다.

  4. 종류의 경계 · 닫힌 집합과 열린 구현

    Box<dyn Trait>을 Vec에 담는 트레이트 객체 경계는 구현 종류가 열려야 할 때 검토합니다. 컴파일 시점에 아는 닫힌 종류는 SpreadsheetCell 같은 enum으로 묶습니다.

  5. 길이와 수명 · 재배치와 소유 값

    push는 가변 벡터 끝에 값을 추가하고, 용량이 부족하면 새 요소 버퍼를 할당해 기존 요소를 옮길 수 있습니다. pop은 마지막 요소를 제거해 그 소유 값을 Option<T>로 반환합니다. 벡터가 드롭되면 남은 소유 요소도 함께 드롭되고 할당한 버퍼가 해제됩니다.

판단 질문: 없는 인덱스가 버그입니까, 입력의 정상적인 부재입니까? 그 답이 &v[i]와 get을 가릅니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명길이 5인 벡터에서 usize 인덱스 i가 범위 안이면 Some 요소 참조, 밖이면 None이다. 2는 값 3을 가리키고, 100에 해당하는 요소는 없다. 인덱싱 []의 실패 정책은 원래 보조 설명과 대조한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
조회 입력

길이 5 Vec에 index 2를 요청한다.

  • len: 5
  • index: 2
  • get의 범위 판정은 요청마다 적용된다.
성공 반환

2<5이므로 값 3을 빌려 반환한다.

  • 조건: true
  • 반환: Some(&3)
  • 값을 복제하거나 Vec의 소유를 넘긴 것이 아니다.
별도 부재 사례

index 100이면 None이다.

  • index: 100
  • 반환: None
  • get의 정상 부재와 []의 범위 밖 panic 정책을 구분한다.

Rust Vec 작업과 접근 실패 정책

길이 5 Vec에서 get(2)는 값 3의 참조 Some, get(100)은 None으로 갈리는 실제 조회다. 한 입력의 범위 판정·반환과 별도 입력의 부재를 표시하면 [] 실패 정책과 차이가 드러난다.

Vec 생성과 추가, 인덱싱과 get의 실패 정책, 닫힌 타입 집합과 소유권 종료를 작업별 선택 기준으로 정리한 다이어그램

CREATE · ACCESS · TYPE · DROP

Vec<T>는 작업보다 실패와 소유권 정책을 먼저 고른다

인덱싱이 위험하고 get이 무조건 안전한 것이 아닙니다. 범위 위반을 프로그램 불변식의 실패로 볼지, 정상적으로 처리할 부재로 볼지가 접근 API를 결정합니다.

get이 범위를 판정해 반환하는 두 경로길이5벡터에서usize인덱스i가범위안이면Some요소참조,밖이면None.2는값3,100은없다.인덱싱[]실패정책은원래보조설명과대조한다.예아니요v.get(i)길이 5 · i: usizei < len범위 안인가?Some(&요소)i=2이면 값 3Nonei=100이면 부재
get이 범위를 판정해 반환하는 두 경로길이5벡터에서usize인덱스i가범위안이면Some요소참조,밖이면None.2는값3,100은없다.인덱싱[]실패정책은원래보조설명과대조한다.예아니요v.get(i)길이 5 · i: usizei < len범위 안인가?Some(&요소)i=2이면 값 3Nonei=100이면 부재

보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.

작업별 선택 계약

생성·접근·타입·수명을 실패 정책과 함께 고른다

  1. 생성 · 추론할 근거를 둔다

    let v: Vec<i32> = Vec::new();처럼 다른 타입 제약이 없을 때 표기합니다. vec!로 만든 값 목록과 이후 push는 추론 근거가 됩니다. 용량 0과 ZST는 요소 버퍼를 할당하지 않습니다.

  2. 인덱싱 · 불변식 위반은 패닉

    &v[i]는 범위 안이라는 프로그램 계약을 짧게 표현하며, 위반하면 현재 스레드에서 패닉합니다.

  3. get · 정상적인 부재는 값으로

    v.get(i)는 Option<&T>를 돌려 Some과 None을 호출자가 분기하게 합니다.

  4. 종류의 경계 · 닫힌 집합과 열린 구현

    Box<dyn Trait>을 Vec에 담는 트레이트 객체 경계는 구현 종류가 열려야 할 때 검토합니다. 컴파일 시점에 아는 닫힌 종류는 SpreadsheetCell 같은 enum으로 묶습니다.

  5. 길이와 수명 · 재배치와 소유 값

    push는 가변 벡터 끝에 값을 추가하고, 용량이 부족하면 새 요소 버퍼를 할당해 기존 요소를 옮길 수 있습니다. pop은 마지막 요소를 제거해 그 소유 값을 Option<T>로 반환합니다. 벡터가 드롭되면 남은 소유 요소도 함께 드롭되고 할당한 버퍼가 해제됩니다.

판단 질문: 없는 인덱스가 버그입니까, 입력의 정상적인 부재입니까? 그 답이 &v[i]와 get을 가릅니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
관계·조건 설명길이 5인 벡터에서 usize 인덱스 i가 범위 안이면 Some 요소 참조, 밖이면 None이다. 2는 값 3을 가리키고, 100에 해당하는 요소는 없다. 인덱싱 []의 실패 정책은 원래 보조 설명과 대조한다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
조회 입력

길이 5 Vec에 index 2를 요청한다.

  • len: 5
  • index: 2
  • get의 범위 판정은 요청마다 적용된다.
성공 반환

2<5이므로 값 3을 빌려 반환한다.

  • 조건: true
  • 반환: Some(&3)
  • 값을 복제하거나 Vec의 소유를 넘긴 것이 아니다.
별도 부재 사례

index 100이면 None이다.

  • index: 100
  • 반환: None
  • get의 정상 부재와 []의 범위 밖 panic 정책을 구분한다.

길이가 5인 벡터에 인덱스 2를 요청하면 2 < 5이므로 값 3의 참조를 Some에 담아 돌려주고, 인덱스 100을 요청하면 범위 밖이므로 None을 돌려줍니다. 같은 인덱스 100을 []로 쓰면 어떻게 되는지 봅니다.

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

    let does_not_exist = &v[100];
    let does_not_exist = v.get(100);

첫 번째 []는 없는 원소를 가리키려 하므로, 이 줄에서 index out of bounds: the len is 5 but the index is 100 패닉이 나서 프로그램이 멈추고 그다음 get까지 가지 않습니다. 벡터의 끝을 넘는 접근이 있으면 안 되는 상황이어서, 그런 일이 생기면 프로그램을 멈춰 드러내고 싶을 때 이 방법을 씁니다. 반면 get은 범위 밖의 인덱스에 패닉 없이 None을 돌려줍니다. 정상적인 상황에서도 범위 밖의 인덱스가 들어올 수 있다면 이 방법을 쓰며, 그때는 Some(&element)와 None을 모두 처리하는 코드를 둡니다. 예를 들어 사용자가 번호를 입력하는데 너무 큰 숫자를 넣었다면, 패닉으로 프로그램을 끝내기보다 None을 받아 지금 벡터에 원소가 몇 개 있는지 알려 주고 다시 입력하게 하는 편이 사용자에게 친절합니다.

벡터 원소의 참조를 가진 동안에는 대여 검사기(borrow checker)가 참조와 대여의 규칙을 적용해, 그 참조와 벡터의 다른 참조가 계속 유효하도록 지킵니다. 같은 스코프에서 가변 참조와 불변 참조를 함께 쓸 수 없다는 규칙을 떠올려 봅니다. 다음 코드는 벡터의 첫 원소를 불변 참조로 가진 채 벡터 끝에 원소를 더하고, 그 뒤에 첫 원소를 다시 쓰려 하므로 컴파일되지 않습니다.

src/main.rs (컴파일되지 않음)
fn main() {
    let mut v = vec![1, 2, 3, 4, 5];

    let first = &v[0];

    v.push(6);

    println!("The first element is: {}", first);
}
error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:5
  |
4 |     let first = &v[0];
  |                  - immutable borrow occurs here
5 |
6 |     v.push(6);
  |     ^^^^^^^^^ mutable borrow occurs here
7 |
8 |     println!("The first element is: {}", first);
  |                                          ----- immutable borrow later used here

첫 원소의 참조가 벡터 끝의 변화와 무슨 상관인지 의아할 수 있습니다. 그러나 벡터는 원소를 메모리에 나란히 저장하므로, 끝에 원소를 더할 때 지금 자리에 공간이 없으면 더 큰 메모리를 새로 할당하고 기존 원소를 그리로 옮깁니다. 그러면 첫 원소의 참조는 해제된 메모리를 가리키게 되므로, 대여 규칙이 이런 상황을 막습니다. 다만 컴파일러가 실제로 검사하는 것은 메모리가 옮겨지는지가 아니라 불변 대여와 가변 대여가 겹치는지이므로, 용량을 미리 넉넉히 잡아 두어도 push 뒤에 first를 다시 쓰면 똑같이 거부됩니다.

Rust Vec 대여와 구조 변경 경계

first=&v[0] 생성→마지막 읽기→push(6)가 허용되는 실제 접근 상태 변화다. push 뒤 first 재사용의 거부 사본은 컴파일 검사로 분리하고 용량이 충분해도 충돌이 남는다는 조건을 보존한다.

Vec 요소 참조와 반복 대여가 살아 있는 동안 구조 변경이 금지되는 이유와 안전한 수정 순서를 정리한 상태표

BORROW STATE · ITERATION · REALLOCATION

요소를 빌린 동안에는 값을 읽거나 고치고, 구조는 대여가 끝난 뒤 바꾼다

직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여를 겹치지 않는 것입니다. push가 용량 부족 때 요소 주소를 옮길 수 있다는 사실은 왜 그 규칙이 필요한지를 보여 줍니다.

대여 사용 구간과 소스 문장의 순서두칼럼은실행시간표가아니라허용또는거부할소스순서다.왼쪽은참조마지막사용후push.오른쪽은push뒤참조재사용으로대여구간겹침.테두리는공유참조사용구간이다.용량충분해도오른쪽소스는거부된다.허용할 소스 순서first = &v[0]공유 대여 시작first 마지막 사용println으로 먼저 읽음push(6)대여 종료 뒤 구조 변경거부할 소스 순서first = &v[0]공유 대여 시작push(6)독점 대여와 겹침first 마지막 사용이 뒤까지 공유 대여
대여 사용 구간과 소스 문장의 순서두칼럼은실행시간표가아니라허용또는거부할소스순서다.왼쪽은참조마지막사용후push.오른쪽은push뒤참조재사용으로대여구간겹침.테두리는공유참조사용구간이다.용량충분해도오른쪽소스는거부된다.허용할 소스 순서first = &v[0]공유 대여 시작first 마지막 사용println으로 먼저 읽음push(6)대여 종료 뒤 구조 변경거부할 소스 순서first = &v[0]공유 대여 시작push(6)독점 대여와 겹침first 마지막 사용이 뒤까지 공유 대여

보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.

겹치는 사용 구간: 컴파일 오류

let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");

거부 실제로 재할당되는지와 무관하게, 살아 있는 요소 참조와 벡터의 독점 변경이 겹칩니다.

마지막 사용 뒤: 구조 변경 가능

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);

허용 선언된 블록 전체가 아니라 참조의 마지막 사용까지가 핵심입니다.

대여 상태별 허용 작업
현재 상태허용되는 작업구조 변경 경계
요소 대여 없음읽기와 함께 push, pop, insert, remove 같은 독점 변경을 시작할 수 있습니다.변경이 새 대여를 만들기 전까지 가능합니다.
공유 반복
for item in &v {
    println!("{item}");
}
반복자가 벡터를 공유 대여하므로 루프 안에서 길이를 바꿀 수 없습니다.
가변 반복
for item in &mut v {
    *item += 50;
}
현재 요소의 값은 바꿀 수 있지만 반복 중 벡터 구조를 다시 빌릴 수 없습니다.
마지막 사용 이후공유·가변 반복 또는 요소 참조의 사용이 끝났습니다.그 뒤에 구조 변경을 배치합니다.

충돌을 푸는 네 가지 방법

  1. 먼저 사용

    요소 참조의 마지막 사용을 push 앞으로 옮깁니다.

  2. 범위를 닫기

    별도 블록에서 읽고 블록이 끝난 뒤 벡터를 변경합니다.

  3. 소유 값을 보관

    작은 Copy 값은 복사하고, 필요하다면 의미를 검토한 뒤 명시적으로 복제합니다.

  4. 변경 뒤 재조회

    append-only push처럼 기존 위치 의미가 유지될 때 인덱스를 보관하고 변경 뒤 다시 빌립니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 다시 확인합니다.

겹치는 사용 구간 · 컴파일 오류

let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");

이후에 쓸 요소 참조와 push의 독점 대여가 겹칩니다.

마지막 사용 뒤 · 구조 변경 가능

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);

먼저 참조를 사용한 뒤 벡터를 변경합니다.

  1. 공유 대여

    for item in &v

    &v[i]나 공유 반복이 살아 있는 동안 읽을 수 있지만 길이 변경은 시작할 수 없습니다.

  2. 가변 반복

    for item in &mut v {
        *item += 50;
    }

    현재 요소는 고칠 수 있지만 push나 pop으로 구조를 바꿀 수 없습니다.

  3. 마지막 사용

    참조의 선언 위치나 블록 전체가 아니라 실제 마지막 사용 뒤에 독점 변경을 배치합니다.

  4. 재할당 근거

    용량 부족 시 push가 요소 버퍼를 옮길 수 있지만, 컴파일러가 직접 적용하는 규칙은 겹치는 공유·독점 대여 금지입니다.

  5. 교정 전략

    참조를 먼저 사용하거나 범위를 닫고, 소유 값을 보관하거나 의미가 유지되는 인덱스로 변경 뒤 다시 조회합니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 확인합니다.

순서: 요소 대여의 마지막 사용 → 반복 종료 → push·pop 같은 구조 변경.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여가 겹치지 않게 하는 것입니다.
관계·조건 설명두 칼럼은 실행 시간표가 아니라 허용 또는 거부할 소스 순서다. 왼쪽은 참조의 마지막 사용 후 push한다. 오른쪽은 push 뒤 참조를 재사용하여 대여 구간이 겹친다. 테두리는 공유 참조의 사용 구간이다. 용량이 충분해도 오른쪽 소스는 거부된다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요소 읽기

first가 v[0]의 요소를 공유 대여한다.

  • first: &v[0]
  • 대여: 공유 활성
  • 같은 Vec의 독점 구조 변경과 겹치지 않아야 한다.
마지막 사용

println으로 first를 마지막으로 읽는다.

  • first: 추가 사용 없음
  • 대여: 변경 전 종료 가능
  • 참조 이름의 블록이 남아 있어도 필요한 접근은 끝날 수 있다.
push 허용

마지막 읽기 뒤 push(6)으로 Vec을 변경한다.

  • 갱신: push(6)
  • 조건: 겹치는 공유 접근 없음
  • push 뒤 first 재사용 사본은 용량과 관계없이 별도 컴파일 거부다.

Rust Vec 대여와 구조 변경 경계

first=&v[0] 생성→마지막 읽기→push(6)가 허용되는 실제 접근 상태 변화다. push 뒤 first 재사용의 거부 사본은 컴파일 검사로 분리하고 용량이 충분해도 충돌이 남는다는 조건을 보존한다.

Vec 요소 참조와 반복 대여가 살아 있는 동안 구조 변경이 금지되는 이유와 안전한 수정 순서를 정리한 상태표

BORROW STATE · ITERATION · REALLOCATION

요소를 빌린 동안에는 값을 읽거나 고치고, 구조는 대여가 끝난 뒤 바꾼다

직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여를 겹치지 않는 것입니다. push가 용량 부족 때 요소 주소를 옮길 수 있다는 사실은 왜 그 규칙이 필요한지를 보여 줍니다.

대여 사용 구간과 소스 문장의 순서두칼럼은실행시간표가아니라허용또는거부할소스순서다.왼쪽은참조마지막사용후push.오른쪽은push뒤참조재사용으로대여구간겹침.테두리는공유참조사용구간이다.용량충분해도오른쪽소스는거부된다.허용할 소스 순서first = &v[0]공유 대여 시작first 마지막 사용println으로 먼저 읽음push(6)대여 종료 뒤 구조 변경거부할 소스 순서first = &v[0]공유 대여 시작push(6)독점 대여와 겹침first 마지막 사용이 뒤까지 공유 대여
대여 사용 구간과 소스 문장의 순서두칼럼은실행시간표가아니라허용또는거부할소스순서다.왼쪽은참조마지막사용후push.오른쪽은push뒤참조재사용으로대여구간겹침.테두리는공유참조사용구간이다.용량충분해도오른쪽소스는거부된다.허용할 소스 순서first = &v[0]공유 대여 시작first 마지막 사용println으로 먼저 읽음push(6)대여 종료 뒤 구조 변경거부할 소스 순서first = &v[0]공유 대여 시작push(6)독점 대여와 겹침first 마지막 사용이 뒤까지 공유 대여

보조 설명 · 아래 원래 예제와 비교 조건도 함께 확인합니다.

겹치는 사용 구간: 컴파일 오류

let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");

거부 실제로 재할당되는지와 무관하게, 살아 있는 요소 참조와 벡터의 독점 변경이 겹칩니다.

마지막 사용 뒤: 구조 변경 가능

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);

허용 선언된 블록 전체가 아니라 참조의 마지막 사용까지가 핵심입니다.

대여 상태별 허용 작업
현재 상태허용되는 작업구조 변경 경계
요소 대여 없음읽기와 함께 push, pop, insert, remove 같은 독점 변경을 시작할 수 있습니다.변경이 새 대여를 만들기 전까지 가능합니다.
공유 반복
for item in &v {
    println!("{item}");
}
반복자가 벡터를 공유 대여하므로 루프 안에서 길이를 바꿀 수 없습니다.
가변 반복
for item in &mut v {
    *item += 50;
}
현재 요소의 값은 바꿀 수 있지만 반복 중 벡터 구조를 다시 빌릴 수 없습니다.
마지막 사용 이후공유·가변 반복 또는 요소 참조의 사용이 끝났습니다.그 뒤에 구조 변경을 배치합니다.

충돌을 푸는 네 가지 방법

  1. 먼저 사용

    요소 참조의 마지막 사용을 push 앞으로 옮깁니다.

  2. 범위를 닫기

    별도 블록에서 읽고 블록이 끝난 뒤 벡터를 변경합니다.

  3. 소유 값을 보관

    작은 Copy 값은 복사하고, 필요하다면 의미를 검토한 뒤 명시적으로 복제합니다.

  4. 변경 뒤 재조회

    append-only push처럼 기존 위치 의미가 유지될 때 인덱스를 보관하고 변경 뒤 다시 빌립니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 다시 확인합니다.

겹치는 사용 구간 · 컴파일 오류

let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(6);
println!("{first}");

이후에 쓸 요소 참조와 push의 독점 대여가 겹칩니다.

마지막 사용 뒤 · 구조 변경 가능

let mut v = vec![1, 2, 3];
let first = &v[0];
println!("{first}");
v.push(6);

먼저 참조를 사용한 뒤 벡터를 변경합니다.

  1. 공유 대여

    for item in &v

    &v[i]나 공유 반복이 살아 있는 동안 읽을 수 있지만 길이 변경은 시작할 수 없습니다.

  2. 가변 반복

    for item in &mut v {
        *item += 50;
    }

    현재 요소는 고칠 수 있지만 push나 pop으로 구조를 바꿀 수 없습니다.

  3. 마지막 사용

    참조의 선언 위치나 블록 전체가 아니라 실제 마지막 사용 뒤에 독점 변경을 배치합니다.

  4. 재할당 근거

    용량 부족 시 push가 요소 버퍼를 옮길 수 있지만, 컴파일러가 직접 적용하는 규칙은 겹치는 공유·독점 대여 금지입니다.

  5. 교정 전략

    참조를 먼저 사용하거나 범위를 닫고, 소유 값을 보관하거나 의미가 유지되는 인덱스로 변경 뒤 다시 조회합니다. 삽입·삭제·정렬 뒤에는 같은 논리 요소인지 확인합니다.

순서: 요소 대여의 마지막 사용 → 반복 종료 → push·pop 같은 구조 변경.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
본문·주석직접 규칙은 같은 벡터 저장소에 대한 살아 있는 공유 대여와 독점 대여가 겹치지 않게 하는 것입니다.
관계·조건 설명두 칼럼은 실행 시간표가 아니라 허용 또는 거부할 소스 순서다. 왼쪽은 참조의 마지막 사용 후 push한다. 오른쪽은 push 뒤 참조를 재사용하여 대여 구간이 겹친다. 테두리는 공유 참조의 사용 구간이다. 용량이 충분해도 오른쪽 소스는 거부된다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
요소 읽기

first가 v[0]의 요소를 공유 대여한다.

  • first: &v[0]
  • 대여: 공유 활성
  • 같은 Vec의 독점 구조 변경과 겹치지 않아야 한다.
마지막 사용

println으로 first를 마지막으로 읽는다.

  • first: 추가 사용 없음
  • 대여: 변경 전 종료 가능
  • 참조 이름의 블록이 남아 있어도 필요한 접근은 끝날 수 있다.
push 허용

마지막 읽기 뒤 push(6)으로 Vec을 변경한다.

  • 갱신: push(6)
  • 조건: 겹치는 공유 접근 없음
  • push 뒤 first 재사용 사본은 용량과 관계없이 별도 컴파일 거부다.

그림처럼 first가 v[0]을 빌리고, println!에서 first를 마지막으로 읽은 뒤에 push(6)으로 벡터를 바꾸면 대여가 겹치지 않아 허용됩니다. 원소를 빌린 동안에는 그 값을 읽거나 고치기만 하고, 벡터의 구조를 바꾸는 일은 대여가 끝난 뒤에 합니다. Vec<T>의 구현은 러스토노미콘(The Rustonomicon)에서 더 볼 수 있습니다.

벡터 값에 대해 반복하기

벡터의 원소를 차례로 다룰 때는 인덱스로 하나씩 꺼내기보다 모든 원소를 반복합니다. 다음 for는 i32 벡터의 각 원소를 불변 참조로 받아 출력합니다.

    let v = vec![100, 32, 57];
    for i in &v {
        println!("{i}");
    }
100
32
57

가변 벡터의 각 원소를 가변 참조로 받아 모든 원소를 고칠 수도 있습니다. 다음 for는 각 원소에 50을 더합니다.

    let mut v = vec![100, 32, 57];
    for i in &mut v {
        *i += 50;
    }

가변 참조가 가리키는 값을 고치려면 += 앞에 역참조 연산자 *로 i가 가리키는 값을 꺼내야 합니다. 역참조 연산자는 Deref와 역참조 강제 변환에서 자세히 다룹니다. 벡터를 반복하는 것은 불변이든 가변이든 대여 규칙 덕분에 안전합니다. 위의 두 for 본문에서 원소를 더하거나 지우려 하면 앞의 예제와 비슷한 컴파일 오류가 납니다. for가 가진 벡터의 참조가 반복하는 동안 벡터 전체가 바뀌는 것을 막기 때문입니다.

열거형을 이용해 여러 타입 저장하기

벡터는 같은 타입의 값만 담을 수 있어서, 서로 다른 타입의 항목을 목록에 담아야 할 때는 불편합니다. 다행히 열거형의 배리언트는 모두 같은 열거형 타입이므로, 서로 다른 타입의 값을 담아야 한다면 열거형을 정의해 쓸 수 있습니다. 예를 들어 스프레드시트의 한 행에서 값을 가져오는데, 어떤 열은 정수, 어떤 열은 실수, 어떤 열은 문자열이라고 합시다. 배리언트마다 다른 타입의 값을 담는 열거형을 정의하면, 모든 배리언트가 같은 타입으로 취급되므로 그 열거형을 담는 벡터를 만들 수 있습니다.

    enum SpreadsheetCell {
        Int(i32),
        Float(f64),
        Text(String),
    }

    let row = vec![
        SpreadsheetCell::Int(3),
        SpreadsheetCell::Text(String::from("blue")),
        SpreadsheetCell::Float(10.12),
    ];

러스트는 벡터에 담을 타입을 컴파일할 때 알아야 원소 하나에 힙 메모리가 얼마나 필요한지 정확히 알 수 있습니다. 또 이 벡터에 어떤 타입이 들어갈 수 있는지를 분명히 해야 합니다. 어떤 타입이든 담는 벡터를 허용한다면, 원소마다 하는 연산이 어떤 타입에서는 오류를 낼 수 있습니다. 열거형과 match를 쓰면, 5장에서 본 것처럼 가능한 모든 경우를 처리했는지 컴파일할 때 확인됩니다.

프로그램이 실행 중에 벡터에 담을 타입을 모두 알 수 없다면 열거형은 쓸 수 없으며, 그때는 트레이트 객체와 다형성에서 볼 트레이트 객체(trait object)를 씁니다. 지금까지 벡터를 쓰는 가장 흔한 방법을 보았으며, 표준 라이브러리의 Vec에는 쓸모 있는 메서드가 많으니 API 문서를 살펴보세요. 예를 들어 push와 함께 pop은 마지막 원소를 지우고 돌려줍니다.

벡터가 버려지면 벡터의 요소도 버려집니다

다른 struct처럼 벡터도 스코프를 벗어나면 해제됩니다. 아래 코드는 중괄호로 새 스코프를 만들어 그 안에서 벡터를 만들고, 닫는 중괄호에서 벡터가 해제된다는 것을 주석으로 표시합니다.

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

        // v로 할 일을 함
    } // <- v가 스코프를 벗어나 해제됨

벡터가 버려지면 그 안의 원소도 모두 버려지므로, 벡터가 담고 있던 정수들의 메모리도 정리됩니다. 대여 검사기는 벡터의 원소를 가리키는 참조가 벡터가 유효한 동안에만 쓰이는지 확인합니다.

이제 원소를 읽고, 구조를 바꾸고, 원소를 고치는 일을 차례로 하는 프로그램을 봅니다.

src/main.rs
fn main() {
    let mut values = vec![100, 32, 57];
    let saved = values[0];

    let first = &values[0];
    println!("first: {first}");

    values.push(6);
    for value in &mut values {
        *value += 50;
    }

    let removed = values.pop();
    println!("values: {values:?}; removed: {removed:?}");
    println!("saved: {saved}; index3: {:?}", values.get(3));
}
first: 100
values: [150, 82, 107]; removed: Some(56)
saved: 100; index3: None

saved는 values[0]의 i32를 복사한 값이므로 벡터와 관계없이 100으로 남습니다. first는 첫 println!에서 마지막으로 쓰이므로 그 뒤의 push와 겹치지 않습니다. push 뒤 목록은 100, 32, 57, 6이고 각 원소에 50을 더하면 150, 82, 107, 56이며, pop이 마지막 56을 Some(56)으로 돌려주고 목록에서 지웁니다. 남은 원소는 세 개이므로 get(3)은 None입니다. 벡터의 길이와 용량은 서로 다른 값이며, 용량이 얼마씩 늘어나는지는 Vec의 보장 사항에 따라 정해져 있지 않으므로 기대하지 않습니다.

더 깊이: 용량은 언제 늘고 언제 줄까

용량이 얼마씩 늘어나는지는 정해져 있지 않지만, Vec의 보장 사항에는 기대어도 되는 규칙이 여럿 있습니다. Vec::new, vec![], Vec::with_capacity(0)처럼 용량이 0인 벡터는 메모리를 할당하지 않으며, 크기가 0인 타입의 원소만 담는 벡터도 할당하지 않습니다. push와 insert는 보고된 용량이 충분하면 결코 다시 할당하지 않고, 길이가 용량과 같을 때만 다시 할당합니다. 성장 전략이 무엇이든 push는 상각(amortized) O(1)이 되도록 보장되며, 이는 가끔 일어나는 재할당의 비용을 여러 번의 push에 나누어 보면 한 번에 상수 시간이라는 뜻입니다. 두 배씩 늘리는 배열로 이 계산을 따라가는 과정은 용량이 늘어나는 동적 배열 만들기에 있으며, 그 글에서는 상각을 분할 상환이라고 부릅니다.

벡터는 저절로 줄어들지 않습니다. 원소를 모두 지워도 용량은 그대로이므로, 비운 벡터를 같은 길이까지 다시 채울 때는 할당자를 부르지 않습니다. 쓰지 않는 메모리를 돌려주고 싶다면 shrink_to_fit이나 shrink_to를 직접 부릅니다. 또 vec![x; n], 원소를 나열한 vec![...], Vec::with_capacity(n)은 원소 n개에 꼭 맞는 크기를 할당자에게 요청하므로, 넣을 원소의 수를 미리 안다면 with_capacity로 중간의 재할당을 피할 수 있습니다. 다만 할당자는 요청보다 큰 메모리를 돌려줄 수 있으므로 capacity()가 n보다 클 수도 있습니다.

정리

  • 벡터 Vec<T>는 같은 타입의 값을 메모리에 나란히 담으며, Vec::new나 vec!으로 만들고 push로 원소를 더합니다.
  • &v[i]는 범위를 벗어나면 패닉을 내고, v.get(i)는 Option<&T>를 돌려주어 범위 밖이면 None입니다.
  • 원소의 참조를 가진 동안에는 벡터를 바꿀 수 없으며, 참조를 마지막으로 쓴 뒤에는 바꿀 수 있습니다.
  • for x in &v는 원소를 불변 참조로, for x in &mut v는 가변 참조로 반복하며, 원소를 고칠 때는 *로 역참조합니다.
  • 서로 다른 타입의 값을 담으려면 배리언트마다 타입이 다른 열거형을 원소로 씁니다.
  • 벡터가 버려지면 그 안의 원소도 모두 버려집니다.

연습 문제

1. 목록을 따라가기

새 벡터를 [2, 4]로 만들고 첫 정수를 saved에 복사한 뒤, 모든 원소에 3을 곱하고, 8을 push한 다음 한 번 pop합니다. 마지막 목록, saved, pop이 돌려준 값, get(2)의 결과는 무엇인가요?

해설 보기

3을 곱하면 [6, 12], 8을 더하면 [6, 12, 8]이고, pop이 Some(8)을 돌려주며 목록은 [6, 12]가 됩니다. saved는 처음에 복사한 2이고, 원소가 두 개이므로 get(2)는 None입니다. 원소에 곱하는 for는 for x in &mut v { *x *= 3; }처럼 가변 참조로 반복합니다.

2. 사용자가 고른 번호

사용자가 입력한 번호로 벡터의 원소를 보여 주는 프로그램에서, 번호가 범위를 벗어나면 프로그램을 끝내지 말고 원소의 개수를 알려 주고 싶습니다. &v[i]와 v.get(i) 가운데 무엇을 써야 하나요?

해설 보기

v.get(i)를 씁니다. 사용자의 입력은 범위를 벗어날 수 있는 정상적인 경우이므로, get이 돌려준 Option을 match로 다뤄 Some(x)이면 원소를 보여 주고 None이면 v.len()으로 원소의 개수를 알려 줍니다. &v[i]는 범위를 벗어나면 패닉으로 프로그램이 끝나므로, 범위를 벗어나면 안 되는 상황에 씁니다.

3. 반복하면서 더하기

다음 코드는 컴파일되나요? 되지 않는다면 어떻게 고치나요?

let mut v = vec![1, 2, 3];
for x in &v {
    if *x == 2 {
        v.push(4);
    }
}
해설 보기

컴파일되지 않습니다. for가 반복하는 동안 &v로 벡터를 불변으로 빌리고 있는데, 본문의 v.push(4)가 같은 벡터를 가변으로 빌리려 하므로 E0502 오류가 납니다. 반복이 끝난 뒤에 더하도록, 먼저 let has_two = v.contains(&2);처럼 조건을 구하고 반복이 끝난 다음 if has_two { v.push(4); }로 바꿉니다.

다음 글 UTF-8 문자열에서는 String을 만들고 고치는 방법과, 러스트의 문자열이 왜 인덱스로 글자를 꺼낼 수 없는지 살펴봅니다.