Published on

Rust 소유권 에러 E0502·E0499 패턴별 해결

Authors

Rust를 쓰다 보면 컴파일러가 가장 친절하면서도 냉정하게 막아서는 순간이 있습니다. 특히 E0502E0499 는 “소유권/빌림 규칙을 이해하고 있는가?”를 매번 시험하는 대표 에러입니다.

  • E0502: 같은 값에 대해 불변 빌림이 살아있는 동안 가변 빌림을 시도했을 때
  • E0499: 같은 값에 대해 동시에 2개 이상의 가변 빌림을 시도했을 때

이 글에서는 두 에러를 “왜 발생했는지”보다 더 중요한 “어떤 코드 패턴에서 반복되는지”에 초점을 맞춰, 실무에서 가장 자주 쓰는 해결법을 패턴별로 정리합니다.

관련해서 5분 요약 버전이 필요하면 다음 글도 함께 보세요: Rust E0502·E0499 빌림 충돌 5분 해결


E0502: 불변 빌림이 끝나기 전에 가변 빌림을 시도함

패턴 1) 불변 참조를 잡아둔 채로 수정하려는 경우

아래 코드는 name_refuser.name 을 불변으로 빌린 상태에서, 같은 user 를 가변으로 빌려 수정하려 해서 E0502 가 납니다.

#[derive(Debug)]
struct User {
    name: String,
    age: u32,
}

fn main() {
    let mut user = User {
        name: "alice".to_string(),
        age: 20,
    };

    let name_ref = &user.name; // 불변 빌림 시작

    user.age += 1; // 가변 빌림 필요: E0502

    println!("{}", name_ref);
}

해결 A) 불변 빌림의 생존 범위를 줄이기(스코프 분리)

불변 참조가 필요한 구간을 블록으로 감싸 “빌림이 끝나는 지점”을 명확히 만들면 됩니다.

fn main() {
    let mut user = User {
        name: "alice".to_string(),
        age: 20,
    };

    {
        let name_ref = &user.name;
        println!("{}", name_ref);
    } // 여기서 불변 빌림 종료

    user.age += 1; // OK
}

해결 B) 참조 대신 값 복사/복제하기

StringCopy 가 아니므로 clone() 이 필요합니다. 비용이 있지만, 로직이 단순해지고 빌림 충돌이 사라집니다.

fn main() {
    let mut user = User {
        name: "alice".to_string(),
        age: 20,
    };

    let name = user.name.clone();
    user.age += 1;
    println!("{}", name);
}

clone() 이 부담이라면, 종종 “로그/메트릭 출력용 문자열” 정도는 format! 으로 즉시 만들고 참조를 오래 들고 있지 않게 구성하는 편이 낫습니다.


패턴 2) iter() 로 순회하며 같은 컬렉션을 수정하려는 경우

Veciter() 로 불변 순회하는 동안, 같은 Vecpush 하면 내부 버퍼 재할당 가능성 때문에 안전하지 않습니다. 그래서 E0502 가 납니다.

fn main() {
    let mut v = vec![1, 2, 3];

    for x in v.iter() {
        if *x == 2 {
            v.push(4); // E0502
        }
    }
}

해결 A) 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);
    }
}

해결 B) 인덱스 기반 루프(단, 길이 변화에 주의)

길이가 늘어날 수 있으면 종료 조건을 고정해야 무한 루프를 피합니다.

fn main() {
    let mut v = vec![1, 2, 3];
    let n = v.len();

    for i in 0..n {
        if v[i] == 2 {
            v.push(4);
        }
    }
}

이 방식은 “기존 원소만 검사하고 뒤에 추가는 허용” 같은 요구에 적합합니다.

해결 C) retain / drain_filter / 새 벡터로 재구성

수정이 “추가”가 아니라 “필터링/변환”이라면 컬렉션 API를 쓰는 편이 더 안전하고 빠릅니다.

fn main() {
    let mut v = vec![1, 2, 3, 2];
    v.retain(|x| *x != 2);
    assert_eq!(v, vec![1, 3]);
}

패턴 3) HashMap 에서 get() 으로 읽고 entry() 로 쓰기

get() 으로 불변 빌림을 잡아두고, 같은 맵에 entry() 로 가변 접근하면 E0502 가 납니다.

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"); // 불변 빌림

    m.entry("a".to_string()).and_modify(|x| *x += 1); // E0502

    println!("{:?}", v);
}

