콘텐츠로 이동

Rust 에러 E0502: cannot borrow as mutable because it is also borrowed as immutable

E0502는 코드의 다른 부분이 참조자로 값을 아직 읽고 있는데, 그 값을 바꾸려고 했다는 뜻이에요. 벡터를 다룰 때 정말 자주 나오고, 보통은 한 줄을 옮기거나 값 하나를 복사하면 해결돼요. 이 페이지에서는 고치는 방법을 먼저 보여 주고, 그다음에 Rust가 왜 이걸 고집하는지 설명할게요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0502]: cannot borrow `v` as mutable because it is also borrowed as immutable
--> src/main.rs:4:5
|
3 | let first = &v[0]; // Vec 안을 가리키는 참조자
| - immutable borrow occurs here
4 | v.push(4); // ❌ `first`가 살아 있는 동안 Vec을 바꿔요
| ^^^^^^^^^ mutable borrow occurs here
5 | println!("First: {first}"); // ...여기서 `first`를 쓰기 때문이에요
| ----- immutable borrow later used here

“v가 이미 불변으로 대여되어 있어서 가변으로 대여할 수 없다”는 뜻이에요. 라벨은 차례대로 “불변 대여가 여기서 일어남”, “가변 대여가 여기서 일어남”, “불변 대여가 나중에 여기서 쓰임”이라고 말해요.

이 에러를 내는 프로그램은 다음과 같아요.

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0]; // Vec 안을 가리키는 참조자
v.push(4); // ❌ `first`가 살아 있는 동안 Vec을 바꿔요
println!("First: {first}"); // ...여기서 `first`를 쓰기 때문이에요
}

컴파일러가 세 줄을 가리키는데, 에러를 이해하려면 세 줄이 모두 필요해요.

  1. 3번째 줄: &v[0]이 v 안을 가리키는 읽기 전용 참조자를 만들어요. 이게 불변 대여(immutable borrow)예요.
  2. 4번째 줄: v.push(4)는 v를 바꿔야 하니까 가변 대여(mutable borrow)를 해요.
  3. 5번째 줄: first를 다시 써요. 이 줄 때문에 첫 번째 대여가 4번째 줄을 지나서까지 살아 있게 돼요.

5번째 줄이 없다면 코드는 컴파일돼요. 문제는 두 대여가 존재한다는 게 아니라, 두 대여가 겹친다는 거예요.

규칙은 이거예요. 읽는 쪽은 여럿이어도 되고, 쓰는 쪽은 하나만. 둘이 동시에 있으면 안 돼요. 대여 페이지에서 배운 규칙이에요.

Vec에는 아주 구체적인 이유가 있어요. Vec은 요소들을 메모리의 한 블록에 담아 둬요. push를 했는데 블록이 꽉 차 있으면, Vec은 모든 요소를 더 큰 새 블록으로 옮기고 예전 블록을 해제해요. 그러면 예전 블록을 가리키던 참조자(first 같은)는 해제된 메모리를 가리키게 돼요. C++에서는 이게 그 유명한 “반복자 무효화”(iterator invalidation) 버그이고, 프로그램이 죽거나 조용히 쓰레기 값을 읽게 돼요.

친구 주소를 적어 뒀는데 친구가 더 큰 집으로 이사 간 것과 비슷해요. 메모는 멀쩡해 보여도, 가리키는 곳은 빈 건물이에요. Rust는 빈 건물에 찾아가게 두는 대신, 아예 컴파일을 거부해요.

가장 먼저 시도해 보세요. Rust 2018부터 대여는 닫는 중괄호가 아니라 마지막으로 쓰인 곳에서 끝나요. 이걸 논-렉시컬 라이프타임(non-lexical lifetimes)이라고 해요. 그러니 벡터를 바꾸기 전에 참조자를 다 쓰면 돼요.

fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
println!("First: {first}"); // `first`를 마지막으로 써요. 대여가 여기서 끝나요
v.push(4); // ✅ 이제 읽는 사람이 없어요
println!("Now: {:?}", v);
}

실행 결과:

First: 1
Now: [1, 2, 3, 4]
▶ 직접 실행해 보기

값을 바꾼 뒤에도 예전 값이 필요할 때 이 방법을 고르세요. 참조자 대신 복사본을 가져오는 거예요. 숫자라면 &만 빼면 되고, String이라면 .clone()을 쓰세요.

fn main() {
let mut queue = vec![String::from("deploy"), String::from("test")];
let next = queue[0].clone(); // 참조자가 아니라 나만의 복사본이에요
queue.push(String::from("release"));
println!("다음 작업: {next}");
println!("대기열: {:?}", queue);
}

실행 결과:

다음 작업: deploy
대기열: ["deploy", "test", "release"]
▶ 직접 실행해 보기

Vec<i32>에서 let first = v[0];이라고 쓰면 공짜로 복사본이 생겨요. i32는 Copy이기 때문이에요.

