- Published on
Rust E0502/E0507 한방에 끝내는 소유권·빌림
- Authors
- Name
- 스타차일드
- https://x.com/ETFBITX
서버 장애를 보면 로그 한 줄이 원인을 말해주듯, Rust에선 E0502/E0507이 소유권과 빌림의 충돌 지점을 정확히 찍어줍니다. 문제는 메시지를 읽고도 "그래서 뭘 어떻게 바꾸라는 거지"에서 막힌다는 점입니다. 이 글은 E0502(가변/불변 빌림 충돌)와 E0507(빌린 값에서 이동)만 집중해서, 어떤 코드가 왜 깨지고 어떻게 고치면 되는지 패턴으로 정리합니다.
실무 디버깅 관점의 글이 필요하다면, 비슷하게 "증상-원인-해결"로 파고드는 글로 systemd 서비스 재시작 루프 진단 - 로그·유닛·쉘도 함께 참고하면 접근 방식이 비슷해서 도움이 됩니다.
E0502: 불변으로 빌려놓고 가변으로 또 빌릴 수 없다
에러 요약
- 이미
&T(불변 참조)로 빌린 값이 살아있는 동안 - 같은 값을
&mut T(가변 참조)로 빌리려 하면 - 컴파일러가 데이터 레이스 가능성을 차단하며 E0502를 냅니다.
Rust의 핵심 규칙을 한 줄로 줄이면 다음입니다.
- 같은 스코프에서
&T는 여러 개 가능 &mut T는 오직 하나만 가능&T와&mut T는 동시에 공존 불가
흔한 실패 예제 1: 불변 참조를 잡아둔 채로 push
fn main() {
let mut v = vec![1, 2, 3];
let first = &v[0];
v.push(4); // E0502
println!("{}", first);
}
왜 실패하나
first는 v 내부를 가리키는 불변 참조입니다. 그런데 v.push(4)는 벡터의 재할당(reallocation)을 일으킬 수 있고, 그러면 first가 가리키던 주소가 무효가 될 수 있습니다. Rust는 이 가능성 자체를 금지합니다.
해결 1: 참조의 생존 범위를 줄이기
fn main() {
let mut v = vec![1, 2, 3];
{
let first = &v[0];
println!("{}", first);
}
v.push(4);
}
핵심은 "불변 참조가 더 이상 필요 없을 때 빨리 떨어뜨리기"입니다. 중괄호 블록은 가장 단순한 방법입니다.
해결 2: 값 복사(또는 복제)로 참조 자체를 없애기
fn main() {
let mut v = vec![1, 2, 3];
let first = v[0]; // i32는 Copy
v.push(4);
println!("{}", first);
}
Copy 타입이면 참조 대신 값을 복사해두는 편이 가장 깔끔합니다. 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);
}
clone()은 비용이 있으니, 데이터 크기와 호출 빈도를 보고 판단하세요.
흔한 실패 예제 2: 불변으로 읽고, 같은 스코프에서 가변으로 수정
fn main() {
let mut s = String::from("hello");
let len = s.len();
let r = &s;
s.push('!'); // E0502
println!("{} {}", len, r);
}
해결: 읽기와 쓰기를 단계로 분리
fn main() {
let mut s = String::from("hello");
let len = s.len();
s.push('!');
let r = &s;
println!("{} {}", len, r);
}
"읽기 단계"와 "쓰기 단계"를 분리하면 대부분의 E0502는 사라집니다. Rust는 특히 "참조가 살아있는 기간"을 기준으로 판단하므로, 변수 선언 순서와 스코프가 곧 해결책인 경우가 많습니다.
실전 패턴: iterator와 collect로 빌림 충돌을 없애기
다음처럼 한 컬렉션을 순회하면서 동시에 수정하려 하면 E0502가 자주 납니다.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
for x in v.iter() {
if *x % 2 == 0 {
v.push(*x); // E0502
}
}
}
해결은 "읽기 대상"과 "쓰기 대상"을 분리하는 것입니다.
fn main() {
let mut v = vec![1, 2, 3, 4, 5];
let to_add: Vec<i32> = v.iter().copied().filter(|x| x % 2 == 0).collect();
v.extend(to_add);
println!("{:?}", v);
}
이 패턴은 성능도 예측 가능하고, 코드 리뷰에서도 의도가 명확합니다.
E0507: 빌린 값에서 move 할 수 없다
에러 요약
- 어떤 값을
&T또는&mut T로 "빌려" 쓰는 중인데 - 그 값을 소유권 이동(move)하려고 하면
- Rust가 "너 그거 소유자 아니잖아"라며 E0507을 냅니다.
흔한 실패 예제 1: &T에서 String을 꺼내 move
fn take(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hi");
let r = &s;
take(*r); // E0507
}
*r는 String 값을 "복사"가 아니라 "이동"하려는 시도입니다. String은 Copy가 아니므로, 소유권이 필요합니다.
해결 1: 참조로 받도록 함수 시그니처 변경
fn take_ref(s: &str) {
println!("{}", s);
}
fn main() {
let s = String::from("hi");
take_ref(&s);
}
가능하다면 가장 좋은 해결은 "소유권을 요구하지 않게" API를 바꾸는 것입니다. 특히 출력, 조회, 검증 같은 읽기 전용 로직은 &str, &T로 받는 편이 좋습니다.
해결 2: clone()으로 명시적으로 소유권을 만들어 move
fn take(s: String) {
println!("{}", s);
}
fn main() {
let s = String::from("hi");
let r = &s;
take(r.clone());
}
복제가 의도에 맞고 비용이 감당 가능할 때 쓰는 해결책입니다. 중요한 건 "move가 필요해서 clone을 한다"는 의도를 코드로 드러낸다는 점입니다.
흔한 실패 예제 2: &mut Option<T>에서 T를 꺼내 move
아래 코드는 Option 안의 값을 꺼내서 쓰고 싶을 때 자주 시도합니다.
fn main() {
let mut opt = Some(String::from("token"));
let r = &mut opt;
let v = r.unwrap(); // E0507
println!("{}", v);
}
unwrap()은 Option<T>를 소비(consuming)합니다. 그런데 지금 가진 건 &mut Option<T>라서 소비할 수 없습니다.
해결 1: take()로 안전하게 빼기
fn main() {
let mut opt = Some(String::from("token"));
let v = opt.take().unwrap();
// opt는 이제 None
println!("{}", v);
println!("{:?}", opt);
}
take()는 내부 값을 빼고 자리를 None으로 남깁니다. "자리 비우고 가져가기"가 의도라면 정답에 가깝습니다.
해결 2: as_ref() 또는 as_mut()로 참조로만 다루기
값을 "꺼내서 소유"할 필요가 없고, 읽기만 하면 된다면 참조로 바꾸세요.
fn main() {
let opt = Some(String::from("token"));
let v: &String = opt.as_ref().unwrap();
println!("{}", v);
}
흔한 실패 예제 3: 구조체 필드를 빌린 상태에서 필드를 move
#[derive(Debug)]
struct User {
name: String,
email: String,
}
fn main() {
let mut u = User {
name: String::from("a"),
email: String::from("a@example.com"),
};
let name_ref = &u.name;
let email = u.email; // E0507 또는 관련 move 에러
println!("{} {}", name_ref, email);
}
이 상황은 "한 필드를 참조로 잡아둔 채" 다른 필드를 move 하려 해서 구조체 전체가 부분적으로 이동(partial move)되면서 꼬이는 전형적인 케이스입니다.
해결 1: 스코프를 분리해 참조를 먼저 끝내기
#[derive(Debug)]
struct User {
name: String,
email: String,
}
fn main() {
let mut u = User {
name: String::from("a"),
email: String::from("a@example.com"),
};
{
let name_ref = &u.name;
println!("{}", name_ref);
}
let email = std::mem::take(&mut u.email);
println!("{}", email);
}
여기서 std::mem::take는 해당 필드를 기본값으로 대체하고(이 경우 빈 문자열), 원래 값을 소유권으로 가져옵니다.
해결 2: 구조를 바꿔서 소유권 이동을 더 명시적으로
필드를 자주 "꺼내" 써야 한다면, 애초에 필드를 Option<String>으로 두고 take()를 쓰는 설계가 더 자연스러운 경우가 많습니다.
#[derive(Debug)]
struct User {
name: String,
email: Option<String>,
}
fn main() {
let mut u = User {
name: String::from("a"),
email: Some(String::from("a@example.com")),
};
let email = u.email.take().unwrap();
println!("{}", email);
println!("{:?}", u);
}
E0502/E0507을 빠르게 푸는 체크리스트
1) "참조가 살아있는 범위"부터 줄여라
- 중괄호 블록으로 스코프를 분리
println!같은 사용 지점을 앞당겨 참조를 빨리 소멸- 불변 참조를 잡아둔 채로
push,insert,remove,retain같은 가변 작업을 하지 않기
2) "소유권이 필요한가"를 먼저 결정하라
- 읽기 전용이면
&T,&str로 API를 바꾸는 것이 최선 - 정말 소유권이 필요하면
clone()을 의도적으로 사용 - 컨테이너에서 꺼내야 하면
take(),mem::take,mem::replace를 우선 검토
3) 읽기와 쓰기를 두 단계로 나눠라
- 1단계: 필요한 데이터만 추출(
collect,map,filter) - 2단계: 원본을 변경(
extend,push,splice)
이 방식은 대규모 데이터 처리에서도 디버깅이 쉽고, 장애 대응처럼 "원인 격리"가 잘 됩니다. 비슷한 사고방식으로 병목을 줄이는 튜닝 사례는 Rust+Qdrant RAG - HNSW 튜닝으로 지연 50%↓도 참고할 만합니다.
자주 쓰는 해결 도구 모음
Option<T>: as_ref, as_mut, take
fn main() {
let mut opt = Some(String::from("x"));
// 빌려서 보기
let r: &String = opt.as_ref().unwrap();
println!("{}", r);
// 소유권으로 꺼내기
let v: String = opt.take().unwrap();
println!("{}", v);
println!("{:?}", opt);
}
std::mem::take와 std::mem::replace
#[derive(Debug)]
struct State {
buf: String,
}
fn main() {
let mut s = State { buf: String::from("payload") };
let owned = std::mem::take(&mut s.buf);
// s.buf는 이제 빈 문자열
println!("owned={}", owned);
println!("state={:?}", s);
}
split_at_mut: 슬라이스를 안전하게 둘로 쪼개 가변 참조 2개 만들기
E0502가 날 때 "같은 배열을 두 군데에서 동시에 &mut로 쓰고 싶다"는 요구가 숨어있는 경우가 많습니다. 이때는 인덱스 기반으로 안전 분할을 하세요.
fn main() {
let mut a = [10, 20, 30, 40];
let (left, right) = a.split_at_mut(2);
left[0] += 1;
right[0] += 2;
println!("{:?}", a);
}
마무리: 에러를 없애는 게 아니라 설계를 정렬하는 과정
E0502는 "동시에 읽고 쓰려는" 설계 충돌을, E0507은 "소유권 없이 이동하려는" 설계 충돌을 드러냅니다. 그래서 해결책은 대개 문법 트릭이 아니라 다음 중 하나로 귀결됩니다.
- 참조 수명을 줄여서 충돌 구간을 없앤다
- 소유권이 필요한지 재정의하고 API를 바꾼다
- 컨테이너에서 값을 꺼내는 정식 도구(
take,mem::take,replace)를 쓴다 - 읽기 단계와 쓰기 단계를 분리해 데이터 흐름을 단순화한다
이 네 가지를 습관처럼 적용하면 E0502/E0507은 "공포의 벽"이 아니라 "설계 가이드"가 됩니다.