해결 A) entry() 로 읽기와 쓰기를 한 번에

use std::collections::HashMap;

fn main() {
    let mut m: HashMap<String, i32> = HashMap::new();

    let x = m.entry("a".to_string()).or_insert(0);
    *x += 1;
}

해결 B) 읽기 결과를 값으로 복사해 빌림을 끊기

값 타입이 Copy 면 특히 깔끔합니다.

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").unwrap_or(&0); // i32는 Copy

    m.entry("a".to_string()).and_modify(|x| *x += 1);

    println!("{}", v);
}

E0499: 동시에 가변 빌림을 2번 이상 시도함

패턴 1) 같은 벡터에서 두 원소를 동시에 &mut 로 잡기

가장 흔한 E0499 입니다.

fn main() {
    let mut v = vec![10, 20, 30];

    let a = &mut v[0];
    let b = &mut v[1]; // E0499

    *a += 1;
    *b += 1;
}

Rust는 두 인덱스가 다르다는 사실을 일반적인 인덱싱만으로는 증명하지 못합니다.

해결 A) split_at_mut 로 비겹치는 슬라이스를 만들기

서로 겹치지 않는 두 구간으로 쪼개면 컴파일러가 안전을 증명할 수 있습니다.

fn main() {
    let mut v = vec![10, 20, 30];

    let (left, right) = v.split_at_mut(1);
    let a = &mut left[0];
    let b = &mut right[0];

    *a += 1;
    *b += 1;

    assert_eq!(v, vec![11, 21, 30]);
}

해결 B) 표준 라이브러리의 get_many_mut 사용(버전 확인)

Rust 버전에 따라 안정화 여부가 다를 수 있어, 팀 표준 툴체인에서는 먼저 확인하세요.

fn main() {
    let mut v = vec![10, 20, 30];

    // 사용 가능 버전이라면 다음처럼 여러 mutable 참조를 동시에 얻을 수 있습니다.
    // let [a, b] = v.get_many_mut([0, 1]).unwrap();
    // *a += 1;
    // *b += 1;
}

툴체인 제약이 있으면 split_at_mut 가 가장 이식성 높은 해법입니다.


패턴 2) 메서드 체인/클로저에서 &mut self 가 중첩되는 경우

다음 코드는 self.items.get_mut() 로 이미 가변 빌림이 발생했는데, 그 상태에서 self.log() 를 호출하면서 또 &mut self 가 필요해 E0499 로 이어질 수 있습니다.

struct Store {
    items: Vec<i32>,
    logs: Vec<String>,
}

impl Store {
    fn log(&mut self, msg: String) {
        self.logs.push(msg);
    }

    fn bump_and_log(&mut self, idx: usize) {
        let x = self.items.get_mut(idx).unwrap();
        *x += 1;

        // 여기서 self 전체를 다시 &mut 로 빌리려 하면 충돌 가능
        self.log(format!("bumped {}", idx));
    }
}

위 예시는 NLL(Non-Lexical Lifetimes) 덕분에 상황에 따라 통과할 수도 있지만, 실제로는 x 를 더 오래 사용하거나 클로저로 넘기는 순간 쉽게 E0499 로 변합니다.

해결 A) 가변 참조 사용 구간을 끝낸 뒤에 다른 &mut self 호출

핵심은 “x 의 생존을 최소화”입니다.

impl Store {
    fn bump_and_log(&mut self, idx: usize) {
        {
            let x = self.items.get_mut(idx).unwrap();
            *x += 1;
        } // items에 대한 가변 빌림 종료

        self.log(format!("bumped {}", idx));
    }
}

해결 B) 구조 분해로 서로 다른 필드를 분리해 빌림

서로 다른 필드는 동시에 가변 빌림이 가능합니다. “self 전체”를 다시 빌리지 않게 만드는 테크닉입니다.

impl Store {
    fn bump_and_log(&mut self, idx: usize) {
        let Store { items, logs } = self;

        if let Some(x) = items.get_mut(idx) {
            *x += 1;
        }

        logs.push(format!("bumped {}", idx));
    }
}