방법 3: iter_mut이나 retain으로 그 자리에서 바꾸기

섹션 제목: “방법 3: iter_mut이나 retain으로 그 자리에서 바꾸기”

for p in prices.iter() 같은 반복문을 쓰고 그 안에서 prices.remove(i)를 호출하거나 요소를 바꾸려고 했다면 이 방법을 고르세요. 이것도 E0502가 나요. 표준 라이브러리에 딱 이런 경우를 위한 메서드가 있어요.

fn main() {
let mut prices = vec![120, 45, 300, 80];
// 모든 요소를 그 자리에서 바꿔요
for p in prices.iter_mut() {
*p = *p * 9 / 10; // 10% 할인
}
// 필요 없는 요소를 지워요
prices.retain(|&p| p >= 50);
println!("{:?}", prices);
}

실행 결과:

[108, 270, 72]
▶ 직접 실행해 보기

iter_mut은 요소마다 &mut를 하나씩 줘서, 쓰는 쪽이 언제나 하나뿐이에요. retain은 지우는 일을 한 번에, 안전하게 대신 해 줘요. 벡터와 반복자를 참고하세요.

방법 4: 바꿀 내용을 모은 뒤 적용하기

섹션 제목: “방법 4: 바꿀 내용을 모은 뒤 적용하기”

읽은 내용을 바탕으로 요소를 추가해야 할 때 이 방법을 고르세요. 먼저 읽어서 모으고, 쓰기는 따로 한 번에 해요.

fn main() {
let mut scores = vec![10, 25, 7, 40];
let bonuses: Vec<i32> = scores.iter().filter(|&&s| s > 20).map(|s| s * 2).collect();
scores.extend(bonuses); // 읽기 대여가 끝났으니 쓸 수 있어요
println!("{:?}", scores);
}

실행 결과:

[10, 25, 7, 40, 50, 80]
▶ 직접 실행해 보기

Vec을 하나 더 만드니 메모리가 조금 더 들지만, 코드가 분명하고 누가 봐도 맞다는 걸 알 수 있어요. 보통은 괜찮은 거래예요.

다른 방법이 맞지 않을 때 마지막으로 쓰세요. 인덱스는 그냥 숫자일 뿐 대여가 아니라서, 값을 바꾸는 걸 막지 않아요.

fn main() {
let mut scores = vec![10, 25, 7];
for i in 0..scores.len() {
if scores[i] > 20 {
scores.push(scores[i] * 2);
}
}
println!("{:?}", scores);
}

실행 결과:

[10, 25, 7, 50]
▶ 직접 실행해 보기

0..scores.len()은 반복문이 시작되기 전에 한 번만 계산되기 때문에, 새로 추가된 요소는 돌지 않아요. 인덱스로 요소를 지울 때는 조심하세요. 위치가 밀려서 요소를 건너뛰거나 패닉이 날 수 있어요. 지울 때는 retain(방법 3)이 더 안전해요.

Vec을 돌면서 그 Vec에 push할 때. for s in &scores { scores.push(..) }는 반복문 내내 scores를 빌리고 있어요. 해결: 먼저 모은 뒤 나중에 extend하거나(방법 4), 인덱스로 도세요(방법 5).

필드를 돌면서 &mut self 메서드를 호출할 때. impl 블록 안에서 아주 흔해요.

fn audit(&mut self) {
for item in &self.items {
self.record(item); // ❌ cannot borrow `*self` as mutable because it is also borrowed as immutable
}
}

self.record는 self.items를 포함한 self 전체를 가변으로 빌려요. 해결: 다른 필드를 직접 건드리세요(self.log.push(item.clone())). Rust는 서로 다른 필드를 동시에 빌리는 건 허용하거든요. 아니면 항목들을 먼저 복제하세요. 구조체를 참고하세요.

HashMap에서 꺼낸 값을 들고 있으면서 삽입할 때. let apples = stock.get("apples").unwrap(); 다음에 stock.insert(..)를 하고 apples를 쓰면 E0502가 나요. 해결: let apples = *stock.get("apples").unwrap();이나 .copied()로 값을 복사해 오세요. HashMap을 참고하세요.

  • E0499: 가변 대여 두 개가 동시에 있어요. 같은 개념인데, 읽는 쪽과 쓰는 쪽이 아니라 쓰는 쪽이 둘이에요.
  • E0382: 값을 이동시킨 뒤에 또 썼어요. E0502를 대여 대신 이동으로 고치려다 자주 만나요.
  • E0106: 참조자를 반환하는 함수에 라이프타임 지정자가 빠졌어요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. let first = &v[0]; v.push(4); println!("{first}");는 왜 컴파일되지 않을까요?

2. Vec<i32>에서 50 미만인 요소를 모두 지우고 싶어요. E0502를 피하는 가장 깔끔한 방법은?

3. 요즘 Rust에서 불변 대여는 언제 끝날까요?

점수: 0 / 3