- Published on
Rust E0502·E0499 소유권 오류 5분 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Rust를 처음(혹은 오랜만에) 쓰면 가장 빨리 마주치는 벽이 빌림 검사기(borrow checker)입니다. 그중에서도 E0502(불변 빌림과 가변 빌림 충돌), E0499(가변 빌림이 동시에 두 번 발생)만 잡아도 체감 난이도가 확 내려갑니다.
이 글은 “왜 안 되는지”를 길게 설명하기보다, 에러를 5분 안에 해결하는 사고 순서와 코드 패턴을 제공합니다. 아래 예제는 모두 바로 복붙해서 cargo run 혹은 cargo test로 확인할 수 있게 구성했습니다.
참고로 이런 류의 문제는 원인이 명확한데도 로그/에러 메시지 때문에 길을 잃기 쉽습니다. CI에서 재현이 어려울 때는 캐시/동시성 같은 환경 요인도 함께 정리해두면 좋습니다. 예: GitHub Actions 캐시로 Node.js CI 2배 빠르게, 실패 디버깅
1) E0502·E0499 한 줄 정의(해석부터)
E0502: immutable borrow + mutable borrow가 겹침
- 이미 어떤 값이 불변으로 빌려진 상태에서
- 같은 값에 대해 가변 빌림을 시도할 때
- 혹은 그 반대 순서로도 동일하게 발생
에러 메시지에서 핵심은 보통 이런 문장입니다.
cannot borrow ... as mutable because it is also borrowed as immutable
E0499: mutable borrow가 동시에 2개
- 어떤 값에 대해 가변 빌림은 동시에 하나만 허용
- 두 번째
&mut가 생기는 순간 E0499
에러 메시지에서 핵심은 보통 이런 문장입니다.
cannot borrow ... as mutable more than once at a time
2) 5분 해결 체크리스트(순서대로 보면 대부분 끝)
- 빌림의 “수명”이 어디까지 이어지는지 먼저 찾기
- Rust는 “변수 스코프 끝”이 아니라 “마지막 사용 지점”까지 빌림이 이어질 수 있습니다(특히 NLL 이전 감각으로 보면 헷갈림).
- 충돌하는 참조가 있다면, 둘 중 하나를 스코프 밖으로 밀어내기
{ ... }블록으로 참조를 빨리 끝내기
- 참조 대신 값을 쓰도록 복사/클론/추출
Copy타입이면 값 복사로 끝- 아니면
clone()혹은to_owned()로 소유권 있는 값 만들기
- 컨테이너를 동시에 만지면, 인덱싱을 줄이고 API를 바꾸기
Vec는split_at_mut같은 안전한 분할 API 사용HashMap은get_mut/entry로 한 번에 처리
- 정말로 “동시 가변 접근”이 필요하면, 내부 가변성(RefCell/Mutex/RwLock) 고려
- 단, 이건 마지막 카드(런타임 비용/패닉/락 비용)
3) E0502 대표 패턴 4가지와 즉시 해결법
패턴 A: 불변 참조를 잡아둔 채로 수정
문제 코드
fn main() {
let mut s = String::from("hello");
let r = &s; // 불변 빌림
s.push('!'); // 가변 빌림 시도 -> E0502
println!("{}", r);
}
해결 1: 불변 참조를 더 빨리 끝내기(스코프 분리)
fn main() {
let mut s = String::from("hello");
{
let r = &s;
println!("{}", r);
} // 여기서 r의 빌림이 끝남
s.push('!');
println!("{}", s);
}
해결 2: 필요한 값만 복사/복제해서 참조를 없애기
fn main() {
let mut s = String::from("hello");
let snapshot = s.clone();
s.push('!');
println!("before: {}, after: {}", snapshot, s);
}
패턴 B: 불변 참조를 만든 뒤 같은 변수에 &mut를 만듦
문제 코드
fn main() {
let mut v = vec![1, 2, 3];
let a = &v[0]; // 불변 빌림
let b = &mut v[1]; // 가변 빌림 -> E0502
*b += 10;
println!("{}", a);
}
해결: 인덱싱 대신 “분할” API 사용
Vec에서 서로 다른 원소를 동시에 가변/불변으로 다루려면, 슬라이스를 분리해 서로 다른 영역임을 컴파일러가 알게 해야 합니다.
fn main() {
let mut v = vec![1, 2, 3];
let (left, right) = v.split_at_mut(1);
let a = left[0]; // 값 복사(i32는 Copy)
let b = &mut right[0]; // 원래 v[1]
*b += 10;
println!("{}", a);
println!("{:?}", v);
}
패턴 C: println!/로그 때문에 빌림이 길어짐
로그가 참조를 잡고 있는 동안, 아래에서 가변 접근을 하면 E0502가 납니다.
문제 코드
fn main() {
let mut s = String::from("hello");
let r = &s;
// 디버깅하다가 아래에서 수정하려고 하면 충돌
// println!("debug: {}", r);
s.push_str(" world");
println!("{}", r);
}
해결: 로그를 먼저 찍고 참조 사용을 끝내기
fn main() {
let mut s = String::from("hello");
{
let r = &s;
println!("debug: {}", r);
}
s.push_str(" world");
println!("{}", s);
}
패턴 D: HashMap에서 get과 get_mut를 섞음
문제 코드
use std::collections::HashMap;
fn main() {
let mut m = HashMap::from([(String::from("a"), 1)]);
let cur = m.get("a"); // 불변 빌림
let cur_mut = m.get_mut("a"); // 가변 빌림 -> E0502
if let Some(v) = cur_mut {
*v += 1;
}
println!("{:?}", cur);
}
해결: entry로 한 번에 처리(가장 깔끔)
use std::collections::HashMap;
fn main() {
let mut m = HashMap::new();
*m.entry(String::from("a")).or_insert(0) += 1;
println!("{:?}", m);
}
4) E0499 대표 패턴 4가지와 즉시 해결법
패턴 A: 같은 값에 &mut를 두 번
문제 코드
fn main() {
let mut x = 0;
let a = &mut x;
let b = &mut x; // E0499
*a += 1;
*b += 1;
}
해결: 한 번에 끝내거나, 값을 합쳐서 한 번만 빌리기
fn main() {
let mut x = 0;
{
let a = &mut x;
*a += 1;
}
{
let b = &mut x;
*b += 1;
}
println!("{}", x);
}
패턴 B: Vec의 두 원소를 동시에 &mut로 잡기
문제 코드
fn main() {
let mut v = vec![10, 20, 30];
let a = &mut v[0];
let b = &mut v[1]; // E0499 (인덱싱은 분리 증명이 안 됨)
*a += 1;
*b += 1;
}
해결: 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;
println!("{:?}", v);
}
패턴 C: 반복문에서 가변 참조를 오래 들고 있음
반복 중에 &mut를 잡고, 같은 컨테이너를 또 접근하려 하면 흔히 E0499가 납니다.
문제 코드
fn main() {
let mut v = vec![1, 2, 3];
for i in 0..v.len() {
let x = &mut v[i];
// 여기서 v를 또 건드리면(예: push) 충돌 가능
// v.push(4);
*x += 1;
}
}
해결: 2단계로 나누기(읽기 단계/쓰기 단계)
fn main() {
let mut v = vec![1, 2, 3];
// 쓰기만 필요하면 iter_mut가 가장 단순
for x in v.iter_mut() {
*x += 1;
}
println!("{:?}", v);
}
패턴 D: 구조체 메서드에서 self를 두 번 가변 대여
문제 코드
struct App {
a: i32,
b: i32,
}
impl App {
fn bump_both(&mut self) {
let x = &mut self.a;
let y = &mut self.b; // E0499 (self가 이미 가변 대여됨)
*x += 1;
*y += 1;
}
}
fn main() {
let mut app = App { a: 0, b: 0 };
app.bump_both();
}
해결: 한 번에 구조 분해로 필드를 분리
struct App {
a: i32,
b: i32,
}
impl App {
fn bump_both(&mut self) {
let App { a, b } = self;
*a += 1;
*b += 1;
}
}
fn main() {
let mut app = App { a: 0, b: 0 };
app.bump_both();
println!("{} {}", app.a, app.b);
}
5) 그래도 안 풀릴 때: “소유권 설계”를 바꾸는 3가지 옵션
옵션 1: 값을 함수 밖으로 빼서 반환으로 합치기
&mut를 여기저기 전달하기보다, 변경 결과를 반환해 합치는 방식이 더 Rust스럽고 안전합니다.
fn add_suffix(mut s: String) -> String {
s.push_str("!");
s
}
fn main() {
let s = String::from("hello");
let s = add_suffix(s);
println!("{}", s);
}
옵션 2: 내부 가변성 RefCell (단일 스레드)
컴파일 타임이 아니라 런타임에 빌림 규칙을 검사합니다. 규칙을 깨면 패닉이 나므로, “정말 필요한 경우만” 쓰는 게 좋습니다.
use std::cell::RefCell;
fn main() {
let v = RefCell::new(vec![1, 2, 3]);
{
let mut r = v.borrow_mut();
r.push(4);
}
println!("{:?}", v.borrow());
}
옵션 3: Mutex/RwLock (멀티 스레드)
공유 상태가 필요하면 락이 정답인 경우도 많습니다. 다만 성능/데드락 설계가 과제가 됩니다.
6) 에러 메시지에서 “딱 여기만” 보면 빨리 끝난다
first borrowed here/borrowed here라인이 첫 빌림 시작점second borrow occurs here라인이 충돌 지점borrow later used here라인이 빌림이 끝나지 않는 이유(마지막 사용)
즉, 해결은 보통 아래 셋 중 하나입니다.
- 마지막 사용을 앞당긴다(로그/출력/참조 사용을 먼저 끝냄)
- 스코프를 쪼갠다(블록으로 참조 수명 단축)
- 동시에 접근하지 않게 API/자료구조 사용법을 바꾼다(
split_at_mut,entry등)
7) 마무리: E0502·E0499는 “버그”가 아니라 설계 힌트
E0502/E0499는 Rust가 괴롭히는 게 아니라, 데이터 경쟁/유효하지 않은 참조로 이어질 수 있는 코드를 미리 차단하는 신호입니다. 위 패턴(스코프 분리, 값 스냅샷, 컨테이너 분할, entry 활용)만 익혀도 대부분의 소유권 오류는 5분 안에 정리됩니다.
성능 튜닝이나 빌드/배포 파이프라인에서 문제를 줄이는 것도 결국 같은 결입니다. 작은 규칙을 자동화하고, 충돌 지점을 줄이면 전체 개발 속도가 올라갑니다. CI 최적화가 필요하다면 GitHub Actions 동시 실행 막힘 해결 - concurrency·cancel-in-progress 같은 글도 함께 참고해두면 도움이 됩니다.