Rust 빌림 에러 총정리: value moved, cannot borrow, does not live long enough
Rust를 배우는 초반의 체감 난이도는 대부분 빌림 검사기(borrow checker)와의 실랑이에서 옵니다. 그런데 실제로 만나는 에러의 종류는 손에 꼽을 만큼 적고, 각각의 처방도 정형화되어 있습니다. 이 글은 그 대표 에러들을 코드(E 번호)별로 정리한 해결 모음입니다. 원리를 처음 배우는 분은 소유권과 빌림 편을 먼저 읽고 오시는 것을 권합니다.
E0382: value borrowed here after move — 이동한 값을 썼다 #
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: 불변 빌림과 가변 빌림이 겹쳤다 #
let first = &items[0]; // 불변 빌림 시작
items.push(99); // error[E0502]: cannot borrow `items` as mutable
println!("{first}"); // 불변 빌림이 여기까지 살아 있음
“읽는 사람이 있는 동안 수정 불가"라는 규칙 그대로입니다. 핵심 지식은 빌림은 마지막 사용 지점까지만 살아 있다는 것입니다. 그래서 처방은 순서 재배치가 첫 번째입니다.
let first = items[0]; // 참조 대신 값 복사 (Copy 타입일 때)
items.push(99); // 문제없음
읽기를 먼저 끝내거나(마지막 사용을 수정 앞으로), 참조 대신 값을 복사해 빌림 자체를 없애는 방향입니다. 순회 중 수정이라면 “수정할 것을 모아 뒀다 순회 후 반영"이 정석입니다.
E0499: 가변 빌림이 두 개다 #
let a = &mut scores[0];
let b = &mut scores[1]; // error[E0499]: cannot borrow `scores` as mutable more than once
두 요소가 서로 다른 칸이어도, 컴파일러 입장에서는 같은 컬렉션에 대한 가변 빌림 두 개입니다. 인덱스가 겹치지 않음을 증명할 수 없기 때문입니다. 표준 라이브러리가 이 경우를 위한 안전한 칼을 제공합니다.
let (left, right) = scores.split_at_mut(1);
let a = &mut left[0];
let b = &mut right[0]; // 서로 겹치지 않는 두 조각
두 요소를 바꾸는 것이 목적이면 scores.swap(0, 1)처럼 목적에 맞는 메서드가 이미 있는 경우가 많습니다.
E0506: 빌려 간 값에 대입했다 #
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 — 값이 참조보다 먼저 죽는다 #
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: 클로저가 캡처한 변수를 수정하려 했다 #
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 구조로 바꿉니다.