Published on

Rust cannot borrow as mutable 에러 7패턴

Authors

서버나 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을 보면 아래 순서로 보면 빠릅니다.

  1. 같은 값에 대한 &가 남아있는지 확인
  2. &mut를 두 개 만들고 있지 않은지 확인
  3. iter()/get()로 얻은 참조를 들고 변경 메서드를 호출하지 않는지 확인
  4. 스코프를 { ... }로 잘라 drop 시점을 명확히 할 수 있는지 확인
  5. “참조가 꼭 필요했나?”를 되묻고, 값 복사(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패턴을 머릿속에 넣어두면, 비슷한 에러를 봐도 원인 추적 시간이 크게 줄어듭니다.