콘텐츠로 이동

Rust 에러 E0499: cannot borrow as mutable more than once at a time (가변 대여 두 번)

E0499 에러는 같은 값에 대한 가변 참조자(&mut) 두 개를 동시에 가지려고 했다는 뜻이에요. 가장 흔한 대여 검사기(borrow checker) 에러 중 하나인데, 고치는 건 대개 간단해요. 한 줄만 옮기면 될 때가 많아요. (다른 에러를 찾고 있나요? Rust 컴파일러 에러 해설을 보세요.)

error[E0499]: cannot borrow `balance` as mutable more than once at a time
--> src/main.rs:5:20
|
4 | let deposit = &mut balance;
| ------------ first mutable borrow occurs here
5 | let withdraw = &mut balance;
| ^^^^^^^^^^^^ second mutable borrow occurs here
6 |
7 | *deposit += 50;
| -------------- first borrow later used here

“balance를 한 번에 두 번 이상 가변으로 빌릴 수 없다”는 뜻이에요. 라벨은 차례대로 “첫 번째 가변 대여가 여기서 일어남”, “두 번째 가변 대여가 여기서 일어남”, “첫 번째 대여가 나중에 여기서 쓰임”이라고 말해요.

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

fn main() {
let mut balance = 100;
let deposit = &mut balance;
let withdraw = &mut balance;
*deposit += 50;
*withdraw -= 30;
println!("Balance: {balance}");
}

컴파일러가 세 곳을 가리키는데, 셋을 합치면 전체 이야기가 돼요.

  1. 4번째 줄: deposit이 balance의 가변 대여가 돼요.
  2. 5번째 줄: withdraw가 balance의 두 번째 가변 대여가 되려고 해요.
  3. 7번째 줄: deposit을 5번째 줄 이후에도 써요. 그래서 두 번째 대여가 시작될 때 첫 번째 대여가 아직 살아 있어요.

세 번째 라벨이 중요해요. &mut 대여 두 개는 그 라이프타임이 겹칠 때만 문제가 돼요. deposit을 5번째 줄 전에 다 썼다면 에러가 나지 않아요.

“편집” 열쇠가 하나뿐인 공유 문서를 떠올려 보세요. 열쇠를 가진 사람만 문서를 고칠 수 있어요. Rust는 열쇠를 복제해 주지 않아요. 두 번째 사람은 첫 번째 사람이 열쇠를 돌려줄 때까지 기다려야 해요.

Rust의 대여 규칙은 이거예요. 어느 순간이든 읽는 쪽(&)은 여럿, 또는 쓰는 쪽(&mut)은 하나만 있을 수 있고, 쓰는 쪽이 둘 이상일 수는 없어요. &mut 참조자는 “내가 있는 동안에는 아무도 이 값을 건드릴 수 없다”는 약속이에요.

이 약속이 실제 버그를 막아 줘요. 두 코드가 같은 값을 동시에 바꿀 수 있다면, 한쪽이 Vec의 크기를 바꾸는 동안 다른 쪽은 예전 메모리를 가리키고 있을 수 있어요. 다른 언어에서는 이게 프로그램 충돌이나 조용한 데이터 손상으로 이어져요. 스레드 사이라면 데이터 경합(data race)이 되고요.

이 규칙 덕분에 컴파일러는 &mut가 살아 있는 동안 그게 데이터에 닿는 유일한 길이라고 확신할 수 있어요. 이 안전성을 실행 비용 없이 얻는 거예요. 대신 가끔은 두 대여가 실제로는 부딪히지 않는다는 걸 컴파일러에게 더 분명하게 알려 줘야 해요.

방법 1: 첫 번째 대여를 끝낸 뒤 두 번째 대여 시작하기

섹션 제목: “방법 1: 첫 번째 대여를 끝낸 뒤 두 번째 대여 시작하기”

가장 먼저 시도해 보세요. E0499는 대부분 이걸로 해결돼요. 대여는 블록 끝이 아니라 마지막으로 쓰인 곳까지만 이어져요. 그러니 첫 번째 참조자로 할 일을 다 하고 나서 두 번째 참조자를 만들면 돼요.

fn main() {
let mut balance = 100;
let deposit = &mut balance;
*deposit += 50; // `deposit`을 마지막으로 써요. 대여가 여기서 끝나요
let withdraw = &mut balance; // ✅ 이제 아무도 빌리고 있지 않아요
*withdraw -= 30;
println!("잔액: {balance}");
}

실행 결과:

잔액: 120
▶ 직접 실행해 보기

변수를 따로 만들 필요가 없을 때도 많아요. balance += 50; 다음에 balance -= 30;이라고 써도 똑같이 돼요.

방법 2: 겹치지 않는 부분을 빌리기

섹션 제목: “방법 2: 겹치지 않는 부분을 빌리기”

가끔은 정말로 &mut 두 개가 동시에 필요한데, 둘이 데이터의 서로 다른 부분을 가리키는 경우가 있어요. 컴파일러가 이걸 늘 알아채지는 못하니까, 직접 보여 줘야 해요.

Vec이나 슬라이스의 두 요소라면, tasks[0]과 tasks[1]은 둘 다 tasks 전체를 빌려요. split_at_mut (영어)을 써서, 절대 겹치지 않는 두 반쪽으로 나누세요.

fn main() {
let mut tasks = vec![String::from("write"), String::from("test")];
// Vec을 겹치지 않는 두 반쪽으로 나눠요
let (left, right) = tasks.split_at_mut(1);
let first = &mut left[0];
let second = &mut right[0];
std::mem::swap(first, second);
println!("{tasks:?}");
}

