- Published on
Rust 빌림체커 E0502/E0506 5분 해결법
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버나 CLI를 Rust로 짜다 보면, 컴파일러가 “너 지금 동시에 빌렸어” 혹은 “빌린 상태에서 값을 바꾸려 했어”라고 단호하게 막는 순간이 옵니다. 그 대표가 E0502(immutable과 mutable 동시 빌림)와 E0506(빌린 값을 대입으로 덮어씀)입니다.
이 글은 원리를 길게 설명하기보다, 5분 안에 고치는 패턴을 중심으로 정리합니다. 에러 메시지에 적힌 “borrowed here / later used here”를 어떻게 읽고, 어떤 리팩터링을 적용하면 되는지에 집중합니다.
관련해서 동시성 코드에서 Mutex나 spawn을 잘못 쓰면 비슷한 체감의 “막힘”이 생기는데, Tokio 쪽 이슈는 별도 글인 Rust Tokio join! 교착? spawn·Mutex 오용 해결도 참고하면 좋습니다.
0분 진단: E0502/E0506 한 줄 요약
E0502: 불변 참조(&T)가 살아있는 동안 같은 값을 가변 참조(&mut T)로 빌리려 함E0506: 어떤 값이 참조로 빌려져 살아있는 동안, 그 값에 대입(재할당)해서 덮어씀
둘 다 핵심은 같습니다.
- “참조가 살아있는 스코프”가 생각보다 길다
- “참조를 만든 뒤에” 그 참조가 마지막으로 사용되는 지점까지가 생존 범위다
이제부터는 가장 흔한 케이스별로 “바로 고치는 레시피”를 봅니다.
1) E0502: 읽고(&) 나서 쓰려고(&mut) 할 때
재현 코드
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4);
println!("{}", first);
}
대략 이런 형태로 E0502가 납니다. 이유는 first가 &v[0]을 가리키는 동안 push가 벡터 재할당을 일으킬 수 있어서(메모리 이동 가능) 안전하지 않기 때문입니다.
5분 해결법 A: 값을 복사하거나 클론해서 “참조 수명”을 끊기
Copy 타입이면 가장 빠릅니다.
fn main() {
let mut v = vec![1, 2, 3];
let first = v[0]; // i32는 Copy
v.push(4);
println!("{}", first);
}
String 같은 비 Copy 타입이면 clone으로 끊습니다.
fn main() {
let mut v = vec![String::from("a"), String::from("b")];
let first = v[0].clone();
v.push(String::from("c"));
println!("{}", first);
}
트레이드오프는 복사 비용이지만, “일단 컴파일”이 목표라면 가장 단순합니다.
5분 해결법 B: 스코프를 줄여 참조가 빨리 죽게 만들기
참조를 만든 뒤 바로 쓰고, 그 다음에 변경하면 됩니다.
fn main() {
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first);
} // 여기서 first가 drop
v.push(4);
}
이 패턴은 특히 “로그 찍으려고 잠깐 빌린 것” 때문에 막힐 때 유용합니다.
5분 해결법 C: 불변/가변 빌림을 분리하는 API로 바꾸기
같은 컨테이너를 동시에 다루는 경우, 표준 라이브러리의 “분할 빌림” API를 쓰면 해결됩니다.
예: split_at_mut로 슬라이스를 안전하게 두 조각으로 나누기
fn main() {
let mut a = [10, 20, 30, 40];
let (left, right) = a.split_at_mut(2);
left[0] += 1;
right[0] += 1;
println!("{:?}", a);
}
left와 right는 겹치지 않으니 동시에 &mut가 가능합니다.
예: HashMap::get_mut과 HashMap::get을 섞지 말고 흐름을 바꾸기
get으로 불변 참조를 잡아둔 채 insert나 get_mut을 하려 하면 E0502가 자주 납니다. 이때는 entry API로 “한 번에” 처리합니다.
use std::collections::HashMap;
fn main() {
let mut m: HashMap<String, i32> = HashMap::new();
let key = "k".to_string();
*m.entry(key).or_insert(0) += 1;
}
핵심은 “읽기와 쓰기를 분리하지 말고, 원자적 흐름으로 합치기”입니다.
2) E0506: 빌린 상태에서 값을 덮어쓰는 패턴
재현 코드
fn main() {
let mut s = String::from("hello");
let r = &s;
s = String::from("world");
println!("{}", r);
}
s를 r이 빌린 상태에서 s = ...로 아예 다른 String으로 교체하려 하니 E0506이 납니다.
5분 해결법 A: 대입을 참조 사용 이후로 옮기기
가장 단순한 해결은 “순서 바꾸기”입니다.
fn main() {
let mut s = String::from("hello");
let r = &s;
println!("{}", r);
s = String::from("world");
println!("{}", s);
}
5분 해결법 B: 덮어쓰기 대신 내부를 수정하는 메서드 사용
대입이 아니라 clear, push_str, replace_range 같은 “in-place 변경”을 쓰면, 참조와 충돌하는 지점을 줄일 수 있습니다(물론 여전히 &mut가 필요하면 스코프 조정이 필요합니다).
fn main() {
let mut s = String::from("hello");
// r을 오래 들고 있지 않게 먼저 사용
println!("{}", &s);
s.clear();
s.push_str("world");
println!("{}", s);
}
5분 해결법 C: std::mem::take 또는 std::mem::replace로 소유권을 분리
“기존 값을 꺼내서 다른 곳에 쓰고, 원래 변수엔 새 값을 넣고 싶다”면 take나 replace가 깔끔합니다.
fn main() {
let mut s = String::from("hello");
let old = std::mem::take(&mut s); // s는 빈 문자열로 바뀜
// 여기서 old를 마음껏 사용
let _len = old.len();
s = String::from("world");
println!("{}", s);
}
이 패턴은 구조체 필드를 빼서 가공한 뒤 다시 넣을 때도 자주 씁니다.
3) “참조가 생각보다 오래 살아있다”를 잡는 법
E0502/E0506을 자주 내는 진짜 원인은 문법이 아니라 수명 범위 착각입니다.
체크리스트
- 참조를 만든 줄과, 그 참조를 “마지막으로 사용한 줄”을 찾기
- 그 사이에
push,insert, 대입,swap,sort같은 “구조 변경”이 있는지 보기 - 참조를 더 짧게 만들 수 있는지(스코프 블록, 즉시 사용, 값 복사) 판단
특히 Rust는 NLL(Non-Lexical Lifetimes) 덕분에 “블록 끝까지”가 아니라 “마지막 사용 지점까지”로 수명이 줄어들긴 하지만, 다음 같은 경우는 여전히 길어집니다.
- 참조가
println!같은 매크로 인자로 넘어가며 사용 지점이 뒤로 밀림 - 이터레이터 체인에서 참조가 클로저에 캡처되어 생존 범위가 늘어남
- 구조체에 참조를 저장해 두어 스코프가 함수 전체로 확장됨
4) 실전 패턴: 반복문에서 E0502가 나는 경우
가장 흔한 실전 케이스는 “순회하면서 수정”입니다.
문제 코드: 인덱스로 읽고, 같은 벡터를 수정
fn main() {
let mut v = vec![1, 2, 3, 4];
for i in 0..v.len() {
let x = &v[i];
if *x % 2 == 0 {
v.push(*x); // E0502 가능
}
}
}
해결법 A: 먼저 수집하고 나중에 반영(2-phase update)
fn main() {
let mut v = vec![1, 2, 3, 4];
let mut to_add = Vec::new();
for &x in v.iter() {
if x % 2 == 0 {
to_add.push(x);
}
}
v.extend(to_add);
println!("{:?}", v);
}
이 방식은 빌림체커를 “속이는” 게 아니라, 데이터 흐름을 더 안전하게 만드는 정석입니다.
해결법 B: retain/drain_filter 계열로 의도를 API에 맡기기
필터링/삭제 같은 작업은 전용 API가 빌림 규칙을 만족하도록 설계되어 있습니다.
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];
v.retain(|x| x % 2 == 0);
println!("{:?}", v);
}
5) 실전 패턴: 구조체 메서드에서 self를 빌려놓고 self를 또 수정
self.field를 불변으로 빌린 다음 self.other_field를 수정하려다 E0502가 나는 경우가 많습니다.
문제 코드
struct App {
name: String,
count: usize,
}
impl App {
fn bump_if_named(&mut self) {
let n = &self.name;
if n == "admin" {
self.count += 1; // E0502가 날 수 있는 전형
}
}
}
해결법: 필요한 값만 미리 복사하거나, 비교를 즉시 끝내기
struct App {
name: String,
count: usize,
}
impl App {
fn bump_if_named(&mut self) {
let is_admin = self.name == "admin"; // 여기서 비교 끝
if is_admin {
self.count += 1;
}
}
}
비교에 필요한 정보만 뽑아 “불변 빌림”을 짧게 끝내는 게 포인트입니다.
6) 빠른 결론: 5분 안에 적용하는 우선순위
- 순서 변경: 참조 사용을 먼저 끝내고 변경은 나중에
- 스코프 축소:
{ ... }블록으로 참조 수명 단축 - 값 복사/클론: 참조 대신 소유 값으로 끊기
- 2-phase update: 먼저 읽어 수집, 그 다음에 한 번에 수정
- 전용 API 사용:
entry,split_at_mut,retain,extend등으로 의도를 표현
E0502/E0506은 “Rust가 불친절해서”가 아니라, 실제로 위험한 메모리 상황을 컴파일 타임에 차단하는 신호입니다. 위 패턴들 중 하나로 코드를 정리하면, 대개 성능과 가독성도 함께 좋아집니다.
동시성 코드에서 빌림 문제와 함께 락 범위가 길어져 교착이나 병목이 생기는 경우도 많으니, Tokio 기반이라면 앞서 언급한 Rust Tokio join! 교착? spawn·Mutex 오용 해결도 같이 보면 문제를 더 빨리 분리할 수 있습니다.