Rust 빌림 에러 총정리: value moved, cannot borrow, does not live long enough

5 분 소요

Rust를 배우는 초반의 체감 난이도는 대부분 빌림 검사기(borrow checker)와의 실랑이에서 옵니다. 그런데 실제로 만나는 에러의 종류는 손에 꼽을 만큼 적고, 각각의 처방도 정형화되어 있습니다. 이 글은 그 대표 에러들을 코드(E 번호)별로 정리한 해결 모음입니다. 원리를 처음 배우는 분은 소유권빌림 편을 먼저 읽고 오시는 것을 권합니다.

E0382: value borrowed here after move — 이동한 값을 썼다 #

src/main.rs
let name = String::from("보고서");
let title = name;
println!("{name}"); // error[E0382]: borrow of moved value: `name`

힙을 소유한 값의 대입·함수 전달은 이동이고, 원래 변수는 무효가 됩니다. 처방은 상황별로 셋입니다.

  • 읽기만 필요했다면: 이동 대신 참조를 넘깁니다. let title = &name; 또는 함수 매개변수를 &str로.
  • 정말 두 벌이 필요하다면: let title = name.clone();으로 복사 비용을 명시합니다.
  • 함수가 소유권을 가져갔다면: 매개변수를 참조로 바꾸는 것이 우선이고, 안 되면 반환으로 돌려받습니다.

반복문에서 for x in vec처럼 컬렉션을 통째로 이동시킨 뒤 다시 쓰는 실수도 같은 에러입니다. for x in &vec으로 빌려서 순회하면 됩니다.

E0502: 불변 빌림과 가변 빌림이 겹쳤다 #

src/main.rs
let first = &items[0];            // 불변 빌림 시작
items.push(99);                   // error[E0502]: cannot borrow `items` as mutable
println!("{first}");              // 불변 빌림이 여기까지 살아 있음

“읽는 사람이 있는 동안 수정 불가"라는 규칙 그대로입니다. 핵심 지식은 빌림은 마지막 사용 지점까지만 살아 있다는 것입니다. 그래서 처방은 순서 재배치가 첫 번째입니다.

src/main.rs
let first = items[0];  // 참조 대신 값 복사 (Copy 타입일 때)
items.push(99);        // 문제없음

읽기를 먼저 끝내거나(마지막 사용을 수정 앞으로), 참조 대신 값을 복사해 빌림 자체를 없애는 방향입니다. 순회 중 수정이라면 “수정할 것을 모아 뒀다 순회 후 반영"이 정석입니다.

E0499: 가변 빌림이 두 개다 #

src/main.rs
let a = &mut scores[0];
let b = &mut scores[1]; // error[E0499]: cannot borrow `scores` as mutable more than once

두 요소가 서로 다른 칸이어도, 컴파일러 입장에서는 같은 컬렉션에 대한 가변 빌림 두 개입니다. 인덱스가 겹치지 않음을 증명할 수 없기 때문입니다. 표준 라이브러리가 이 경우를 위한 안전한 칼을 제공합니다.

src/main.rs
let (left, right) = scores.split_at_mut(1);
let a = &mut left[0];
let b = &mut right[0]; // 서로 겹치지 않는 두 조각

두 요소를 바꾸는 것이 목적이면 scores.swap(0, 1)처럼 목적에 맞는 메서드가 이미 있는 경우가 많습니다.

E0506: 빌려 간 값에 대입했다 #

src/main.rs
let entry = &user.name;
user.name = String::from("새 이름"); // error[E0506]: cannot assign to `user.name`
println!("{entry}");

참조가 살아 있는 값을 통째로 갈아 끼우려는 경우입니다. E0502와 같은 원리이고 처방도 같습니다. 참조의 마지막 사용을 대입 앞으로 옮기거나, 참조 대신 필요한 부분을 복사해 둡니다.

E0597: borrowed value does not live long enough — 값이 참조보다 먼저 죽는다 #

src/main.rs
let best;
{
    let scores = vec![90, 85];
    best = &scores[0];
} // error[E0597]: `scores` does not live long enough
println!("{best}");

참조가 가리키는 값이 스코프를 벗어나 해제되는데 참조는 더 오래 쓰이려는 경우입니다. 함수에서 지역 변수의 참조를 반환하는 고전 사례가 같은 계열(E0106과 함께)입니다. 처방은 방향 전환입니다. 참조를 오래 살리려 하지 말고, 소유권을 밖으로 옮깁니다. 위라면 best = scores[0];(Copy) 또는 let scores를 바깥 스코프로 올리는 것, 함수라면 &String이 아니라 String을 반환하는 것입니다. 수명 표기('a)는 값을 오래 살리는 도구가 아니라 관계의 선언일 뿐이라는 것을 기억하면(기초 9편), 표기를 추가하는 쪽이 아니라 소유권을 옮기는 쪽이 답인 경우를 구분할 수 있습니다.

E0596: 클로저가 캡처한 변수를 수정하려 했다 #

src/main.rs
let mut count = 0;
let inc = || count += 1; // 클로저 선언 자체는 통과
some_parallel_api(inc);  // error[E0596]: captured variable in a `Fn` closure

순차 코드라면 클로저를 let mut inc = move || ...로 바꾸거나 FnMut를 받는 API를 쓰면 풀립니다. 그런데 병렬 API(rayon 등)에서 이 에러가 났다면 이야기가 다릅니다. 여러 스레드가 같은 변수를 동시에 수정하려는 설계, 즉 데이터 레이스를 컴파일러가 막은 것입니다. 이때의 처방은 클로저 수선이 아니라 구조 변경입니다. 실전 5편에서 다뤘듯 스레드별 부분 결과를 만들어 병합(fold·reduce)하는 것이 정석입니다.

공통 요령 #

  • help 줄부터 읽습니다. Rust 에러의 절반은 처방전을 동봉하고 있고, rustc --explain E0382가 상세 해설을 보여 줍니다.
  • 에러를 지우는 최소 수단을 고릅니다. 참조로 충분한 곳에 clone을 쓰면 동작은 하지만 비용이 남고, clone이면 충분한 곳에 수명 표기를 도입하면 복잡도가 남습니다. 참조 → 순서 재배치 → 복사(clone) → 구조 변경 순서로 가볍게부터 시도하는 것을 권합니다.
  • 빌림 검사기는 목록의 마지막이 아니라 처음에 만나는 리뷰어입니다. 여기 나온 에러 대부분은 다른 언어라면 런타임 버그(무효 참조, 반복 중 수정, 데이터 레이스)로 늦게 나타났을 코드입니다.

정리 #

  • 이동 후 사용(E0382)은 참조·clone·반환 중 상황에 맞는 최소 수단으로 풉니다. 순회는 &vec이 기본입니다.
  • 빌림 충돌(E0502·E0506)은 “빌림은 마지막 사용까지"를 이용해 읽기를 앞으로, 수정을 뒤로 재배치하는 것이 첫 처방입니다.
  • 같은 컬렉션의 가변 빌림 두 개(E0499)는 split_at_mut·swap 같은 표준 라이브러리의 안전한 분할 도구가 답입니다.
  • 수명 부족(E0597)은 참조를 살리는 방향이 아니라 소유권을 옮기는 방향으로 풉니다.
  • 병렬 클로저의 E0596은 버그 신고가 아니라 데이터 레이스 차단입니다. fold·reduce 구조로 바꿉니다.
X