실행 결과:

["test", "write"]
▶ 직접 실행해 보기

(그냥 맞바꾸기만 할 거라면 tasks.swap(0, 1)이 더 간단해요. split_at_mut은 두 참조자로 실제 작업을 해야 할 때 쓰는 거예요.)

구조체에서는 p.name_mut()와 p.score_mut()처럼 각각 &mut self를 받는 메서드 두 개를 부를 때 같은 에러가 나요. 메서드를 호출할 때마다 구조체 전체를 빌리거든요. 대신 필드를 직접 빌리면, 컴파일러가 둘이 따로라는 걸 알아봐요.

struct Player {
name: String,
score: u32,
}
fn main() {
let mut p = Player { name: String::from("Ferris"), score: 0 };
// 메서드를 거치지 않고 필드를 직접 빌려요
let name = &mut p.name;
let score = &mut p.score;
name.push_str(" the Crab");
*score += 10;
println!("{}의 점수: {}점", p.name, p.score);
}

실행 결과:

Ferris the Crab의 점수: 10점
▶ 직접 실행해 보기

방법 3: 참조자를 들고 있지 말고 인덱스 쓰기

섹션 제목: “방법 3: 참조자를 들고 있지 말고 인덱스 쓰기”

전형적인 예가 있어요. iter_mut()으로 Vec을 돌면서 그 안에서 push도 하려는 경우예요. 반복자가 반복문 내내 &mut를 들고 있으니, push가 두 번째 &mut가 돼요.

fn main() {
let mut queue = vec![1, 2, 3];
for job in queue.iter_mut() {
*job *= 10;
queue.push(*job); // ❌ E0499: `queue`는 이미 iter_mut()이 빌리고 있어요
}
}

반복문이 컬렉션 자체를 바꿔야 한다면, 대신 위치(인덱스)를 도세요. 인덱스는 그냥 숫자라서 아무것도 빌리지 않아요.

fn main() {
let mut queue = vec![1, 2, 3];
// `queue` 안을 가리키는 참조자 대신 위치를 돌아요
for i in 0..queue.len() {
queue[i] *= 10;
let job = queue[i];
queue.push(job);
}
println!("{queue:?}");
}

실행 결과:

[10, 20, 30, 10, 20, 30]
▶ 직접 실행해 보기

0..queue.len()은 반복문이 시작되기 전에 한 번만 계산되기 때문에, 새로 추가된 요소는 다시 돌지 않아요. queue[i]의 대여는 각각 한 줄 동안만 이어져요.

RefCell은 대여 검사를 컴파일할 때가 아니라 실행할 때 해요. 가변 대여 두 개가 같은 순간에 살아 있지만 않으면, mut 변수 없이도 borrow_mut()을 호출할 수 있어요.

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100); // 참고: `mut`가 필요 없어요
*balance.borrow_mut() += 50; // 빌리고, 바꾸고, `;`에서 대여가 끝나요
*balance.borrow_mut() -= 30;
println!("잔액: {}", balance.borrow());
}

실행 결과:

잔액: 120
▶ 직접 실행해 보기

솔직하게 말하면, RefCell은 규칙을 없애 주는 게 아니라 나중에 검사할 뿐이에요. borrow_mut() 가드 두 개를 동시에 살려 두면, 프로그램은 컴파일되지만 실행할 때 패닉이 나요.

use std::cell::RefCell;
fn main() {
let balance = RefCell::new(100);
let mut deposit = balance.borrow_mut();
let mut withdraw = balance.borrow_mut(); // 💥 `deposit`이 아직 살아 있어요
*deposit += 50;
*withdraw -= 30;
}
thread 'main' (12345) panicked at src/main.rs:7:32:
RefCell already borrowed

“RefCell이 이미 대여 중”이라는 패닉이에요. 그러니 RefCell은 데이터가 정말로 공유될 때(예를 들어 Rc<RefCell<T>> 안에서), 그리고 방법 1–3이 맞지 않을 때만 꺼내 쓰세요. 컴파일러를 조용히 시키는 도구가 아니에요.

  • 반복문 안에서 &mut 저장하기. for 반복문 안에서 handles.push(&mut counter)를 하면 “mutably borrowed here in the previous iteration of the loop”(이전 반복에서 여기서 가변으로 대여됨)라는 에러가 나요. 반복할 때마다 같은 값에 대한 &mut가 하나씩 늘어나기 때문이에요. 해결: 인덱스나 소유한 값을 저장하거나, 반복문 안에서 값을 직접 바꾸세요.
  • HashMap에 get_mut 두 번 호출하기. let a = stock.get_mut("apples") 다음에 let p = stock.get_mut("pears")를 하면 stock을 두 번 빌려요. 해결: get_mut을 다시 부르기 전에 a를 다 쓰거나, stock.get_disjoint_mut(["apples", "pears"])로 둘을 한 번에 가져오세요.
  • 같은 변수를 바꾸는 클로저 두 개. let mut inc = || count += 1;과 let mut inc2 = || count += 2;는 각자 살아 있는 동안 &mut count를 들고 있어요. 해결: 클로저를 하나만 쓰거나, 두 번째 클로저를 만들기 전에 첫 번째 클로저를 마지막으로 호출하세요. 클로저를 참고하세요.

이해도 점검

1. Rust는 언제 E0499를 낼까요?

2. let a = &mut v;를 만들고, a를 마지막으로 쓴 뒤, let b = &mut v;를 만들어요. 어떻게 될까요?

3. 같은 슬라이스의 서로 다른 요소에 대한 &mut 참조자 두 개를 얻게 해 주는 도구는?

점수: 0 / 3