- Published on
Rust cannot borrow as mutable 에러 7패턴
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 CLI를 Rust로 만들다 보면 빌드가 멈추는 순간이 거의 항상 빌림 규칙(ownership/borrow)에서 옵니다. 그중에서도 cannot borrow as mutable은 “지금 이 값은 가변으로 빌릴 수 없는 상태”라는 뜻인데, 메시지 자체는 단순해도 왜 지금 불가능한지가 케이스마다 다릅니다.
이 글은 실무에서 가장 자주 나오는 cannot borrow as mutable을 7가지 패턴으로 나눠, 에러가 나는 코드와 고치는 코드를 함께 정리합니다.
참고: Rust의 빌림 규칙 핵심은 “동시에
&mut는 하나만” 그리고 “&가 살아있는 동안&mut는 불가”입니다.
패턴 1) 불변 참조가 살아있는 상태에서 가변 빌림
가장 흔한 형태입니다. 이미 &T로 빌려서 읽고 있는데, 같은 스코프에서 &mut T로 다시 빌리려 하면 막힙니다.
fn main() {
let mut s = String::from("hello");
let r = &s; // 불변 빌림
// println!("{}", r);
let w = &mut s; // error: cannot borrow `s` as mutable because it is also borrowed as immutable
w.push_str(" world");
println!("{}", r);
}
해결 1) 불변 참조 사용을 먼저 끝내기(스코프 분리)
fn main() {
let mut s = String::from("hello");
{
let r = &s;
println!("{}", r);
} // 여기서 r drop
let w = &mut s;
w.push_str(" world");
println!("{}", s);
}
해결 2) 값 복사/복제해서 참조 수명 단축
fn main() {
let mut s = String::from("hello");
let snapshot = s.clone();
let w = &mut s;
w.push_str(" world");
println!("before: {}", snapshot);
println!("after: {}", s);
}
패턴 2) 같은 값에 대한 &mut를 두 번 만들기
&mut는 배타적(exclusive)입니다. 즉, 동시에 두 개를 만들 수 없습니다.
fn main() {
let mut v = vec![1, 2, 3];
let a = &mut v;
let b = &mut v; // error: cannot borrow `v` as mutable more than once at a time
a.push(4);
b.push(5);
}
해결 1) 한 번에 하나만 쓰기
fn main() {
let mut v = vec![1, 2, 3];
{
let a = &mut v;
a.push(4);
}
{
let b = &mut v;
b.push(5);
}
println!("{:?}", v);
}
해결 2) 서로 다른 영역을 빌릴 땐 split_at_mut 사용
벡터의 서로 다른 구간을 동시에 수정하고 싶다면 안전한 API를 써야 합니다.
fn main() {
let mut v = vec![10, 20, 30, 40];
let (left, right) = v.split_at_mut(2);
left[0] += 1;
right[0] += 100;
println!("{:?}", v);
}
패턴 3) iter()로 돌면서 컬렉션을 수정하려고 함
반복 중인 컬렉션을 같은 루프에서 수정하는 건 Rust가 강하게 막습니다.
fn main() {
let mut v = vec![1, 2, 3];
for x in v.iter() {
if *x == 2 {
v.push(4); // error: cannot borrow `v` as mutable because it is also borrowed as immutable
}
}
}
해결 1) 인덱스 기반 루프(단, push로 길이 바뀌면 주의)
fn main() {
let mut v = vec![1, 2, 3];
let mut i = 0;
while i < v.len() {
if v[i] == 2 {
v.push(4);
}
i += 1;
}
println!("{:?}", v);
}
해결 2) 변경할 항목을 먼저 수집 후 반영
fn main() {
let mut v = vec![1, 2, 3];
let should_push = v.iter().any(|x| *x == 2);
if should_push {
v.push(4);
}
println!("{:?}", v);
}
해결 3) 원소를 수정만 할 거면 iter_mut()
fn main() {
let mut v = vec![1, 2, 3];
for x in v.iter_mut() {
*x *= 10;
}
println!("{:?}", v);
}
패턴 4) HashMap::get으로 꺼낸 참조를 들고 insert/entry 호출
get은 &V를 반환합니다. 그 참조가 살아있는 동안 같은 HashMap을 가변으로 만지면 충돌합니다.
use std::collections::HashMap;
fn main() {
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".to_string(), 1);
let v = m.get("a");
// v를 사용하는 동안...
m.insert("b".to_string(), 2); // error: cannot borrow `m` as mutable because it is also borrowed as immutable
println!("{:?}", v);
}
해결 1) 필요한 값만 복사해서 참조 수명 종료
use std::collections::HashMap;
fn main() {
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".to_string(), 1);
let a_value = m.get("a").copied();
m.insert("b".to_string(), 2);
println!("a={:?}", a_value);
}
해결 2) 갱신 로직이면 entry로 한 번에 처리
use std::collections::HashMap;
fn main() {
let mut m: HashMap<String, i32> = HashMap::new();
*m.entry("a".to_string()).or_insert(0) += 1;
*m.entry("a".to_string()).or_insert(0) += 1;
println!("{:?}", m);
}
실무에서는 Kafka 컨슈머의 멱등 처리처럼 “키별 카운트/상태를 누적”하는 코드에서 이 패턴이 매우 자주 나옵니다. 상태 누적 설계를 고민 중이라면 Kafka 중복·역순 메시지, DDD로 멱등 처리하기도 같이 보면 구조적으로 도움이 됩니다.
패턴 5) 메서드 체인/클로저가 빌림을 예상보다 길게 잡는 경우
특히 if let/match/클로저 안에서 참조를 잡아두고, 같은 스코프에서 다시 가변 접근을 하면 “참조가 아직 살아있다”고 판단됩니다.
fn main() {
let mut s = String::from("hello");
let first = s.chars().next(); // 내부적으로 s를 불변으로 빌리는 흐름이 이어질 수 있음
if first == Some('h') {
s.push('!'); // 상황에 따라 cannot borrow as mutable 류 에러로 이어질 수 있음
}
println!("{}", s);
}
해결) 필요한 정보만 먼저 값으로 뽑아두기
fn main() {
let mut s = String::from("hello");
let first = s.chars().next().unwrap_or('_');
if first == 'h' {
s.push('!');
}
println!("{}", s);
}
이 케이스는 “왜 이게 아직 borrow 중이지?”라는 느낌을 주는데, 해결 전략은 대개 동일합니다.
- 참조를 변수에 오래 들고 있지 말고, 필요한 데이터만 값으로 만든다
- 스코프를 분리해 drop 시점을 명확히 한다
패턴 6) 인덱싱/슬라이스에서 겹치는 가변 참조 만들기
다음은 같은 배열에서 두 원소를 동시에 가변 참조하려는 코드입니다.
fn main() {
let mut a = [1, 2, 3];
let x = &mut a[0];
let y = &mut a[1]; // error: cannot borrow `a[_]` as mutable more than once at a time
*x += 10;
*y += 20;
}
해결) split_at_mut로 겹치지 않음을 증명
fn main() {
let mut a = [1, 2, 3];
let (l, r) = a.split_at_mut(1);
let x = &mut l[0];
let y = &mut r[0];
*x += 10;
*y += 20;
println!("{:?}", a);
}
패턴 7) 구조체 필드와 메서드에서 self를 동시에 빌림
메서드 내부에서 self.field를 불변으로 빌린 상태에서 self를 가변으로 쓰는 메서드를 다시 호출하면 충돌합니다.
struct App {
name: String,
counter: i32,
}
impl App {
fn bump(&mut self) {
self.counter += 1;
}
fn run(&mut self) {
let n = &self.name; // 불변 빌림
self.bump(); // error: cannot borrow `*self` as mutable because it is also borrowed as immutable
println!("{}", n);
}
}
fn main() {
let mut app = App { name: "svc".to_string(), counter: 0 };
app.run();
}
해결 1) 필드 값을 복사/복제해서 분리
struct App {
name: String,
counter: i32,
}
impl App {
fn bump(&mut self) {
self.counter += 1;
}
fn run(&mut self) {
let n = self.name.clone();
self.bump();
println!("{}", n);
}
}
해결 2) 필드 접근을 메서드 호출 뒤로 이동
struct App {
name: String,
counter: i32,
}
impl App {
fn bump(&mut self) {
self.counter += 1;
}
fn run(&mut self) {
self.bump();
let n = &self.name;
println!("{}", n);
}
}
이 패턴은 웹 서버 핸들러나 상태 머신에서 특히 자주 나옵니다. 상태를 바꾸는 메서드 호출과 로그 출력/메트릭 태깅처럼 읽기 작업이 섞이면서 self의 borrow가 꼬이기 쉽습니다.
디버깅 체크리스트: 원인을 빨리 찾는 순서
cannot borrow as mutable을 보면 아래 순서로 보면 빠릅니다.
- 같은 값에 대한
&가 남아있는지 확인 &mut를 두 개 만들고 있지 않은지 확인iter()/get()로 얻은 참조를 들고 변경 메서드를 호출하지 않는지 확인- 스코프를
{ ... }로 잘라 drop 시점을 명확히 할 수 있는지 확인 - “참조가 꼭 필요했나?”를 되묻고, 값 복사(
copy/clone/to_owned)로 단순화 가능한지 확인
실전 팁: 설계로 예방하기
- 상태 변경과 조회를 한 함수에서 섞지 말고, 단계별로 나누면 borrow 충돌이 급감합니다.
- 컬렉션을 순회하며 수정해야 한다면,
retain,drain,split_at_mut,entry등 “빌림 규칙을 API가 대신 증명해주는” 표준 메서드를 우선 검토하세요. - 비동기/캐시/상태 관리 코드에서는 “읽기 참조를 오래 들고 있는 구조”가 문제를 키웁니다. Next.js RSC에서 캐시로 데이터가 안 바뀌는 상황처럼, 상태와 갱신 타이밍을 분리해 사고를 줄이는 접근이 유효합니다. 관련 글로 Next.js 14 RSC 캐시로 데이터가 안 바뀔 때도 참고할 만합니다.
마무리
cannot borrow as mutable은 Rust가 “데이터 레이스/무효 참조”를 컴파일 타임에 막아주는 대표적인 신호입니다. 에러를 없애는 요령은 결국 두 가지로 수렴합니다.
- 참조의 수명을 짧게 만들기(스코프/값 복사)
- 동시에 필요한 가변 참조를 안전한 API로 분해하기(
split_at_mut,entry,iter_mut등)
위 7패턴을 머릿속에 넣어두면, 비슷한 에러를 봐도 원인 추적 시간이 크게 줄어듭니다.