- Published on
Rust 소유권 충돌 E0502/E0499 즉시 해결
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 CLI를 Rust로 만들다 보면, 컴파일 에러 중 체감 빈도가 가장 높은 축이 E0502와 E0499입니다. 둘 다 핵심은 같습니다. 같은 데이터에 대해 동시에 성립할 수 없는 빌림 규칙을 코드가 요구할 때 발생합니다.
E0502: 불변 빌림이 살아있는 동안 가변 빌림을 시도(또는 그 반대 맥락)E0499: 같은 스코프에서 가변 빌림을 2개 이상 동시에 유지
이 글은 "원리 설명"보다 즉시 고치는 방법에 집중합니다. 패턴별로 "왜 에러가 나는지"와 "어떻게 구조를 바꾸면 되는지"를 짧은 규칙과 코드로 정리합니다.
참고로 더 많은 패턴을 모아둔 글은 아래 링크도 함께 보세요.
- Rust 소유권 에러 E0502·E0499 한방 해결 패턴
- 반복/루프 구조를 바꾸면서 빌림 충돌을 줄이는 관점은 Rust Iterator로 for 루프 제거·성능 최적화도 도움이 됩니다.
에러를 10초 안에 판별하는 체크리스트
E0502가 나면
- 먼저
let x = &something;같은 불변 참조가 살아있는 범위를 찾습니다. - 그 범위 안에서
something을&mut로 잡거나 수정하려는 코드가 있는지 봅니다. - 해결은 보통 둘 중 하나입니다.
- 불변 참조가 더 빨리 drop되도록 스코프를 줄인다
- 필요한 값만 복사/클론해서 참조를 오래 잡지 않는다
E0499가 나면
- 같은 값(예:
vec,self,map)에서&mut가 두 번 이상 생깁니다. - 해결은 보통 셋 중 하나입니다.
- "동시에"가 아니라 "순차적으로" 되도록 스코프/로직을 바꾼다
- 서로 다른 영역임을 컴파일러가 알 수 있게
split_at_mut같은 API를 쓴다 - 구조를 바꿔서 한 번만
&mut를 잡고 그 안에서 처리한다
패턴 1: 스코프를 잘라서 불변 빌림을 먼저 끝내기 (E0502)
가장 흔한 상황입니다. 불변 참조를 잡아놓고, 같은 값에 가변 접근을 하려다 터집니다.
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // E0502: `v` is borrowed as immutable
println!("{}", first);
}
해결은 first를 오래 들고 있지 않게 만드는 것입니다.
해결 1) 값만 복사해서 들고 있기
fn main() {
let mut v = vec![1, 2, 3];
let first = v[0]; // i32는 Copy
v.push(4);
println!("{}", first);
}
해결 2) 불변 참조 사용 범위를 블록으로 제한
fn main() {
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first);
} // 여기서 first drop
v.push(4);
}
실전 팁: "로그 찍으려고 참조 잡았다가" 에러가 나는 경우가 많습니다. 출력/검증 코드를 블록으로 감싸는 것만으로 해결되는 케이스가 많습니다.
패턴 2: &mut self를 잡은 상태에서 self를 또 참조하기 (E0502/E0499)
메서드 내부에서 특히 자주 터집니다.
struct App {
items: Vec<i32>,
}
impl App {
fn add_and_report(&mut self) {
let first = self.items.first();
self.items.push(10); // E0502
println!("{:?}", first);
}
}
해결: 필요한 값만 먼저 뽑아두고, 참조를 오래 유지하지 않기
impl App {
fn add_and_report(&mut self) {
let first = self.items.first().copied();
self.items.push(10);
println!("{:?}", first);
}
}
copied가 안 되는 타입이면 cloned를 고려합니다. 비용이 부담되면, 로그/검증을 앞에서 끝내거나 스코프를 분리하세요.
패턴 3: 인덱스로 같은 벡터를 두 번 &mut로 잡기 (E0499)
아래 코드는 논리적으로는 "서로 다른 원소"를 바꾸는 것 같지만, Rust는 v[a]와 v[b]가 겹칠 수 있음을 컴파일 타임에 완전히 배제하지 못하면 막습니다.
fn swap_bad(v: &mut Vec<i32>, a: usize, b: usize) {
let x = &mut v[a];
let y = &mut v[b]; // E0499
std::mem::swap(x, y);
}
해결 1) 표준 라이브러리 swap 사용
가장 간단하고 안전합니다.
fn swap_ok(v: &mut Vec<i32>, a: usize, b: usize) {
v.swap(a, b);
}
해결 2) split_at_mut로 "서로 다른 영역"을 증명
fn swap_ok(v: &mut [i32], a: usize, b: usize) {
assert!(a != b);
let (i, j) = if a < b { (a, b) } else { (b, a) };
let (left, right) = v.split_at_mut(j);
let x = &mut left[i];
let y = &mut right[0];
std::mem::swap(x, y);
}
split_at_mut는 "두 슬라이스가 겹치지 않는다"를 타입 시스템으로 보장해 주기 때문에, &mut를 2개 만들 수 있습니다.
패턴 4: HashMap에서 get_mut와 insert를 섞다가 터짐 (E0499/E0502)
HashMap을 수정하는 동안 같은 맵을 다시 건드리면, 내부 재해시/리사이즈 가능성 때문에 Rust는 보수적으로 막습니다.
use std::collections::HashMap;
fn bad(map: &mut HashMap<String, i32>, k: String) {
let v = map.get_mut(&k).unwrap();
*v += 1;
map.insert("x".to_string(), 1); // E0499 또는 E0502 계열
}
해결 1) 작업을 "순차"로: 스코프를 분리
use std::collections::HashMap;
fn ok(map: &mut HashMap<String, i32>, k: String) {
{
let v = map.get_mut(&k).unwrap();
*v += 1;
} // 여기서 가변 빌림 종료
map.insert("x".to_string(), 1);
}
해결 2) entry API로 한 번에 처리
use std::collections::HashMap;
fn ok(map: &mut HashMap<String, i32>, k: String) {
*map.entry(k).or_insert(0) += 1;
map.insert("x".to_string(), 1);
}
entry는 "키 조회 + 필요 시 삽입"을 한 번의 가변 접근으로 묶어주기 때문에 빌림 충돌을 줄여줍니다.
패턴 5: 구조체 필드를 동시에 두 번 &mut로 빌리기 (E0499)
아래는 직관적으로 "서로 다른 필드"인데도 실패할 때가 있습니다. 특히 self를 통째로 빌린 상태에서 다시 self를 만지면 터집니다.
struct State {
a: Vec<i32>,
b: Vec<i32>,
}
impl State {
fn bad(&mut self) {
let a = &mut self.a;
let b = &mut self.b; // 상황에 따라 E0499로 이어질 수 있는 구조
a.push(1);
b.push(2);
}
}
대부분의 경우 최신 Rust에서는 위 코드는 통과하지만, 비슷한 형태로 메서드 호출이 섞이거나 self 전체를 캡처하는 클로저가 끼면 쉽게 깨집니다.
해결: "필드 분해"를 먼저 해서 self 빌림을 쪼개기
impl State {
fn ok(&mut self) {
let State { a, b } = self;
a.push(1);
b.push(2);
}
}
이 패턴은 컴파일러가 "서로 다른 필드"임을 더 명확히 추론하도록 돕습니다.
패턴 6: 반복문에서 참조를 쥔 채로 컬렉션을 수정 (E0502)
fn bad(v: &mut Vec<i32>) {
for x in v.iter() {
if *x == 0 {
v.push(1); // E0502
}
}
}
반복자는 내부적으로 v에 대한 불변 빌림을 유지합니다. 그 상태에서 push는 가변 빌림이므로 충돌합니다.
해결 1) 수정 계획을 따로 모아서 2패스로 처리
fn ok(v: &mut Vec<i32>) {
let mut to_add = 0;
for x in v.iter() {
if *x == 0 {
to_add += 1;
}
}
v.extend(std::iter::repeat(1).take(to_add));
}
해결 2) 인덱스 기반으로 순회하며 길이 변화를 통제
fn ok(v: &mut Vec<i32>) {
let mut i = 0;
while i < v.len() {
if v[i] == 0 {
v.push(1);
}
i += 1;
}
}
다만 두 번째 방식은 로직에 따라 무한 루프/예상치 못한 성장 위험이 있어, 가능하면 "계획 수집 후 반영"이 더 안전합니다.
패턴 7: Option/필드 값을 꺼내 쓰고 다시 넣기 (take, replace)
"필드를 잠깐 빼서 작업하고 다시 넣고 싶다"는 요구는 Rust에서 흔합니다. 이때 억지로 참조를 오래 잡으면 E0502/E0499로 이어집니다.
Option::take로 소유권을 이동시키기
#[derive(Default)]
struct Job {
current: Option<Vec<i32>>,
}
impl Job {
fn process(&mut self) {
let mut buf = self.current.take().unwrap_or_default();
buf.push(42);
self.current = Some(buf);
}
}
take는 Option을 None으로 바꾸고, 기존 값을 소유권으로 꺼내옵니다. 참조를 유지하지 않으니 빌림 충돌이 크게 줄어듭니다.
std::mem::replace로 임시 값과 교체
use std::mem;
fn replace_demo(v: &mut Vec<i32>) {
let old = mem::replace(v, Vec::new());
// old를 마음껏 사용
let mut new_v = old;
new_v.push(1);
*v = new_v;
}
디버깅 요령: 컴파일러 메시지를 "수명" 관점으로 읽기
E0502/E0499는 보통 에러 메시지에 다음 정보가 같이 나옵니다.
- 첫 번째 빌림이 어디서 시작했는지
- 두 번째 빌림이 어디서 발생했는지
- 첫 번째 빌림이 어디까지 살아있다고 추론했는지
이때 해결의 핵심은 "첫 번째 빌림의 생존 구간"을 줄이거나, "두 빌림이 겹치지 않음"을 API로 증명하는 것입니다.
실전에서 가장 잘 먹히는 즉시 해결 6가지
- 참조를 오래 잡지 말고, 가능하면
copied/cloned로 값만 들고 있기 - 블록 스코프로 빌림을 강제로 끝내기
- 동시에 두 개의
&mut가 필요하면split_at_mut,swap같은 안전 API 사용 HashMap은entry로 조회와 수정을 합치기- 필드 접근은
let Struct { .. } = self;로 분해해서self전체 빌림을 피하기 - "꺼내서 처리"는
take/replace로 소유권 이동시키기
마무리
E0502와 E0499는 "Rust가 까다롭다"의 상징처럼 보이지만, 사실은 데이터 경쟁과 유스 애프터 프리 같은 버그를 컴파일 타임에 제거하는 장치입니다. 자주 만나는 패턴만 몸에 익히면, 대부분은 스코프를 정리하거나 표준 API로 바꾸는 수준에서 즉시 해결됩니다.
반복문/이터레이터로 구조를 바꿔서 빌림 충돌 자체를 줄이는 접근은 Rust Iterator로 for 루프 제거·성능 최적화도 함께 참고하면 좋습니다.