Rust 에러 E0594: cannot assign, as it is not declared as mutable (mut 없이 대입)
E0594는 무언가 = 값(또는 +=, -=, …)이라고 썼는데, 그 무언가를 읽을 권한만 있다는 뜻이에요. 보통은 mut가 없는 변수의 구조체 필드이거나, & 참조자를 통해 접근한 값이에요. 아주 흔한 에러이고, 고치는 방법은 거의 항상 한 단어, mut예요. 이 페이지에서는 고치는 방법을 먼저 보여 드리고, 그다음에 설명할게요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 모음에서 볼 수 있어요.
에러 메시지
섹션 제목: “에러 메시지”error[E0594]: cannot assign to `p.hp`, as `p` is not declared as mutable --> src/main.rs:8:5 |8 | p.hp = 5; // ❌ `p`는 읽기 전용이에요 | ^^^^^^^^ cannot assign |help: consider changing this to be mutable |7 | let mut p = Player { name: String::from("Ferris"), hp: 10 }; | +++“p가 가변으로 선언되지 않았으므로 p.hp에 대입할 수 없다”는 뜻이에요. help:는 “이걸 가변으로 바꿔 보라”고 제안하고 있어요.
이 에러를 만드는 프로그램은 이거예요.
struct Player { name: String, hp: u32,}
fn main() { let p = Player { name: String::from("Ferris"), hp: 10 }; p.hp = 5; // ❌ `p`는 읽기 전용이에요 println!("{} has {} hp", p.name, p.hp);}두 번째 문구도 자주 보게 될 거예요. “cannot assign to *x, which is behind a & reference”(& 참조자 뒤에 있는 *x에는 대입할 수 없음). 에러 코드는 같고 원인이 살짝 달라요. 이 경우에는 & 참조자를 들고 있는데, & 참조자로는 보기만 할 수 있어요. 아래에서 두 경우를 모두 다뤄요.
무슨 뜻인가요
섹션 제목: “무슨 뜻인가요”8번째 줄의 p.hp = 5 아래에 있는 ^^^^^^^^를 보세요. p의 필드를 바꾸고 있어요. 그런데 p는 그냥 let으로 만들었으니 구조체 전체가 읽기 전용이고, 안에 있는 모든 필드도 마찬가지예요.
help: 줄이 해결 방법을 보여 줘요. let mut p예요. +++는 추가해야 할 세 글자의 위치예요.
두 번째 문구에서는 컴파일러가 *x += 1 같은 줄을 가리키면서 `x` is a `&` reference, so it cannot be written to(x는 & 참조자라서 쓸 수 없음)라고 덧붙여요. 여러분이 소유한 변수를 바꾸는 게 아니에요. 빌려 온 읽기 전용 참조자를 통해 남의 값을 바꾸려는 거예요.
도서관 책을 떠올려 보세요. 원하는 만큼 읽을 수는 있지만, 책에 글씨를 쓰면 안 되죠. 메모를 하려면 내 책을 사거나(let mut), 주인의 허락을 받아야 해요(&mut).
Rust가 이렇게 하는 이유
섹션 제목: “Rust가 이렇게 하는 이유”Rust에서는 따로 말하지 않는 한 모든 것이 읽기 전용이에요. 변수를 바꾸려면 let mut가 필요하고, 참조자가 가리키는 값을 바꾸려면 그 참조자가 &mut여야 해요. 구조체를 가변으로 만들기와 가변 참조자를 참고하세요.
Rust에는 “가변 필드”라는 게 없어요. 가변성은 개별 필드가 아니라 변수(또는 참조자)에 속해요. 그래서 let mut p라고 쓰면 p의 모든 필드를 바꿀 수 있고, 그냥 let p라고 쓰면 어떤 필드도 바꿀 수 없어요. 덕분에 규칙이 단순해요. 값이 어떻게 선언됐는지만 보면 바뀔 수 있는지 알 수 있죠.
참조자 규칙은 뜻밖의 일로부터 여러분을 지켜 줘요. 함수에 &player를 넘기면, 그 함수의 시그니처가 읽기만 하겠다고 약속하는 셈이에요. 이 약속이 없다면 어떤 함수든 몰래 데이터를 바꿀 수 있고, 컴파일러는 읽는 쪽과 쓰는 쪽이 같은 값을 동시에 건드리는 걸 막을 수 없을 거예요(그게 바로 E0502가 다루는 문제예요).
고치는 방법
섹션 제목: “고치는 방법”방법 1: 변수에 mut 붙이기
섹션 제목: “방법 1: 변수에 mut 붙이기”구조체를 직접 소유하고 있고, 그 필드를 바꾸고 싶을 때 고르세요. 대부분은 이게 정답이에요.
struct Player { name: String, hp: u32,}
fn main() { let mut p = Player { name: String::from("Ferris"), hp: 10 }; p.hp = 5; p.hp += 3; println!("{}의 체력: {}", p.name, p.hp);}실행 결과:
Ferris의 체력: 8튜플(pair.0 = 1;)과 배열(grid[2] = 7;)도 마찬가지로, 그 값을 담은 변수에 mut가 필요해요. Vec 안에 있는 구조체라면 team[0].hp = 10;에는 let mut team이 필요해요. 이 경우에는 E0596으로 보고되지만, 해결 방법은 같아요.
딱 한 번만 대입한다면 mut가 아예 필요 없을 수도 있어요. Player { hp: 5, ..other }처럼 처음부터 올바른 값으로 구조체를 만들면 돼요.
방법 2: &mut v나 iter_mut()로 반복하기
섹션 제목: “방법 2: &mut v나 iter_mut()로 반복하기”컬렉션을 돌면서 각 항목을 바꾸고 싶을 때 고르세요. for x in &v와 v.iter()는 읽기 전용인 & 참조자를 내줘요.
fn main() { let scores = vec![10, 20, 30]; for x in &scores { *x += 1; // ❌ `x`는 `&i32`예요 } println!("{scores:?}");}error[E0594]: cannot assign to `*x`, which is behind a `&` reference --> src/main.rs:4:9 |3 | for x in &scores { | ------- this iterator yields `&` references4 | *x += 1; // ❌ `x`는 `&i32`예요 | ^^^^^^^ `x` is a `&` reference, so it cannot be written to |help: use a mutable iterator instead |3 | for x in &mut scores { | +++“이 반복자는 & 참조자를 내놓으니, x에는 쓸 수 없다”는 뜻이고, help:는 가변 반복자를 쓰라고 제안해요.
&mut scores로 반복하고(또는 .iter_mut()를 호출하고), 벡터 자체도 mut로 만드세요.
fn main() { let mut scores = vec![10, 20, 30]; for x in &mut scores { *x += 1; } println!("{scores:?}");
for x in scores.iter_mut() { *x *= 2; } println!("{scores:?}");}실행 결과:
[11, 21, 31][22, 42, 62]&mut scores와 scores.iter_mut()는 같은 일을 해요. 읽기 편한 쪽을 고르세요. 목록을 제자리에서 바꿀 필요가 없다면 새 목록을 만드는 방법도 있어요. let bigger: Vec<i32> = scores.iter().map(|x| x + 1).collect();처럼요. 반복자를 참고하세요.
방법 3: &mut self나 &mut 매개변수 쓰기
섹션 제목: “방법 3: &mut self나 &mut 매개변수 쓰기”메서드나 함수가 넘겨받은 데이터를 바꿀 때 고르세요. &self를 받는 메서드는 구조체를 읽기만 할 수 있어서, 필드에 대입하면 실패해요.
impl Account { fn deposit(&self, amount: u64) { self.balance += amount; // ❌ `self`는 `&` 참조자예요 }}error[E0594]: cannot assign to `self.balance`, which is behind a `&` reference --> src/main.rs:7:9 |7 | self.balance += amount; | ^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so it cannot be written to |help: consider changing this to be a mutable reference |6 | fn deposit(&mut self, amount: u64) { | +++“self는 & 참조자라서 쓸 수 없다”는 뜻이고, help:는 가변 참조자(&mut self)로 바꾸라고 제안해요.
& 매개변수를 받는 일반 함수도 같은 에러를 내요. fn reset(score: &u32) { *score = 0; }은 cannot assign to `*score`, which is behind a `&` reference로 실패해요. 해결 방법도 둘 다 같아요. 시그니처에 &mut라고 쓰고, 호출하는 쪽 변수를 let mut로 만들고, 호출할 때 &mut를 넘기세요.
struct Account { balance: u64,}
impl Account { fn deposit(&mut self, amount: u64) { self.balance += amount; }}
fn reset(score: &mut u32) { *score = 0;}
fn main() { let mut acc = Account { balance: 0 }; acc.deposit(50); acc.deposit(25); println!("잔액: {}", acc.balance);
let mut score = 42; reset(&mut score); println!("점수: {score}");}실행 결과:
잔액: 75점수: 0메서드는 호출할 때 Rust가 &mut를 알아서 붙여 줘요. acc가 let mut이기만 하면 acc.deposit(50)이 스스로 acc를 가변으로 대여해요. 함수는 직접 써야 해요. reset(&mut score)처럼요. &mut u32를 원하는 함수에 &score를 넘기면 E0308(types differ in mutability)이 나요. 메서드로 동작 추가하기를 참고하세요.
방법 4(최후의 수단): Cell이나 RefCell
섹션 제목: “방법 4(최후의 수단): Cell이나 RefCell”&mut를 얻을 수 없을 때만 고르세요. 예를 들어 메서드가 &self로 정해진 트레이트를 구현하거나, 프로그램의 여러 부분이 Rc로 값 하나를 공유하는 경우예요. Cell과 RefCell을 쓰면 & 참조자를 통해서도 값을 바꿀 수 있어요(“내부 가변성”, interior mutability).
use std::cell::{Cell, RefCell};
struct Catalog { items: Vec<String>, lookups: Cell<u32>, history: RefCell<Vec<String>>,}
impl Catalog { fn contains(&self, name: &str) -> bool { self.lookups.set(self.lookups.get() + 1); self.history.borrow_mut().push(name.to_string()); self.items.iter().any(|item| item == name) }}
fn main() { let catalog = Catalog { items: vec![String::from("사과"), String::from("배")], lookups: Cell::new(0), history: RefCell::new(Vec::new()), }; println!("배? {}", catalog.contains("배")); println!("키위? {}", catalog.contains("키위")); println!("조회 횟수: {}", catalog.lookups.get()); println!("기록: {:?}", catalog.history.borrow());}실행 결과:
배? true키위? false조회 횟수: 2기록: ["배", "키위"]Cell은 카운터 같은 작은 Copy 값에, RefCell은 그 밖의 모든 것에 써요. 대가도 솔직하게 말할게요. RefCell은 대여 검사를 컴파일 시점에서 실행 시점으로 옮기고, 동시에 두 번 가변으로 대여하면 패닉이 나요. 그냥 &mut로 된다면 그쪽을 쓰세요. 여러 스레드 사이에서는 대신 Mutex나 원자적 타입(atomic)을 쓰세요(공유 상태 참고). 자세한 내용은 RefCell에 있어요.
자주 보게 되는 곳
섹션 제목: “자주 보게 되는 곳”HashMap 안의 값을 바꿀 때. *stock.get("apple").unwrap() += 1;은 cannot assign to data in a `&` reference로 실패해요. get은 & 참조자를 돌려주기 때문이에요. 해결: *stock.get_mut("apple").unwrap() += 1;이나 *stock.entry("apple").or_insert(0) += 1;을 쓰세요. 해시맵을 참고하세요.
Fn으로 넘긴 클로저가 변수를 바꿀 때. run이 F: Fn()을 받는데 run(|| count += 1)이라고 쓰면 cannot assign to `count`, as it is a captured variable in a `Fn` closure(Fn 클로저에 캡처된 변수라 대입할 수 없음)로 실패해요. 해결: 바운드를 F: FnMut()으로 바꾸세요(run 안에서는 mut f도 필요해요). 바운드를 잘못 골랐을 때를 참고하세요.
static에 둔 전역 카운터. static VISITS: u32 = 0;을 쓰고 VISITS += 1;을 하면 cannot assign to immutable static item `VISITS`로 실패해요. 해결: static VISITS: AtomicU32 = AtomicU32::new(0);과 VISITS.fetch_add(1, Ordering::Relaxed);를 쓰거나, 더 좋은 방법으로는 카운터를 &mut로 넘겨주세요.
관련 에러
섹션 제목: “관련 에러”- E0596: cannot borrow as mutable. 똑같이
mut가 빠진 경우지만, 대입이 아니라list.push(x)같은 메서드 호출에서 나요. - E0384: cannot assign twice to immutable variable. 필드나 참조자가 아니라 변수 전체에 대입할 때(
x = 5;) 나요. - E0308: mismatched types,
types differ in mutability.&mut x가 필요한 곳에&x를 넘겼을 때 나요. - E0502:
&mut권한은 있지만, 다른 곳에서 아직 그 값을 읽고 있어요. - E0507: cannot move out of a value behind a reference. 다시 “읽기 전용” 규칙이에요. 이번에는 쓰기가 아니라 이동이 문제예요.
- 모든 컴파일러 에러: 에러 페이지 전체 목록이에요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. let user = User { age: 30 }; user.age = 31;에서 E0594가 나요. 어떻게 고치나요?
2. for p in &prices { *p += 1; }이 실패해요. 무엇을 바꿔야 하나요?
3. 메서드 fn rename(&self, n: String) { self.name = n; }에서 E0594가 나요. 보통은 어떻게 고치나요?
점수: 0 / 3
더 알아보기
섹션 제목: “더 알아보기”- E0594 — Rust error codes index (영어): 이 에러에 대한 공식 설명이에요.
- 구조체 정의 및 인스턴트화 — The Rust Programming Language 한국어판: 필드 하나가 아니라 인스턴스 전체가 가변이어야 하는 이유를 설명해요.
- 참조와 대여 — The Rust Programming Language 한국어판: “가변 참조자” 부분에서
&와&mut의 차이를 설명해요. - 메서드 문법 — The Rust Programming Language 한국어판: 메서드에
&self,&mut self,self중 무엇이 필요한지 다뤄요. - RefCell<T>와 내부 가변성 패턴 — The Rust Programming Language 한국어판:
RefCell이&를 통해 값을 바꾸게 해 주는 방법과, 실행 시점에 드는 비용을 다뤄요.