이 패턴은 서비스 코드에서 특히 유용합니다. 한 메서드 안에서 self.cache 도 만지고 self.metrics 도 만지는 경우, 구조 분해를 해두면 빌림 충돌이 크게 줄어듭니다.


패턴 3) 반복문에서 이전 원소의 &mut 를 들고 다음 반복에서 또 빌림

fn main() {
    let mut v = vec![1, 2, 3, 4];

    let mut prev = &mut v[0];

    for i in 1..v.len() {
        let cur = &mut v[i]; // E0499 가능
        *cur += *prev;
        prev = cur;
    }
}

해결 A) 인덱스로 상태를 들고 있고, 실제 &mut 는 매 반복마다 짧게

fn main() {
    let mut v = vec![1, 2, 3, 4];

    let mut prev_idx = 0;

    for i in 1..v.len() {
        let prev_val = v[prev_idx];
        v[i] += prev_val;
        prev_idx = i;
    }

    assert_eq!(v, vec![1, 3, 6, 10]);
}

값을 복사할 수 있는 타입이라면 이 방식이 가장 단순합니다.

해결 B) split_at_mut 로 현재와 이전을 분리

fn main() {
    let mut v = vec![1, 2, 3, 4];

    for i in 1..v.len() {
        let (left, right) = v.split_at_mut(i);
        let prev = left.last().unwrap();
        let cur = &mut right[0];
        *cur += *prev;
    }

    assert_eq!(v, vec![1, 3, 6, 10]);
}

자주 쓰는 “해결 전략” 체크리스트

1) 빌림을 짧게: 스코프/블록으로 생존 범위를 끊어라

  • let r = &x; 를 만들었다면, 그 참조가 실제로 필요한 마지막 줄이 어디인지 확인
  • 필요 이상으로 참조를 들고 있으면 E0502E0499 모두를 유발

2) “읽기와 쓰기”를 분리하거나, entry() 같은 단일 API로 합쳐라

  • 컬렉션은 “읽고 나서 쓰기”가 아니라 “한 번에 처리”할 수 있는 API가 많음
  • HashMap 은 특히 entry() 가 정답인 경우가 많음

3) 동시에 두 &mut 가 필요하면 “비겹침”을 컴파일러가 증명할 수 있게 만들어라

  • split_at_mut 는 가장 범용적인 도구
  • 같은 슬라이스에서 서로 다른 원소를 동시에 수정해야 한다면 거의 항상 후보

4) 값 복사/복제는 최후가 아니라 “의도 표현”일 때가 많다

  • Copy 타입은 과감히 값으로 들고 다니면 빌림 충돌이 크게 줄어듦
  • String 이나 큰 구조체는 clone() 비용을 측정하되, 코드 복잡도를 줄이는 쪽이 전체 성능에 유리한 경우도 많음

디버깅 팁: 에러 메시지에서 꼭 봐야 할 2줄

컴파일러는 보통 다음 정보를 줍니다.

  • “첫 빌림이 어디서 시작했는지”
  • “그 빌림이 살아있다고 판단되는 마지막 사용 지점이 어디인지”

E0502E0499 를 보면, 대개 해결은 그 두 지점 사이를 줄이거나(스코프), 빌림의 종류를 바꾸거나(값 복사), 비겹침을 증명하는 API를 쓰는 것(split_at_mut) 중 하나로 귀결됩니다.


마무리: 패턴으로 외우면 해결 속도가 빨라진다

E0502E0499 는 “러스트가 불편하다”가 아니라, 데이터 경합/유즈 애프터 프리 같은 버그를 컴파일 타임에 제거하려는 설계의 결과입니다. 중요한 건 규칙 자체를 암기하는 게 아니라, 자주 터지는 코드를 패턴으로 묶어 즉시 대응하는 것입니다.

  • E0502: 불변 참조를 오래 들고 있지 말고, 읽기/쓰기를 분리하거나 단일 API로 합치기
  • E0499: 동시에 두 &mut 가 필요하면 split_at_mut 등으로 비겹침을 증명하기

추가로 더 많은 케이스(이터레이터 체인, 트리 구조, 그래프, ECS 스타일 데이터 레이아웃)까지 확장하고 싶다면, 위 패턴을 기준으로 코드를 “빌림 생존 범위”와 “비겹침 증명” 관점에서 다시 모델링해보면 대부분 풀립니다.