- Published on
Rust borrow checker E0502 충돌 즉시 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
Rust를 쓰다 보면 가장 자주 마주치는 컴파일 에러 중 하나가 E0502 입니다. 메시지는 대개 비슷합니다. cannot borrow ... as mutable because it is also borrowed as immutable.
핵심은 단순합니다. 같은 값에 대해 불변 참조(&T)가 살아있는 동안 가변 참조(&mut T)를 만들 수 없다는 규칙을 어겼을 때 E0502가 발생합니다. 문제는 실제 코드에서는 이 “살아있다”의 범위가 생각보다 넓게 잡힐 수 있다는 점입니다. 특히 반복문, 클로저, 반환값, 그리고 NLL(Non-Lexical Lifetimes)로도 해결되지 않는 케이스에서 당황하기 쉽습니다.
이 글에서는 E0502를 바로 고치는 실전 패턴을 예제 중심으로 정리합니다. 더 넓게 E0499까지 함께 정리한 글은 Rust E0502/E0499 빌림 충돌 에러 한방 해결도 참고하면 좋습니다.
E0502 에러 메시지 10초 해석법
에러는 보통 아래 3가지를 알려줍니다.
- 어디서 불변 빌림이 시작됐는지
- 어디서 가변 빌림을 시도했는지
- 불변 빌림이 어디까지 살아있다고 보는지
예시(형태만 보세요):
immutable borrow occurs heremutable borrow occurs hereimmutable borrow later used here
세 번째 줄이 특히 중요합니다. 컴파일러가 “아직 불변 참조가 나중에 쓰일 예정이니, 그 사이에 가변 참조를 만들면 안 된다”고 판단한 지점입니다.
패턴 1: 불변 참조의 스코프를 강제로 끝내기
가장 빠른 해결책은 불변 빌림을 더 빨리 끝내는 것입니다.
문제 코드
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // E0502
println!("{}", first);
}
first가 println!에서 사용되므로, 컴파일러는 first의 생존 범위를 push 이후까지로 봅니다.
해결 1: 값 복사(또는 클론)로 참조를 끊기
fn main() {
let mut v = vec![1, 2, 3];
let first = v[0]; // i32는 Copy
v.push(4);
println!("{}", first);
}
Copy 타입이면 이게 가장 깔끔합니다. String 같은 타입이면 clone()이 필요할 수 있습니다.
해결 2: 스코프 블록으로 참조 수명 제한
fn main() {
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first);
} // 여기서 first drop
v.push(4);
}
println!을 먼저 실행해 불변 빌림을 소멸시키는 식으로 순서를 바꾸는 것도 같은 원리입니다.
패턴 2: “읽고-쓰고”를 2단계로 분리하기
E0502는 보통 한 함수 안에서 “읽기 위해 불변 빌림”을 만든 뒤, 같은 컨테이너를 “수정하기 위해 가변 빌림”하려다 발생합니다.
문제 코드: 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");
*m.get_mut("a").unwrap() += 1; // E0502
println!("{:?}", v);
}
해결: 먼저 필요한 정보만 뽑아두고(복사), 그 다음 수정
use std::collections::HashMap;
fn main() {
let mut m: HashMap<String, i32> = HashMap::new();
m.insert("a".to_string(), 1);
let before = *m.get("a").unwrap(); // i32 Copy
*m.get_mut("a").unwrap() += 1;
println!("before={}", before);
}
이 패턴은 데이터가 크면 clone() 비용이 생길 수 있으니, “정말 필요한 최소 데이터만” 복사하는 식으로 설계하는 게 좋습니다.
패턴 3: split_at_mut로 슬라이스를 안전하게 쪼개기
벡터의 서로 다른 원소를 동시에 만지려다 E0502가 자주 납니다.
문제 코드: 같은 벡터에서 읽고 쓰기
fn main() {
let mut v = vec![10, 20, 30, 40];
let a = &v[0];
let b = &mut v[1]; // E0502
*b += *a;
}
해결: split_at_mut로 컴파일러가 “겹치지 않는다”는 걸 알게 하기
fn main() {
let mut v = vec![10, 20, 30, 40];
let (left, right) = v.split_at_mut(1);
let a = left[0]; // Copy로 값만 가져오기
let b = &mut right[0];
*b += a;
}
split_at_mut(n)은 슬라이스를 0..n과 n..으로 나누며, 두 조각이 절대 겹치지 않음을 Rust가 보장합니다.
패턴 4: get과 get_mut을 같은 스코프에서 섞지 말기
특히 컬렉션에서 아래 형태가 흔합니다.
let x = coll.get(key).unwrap();- 그 다음
coll.get_mut(key)또는coll.insert(...)
해결책은 대부분 “불변 참조를 오래 들고 있지 말라”로 귀결됩니다.
해결 옵션
Copy면 값으로 빼기clone()으로 소유권을 가져오기- 불변 참조 사용을 먼저 끝내고, 수정은 뒤로 미루기
- 로직을 함수로 분리해 스코프 자체를 끊기
함수 분리 예시:
use std::collections::HashMap;
fn read_value(m: &HashMap<String, i32>) -> i32 {
*m.get("a").unwrap()
}
fn main() {
let mut m = HashMap::new();
m.insert("a".to_string(), 1);
let before = read_value(&m);
*m.get_mut("a").unwrap() += 1;
println!("{}", before);
}
패턴 5: 반복문에서 “현재 원소 참조”를 잡은 채로 컬렉션 수정하지 않기
반복 중에 push, remove, insert 같은 구조 변경을 하면 거의 항상 빌림이 꼬입니다.
문제 코드
fn main() {
let mut v = vec![1, 2, 3];
for x in &v {
if *x == 2 {
v.push(4); // E0502
}
}
}
해결 1: 인덱스 기반으로 두 단계 처리
fn main() {
let mut v = vec![1, 2, 3];
let mut need_push = false;
for x in &v {
if *x == 2 {
need_push = true;
}
}
if need_push {
v.push(4);
}
}
해결 2: 새 벡터를 만들어 결과를 구성(함수형 스타일)
fn main() {
let v = vec![1, 2, 3];
let mut out = Vec::with_capacity(v.len() + 1);
for &x in &v {
out.push(x);
if x == 2 {
out.push(4);
}
}
println!("{:?}", out);
}
원본을 수정하는 대신 결과를 재구성하면 빌림 충돌이 사라지고 로직도 명확해지는 경우가 많습니다.
패턴 6: 클로저가 참조를 “잡고” 있어서 생기는 E0502
클로저가 환경을 캡처하면 참조 수명이 예상보다 길어질 수 있습니다.
문제 코드
fn main() {
let mut v = vec![1, 2, 3];
let show = || println!("{}", v[0]);
v.push(4); // E0502: show가 v를 불변으로 캡처
show();
}
해결: 필요한 값만 캡처하도록 변경
fn main() {
let mut v = vec![1, 2, 3];
let first = v[0];
let show = move || println!("{}", first);
v.push(4);
show();
}
또는 클로저를 더 늦게 만들거나, 클로저 사용을 먼저 끝낸 뒤 수정하는 방식도 가능합니다.
패턴 7: 정말 필요할 때만 내부 가변성(RefCell, RwLock) 사용
설계상 “여러 곳에서 읽다가 특정 시점에만 쓰기”가 필요하고, 빌림 규칙을 정적으로 맞추기 어려운 경우가 있습니다. 이때 선택지가 내부 가변성입니다.
- 단일 스레드:
std::cell::RefCell - 멀티 스레드:
std::sync::RwLock,Mutex
RefCell 예시(런타임에 빌림 검사):
use std::cell::RefCell;
fn main() {
let v = RefCell::new(vec![1, 2, 3]);
{
let first = v.borrow()[0];
println!("{}", first);
}
v.borrow_mut().push(4);
println!("{:?}", v.borrow());
}
주의할 점은, RefCell은 규칙을 없애는 게 아니라 컴파일 타임에서 런타임으로 미루는 것이라서 잘못 쓰면 패닉이 날 수 있습니다. 즉, “즉시 해결”에는 좋지만, 남용하면 유지보수성이 떨어질 수 있습니다.
체크리스트: E0502를 가장 빨리 없애는 순서
- 불변 참조를 오래 들고 있지 않은가(출력, 반환, 클로저 캡처 포함)
- 필요한 값만
Copy또는clone()으로 빼서 참조를 끊을 수 있는가 - 스코프 블록 또는 함수 분리로 수명을 짧게 만들 수 있는가
- 컬렉션의 서로 다른 영역이면
split_at_mut같은 분해 API를 쓸 수 있는가 - 반복 중 구조 변경이면 “2단계 처리” 또는 “새 컨테이너 생성”으로 바꿀 수 있는가
- 마지막 수단으로 내부 가변성(
RefCell/RwLock)이 설계적으로 타당한가
마무리
E0502는 “Rust가 불편해서”가 아니라, 동시에 읽기와 쓰기가 섞일 때 생기는 실제 버그 가능성을 컴파일 타임에 잘라내는 장치입니다. 해결은 대개 스코프를 줄이거나(참조 수명 단축), 읽기와 쓰기를 분리하거나(2단계), 혹은 데이터 구조를 분해해(겹치지 않음을 증명) 컴파일러가 안전을 확인할 수 있게 만드는 방향으로 갑니다.
추가로 E0499까지 포함해 자주 터지는 패턴을 더 모아둔 정리글은 Rust E0502/E0499 빌림 충돌 에러 한방 해결에서 이어서 보면 좋습니다.