Rust 에러 E0507: cannot move out of a value behind a shared reference (빌린 값 이동)
E0507은 빌리기만 한 것에서 값을 가져가려고(이동시키려고) 했다는 뜻이에요. 빌린 값은 들여다볼 수는 있어도 들고 갈 수는 없어요. 들고 가 버리면 진짜 주인에게 빈자리만 남으니까요. 아주 흔한 에러이고, 특히 Vec<String>, 구조체 필드, Option에서 자주 나와요. 보통은 대여로 바꾸거나 .clone()을 하면 해결돼요. 이 페이지에서는 고치는 방법을 먼저 보여 주고, 그다음에 설명할게요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.
에러 메시지
섹션 제목: “에러 메시지”error[E0507]: cannot move out of `self.name` which is behind a shared reference --> src/main.rs:7:9 |7 | self.name // ❌ `self`가 빌리기만 한 String을 넘겨주려고 해요 | ^^^^^^^^^ move occurs because `self.name` has type `String`, which does not implement the `Copy` trait |help: consider cloning the value if the performance cost is acceptable |7 | self.name.clone() // ❌ `self`가 빌리기만 한 String을 넘겨주려고 해요 | ++++++++“공유 참조자 뒤에 있는 self.name을 밖으로 이동시킬 수 없다”는 뜻이에요. help는 “성능 비용이 괜찮다면 값을 복제해 보라”고 제안해요.
이 에러를 내는 프로그램은 다음과 같아요.
struct User { name: String,}
impl User { fn name(&self) -> String { self.name // ❌ `self`가 빌리기만 한 String을 넘겨주려고 해요 }}
fn main() { let u = User { name: String::from("Ferris") }; println!("{}", u.name());}같은 에러 코드인데 문구가 여러 가지예요. 모두 “빌리기만 했으니 이동시킬 수 없다”는 뜻이에요.
cannot move out of index of `Vec<String>`:let first = names[0];일 때cannot move out of `*r` which is behind a shared reference:r: &String에서let owned = *r;일 때cannot move out of `nick` as enum variant `Some` which is behind a shared reference:match *nick { Some(n) => n, ... }일 때cannot move out of `report`, a captured variable in an `Fn` closure:Fn클로저가 캡처한 변수를 이동시킬 때cannot move out of `self.log` which is behind a mutable reference: 네,&mut여도 그냥 가져갈 수는 없어요cannot move out of an `Rc`:let s: String = *shared;일 때
무슨 뜻인가요
섹션 제목: “무슨 뜻인가요”7번째 줄을 보세요. 메서드는 소유한 값인 String을 반환해요. 그러니 self.name은 문자열을 구조체 밖으로 이동시켜서 호출한 쪽에 넘겨주게 돼요. 그런데 메서드가 가진 건 &self, 즉 사용자에 대한 공유(읽기 전용) 대여뿐이에요. 사용자를 소유하지 않으니, 사용자의 이름을 남에게 줄 수도 없어요.
move occurs because ... does not implement the Copy trait(Copy 트레이트를 구현하지 않아서 이동이 일어남) 부분도 중요해요. 필드가 u32였다면 Rust가 알아서 복사해 줘서 에러가 나지 않았을 거예요. String은 힙 메모리를 소유하기 때문에 복사가 아니라 이동만 할 수 있어요.
친구의 책장을 오후 동안 빌렸다고 생각해 보세요. 책장에 있는 책은 무엇이든 읽을 수 있어요. 하지만 책을 집에 가져갈 수는 없어요. 책장은 내 것이 아니고, 친구가 돌아오면 빈칸만 보게 될 테니까요.
Rust가 이렇게 하는 이유
섹션 제목: “Rust가 이렇게 하는 이유”모든 값에는 주인이 딱 하나 있고, 주인이 사라질 때 값을 정리해요(소유권 참고). 빌린 User에서 self.name을 꺼내 갈 수 있다면, 그 사용자는 아직도 자기가 그 문자열을 소유한다고 생각할 거예요. 나중에 사용자가 해제되면 그 문자열을 한 번 더 해제하거나, 누군가 이미 없는 필드를 읽게 돼요. 이게 “해제 후 사용”(use after free)이나 “이중 해제”(double free)로, C와 C++에서 가장 나쁜 버그 중 두 가지예요.
Vec도 마찬가지예요. names[0]은 여전히 벡터가 소유하고 있어요. let first = names[0];이 값을 꺼내 간다면 벡터의 0번 자리에 구멍이 생기고, 벡터가 해제될 때 그 자리를 또 해제하려고 할 거예요. 그래서 인덱싱은 접근만 허락하고, 소유권은 절대 주지 않아요.
한 줄로 정리하면 이래요. 참조자로는 읽을 수 있고 (&mut라면) 바꿀 수도 있지만, 가져갈 수는 없어요. 소유권을 원한다면 컨테이너를 직접 소유하거나, 값이 있던 자리에 뭔가를 남겨 둬야 해요.
고치는 방법
섹션 제목: “고치는 방법”방법 1: 이동하지 말고 대여하기
섹션 제목: “방법 1: 이동하지 말고 대여하기”값을 읽기만 하면 될 때 이 방법을 고르세요. 대부분은 이게 정답이에요. String 대신 &str을 반환하고, v[0] 대신 &v[0]이라고 쓰고, Option의 내용은 as_deref()나 as_ref()로 빌리세요.
struct User { name: String, nickname: Option<String>,}
impl User { fn name(&self) -> &str { &self.name }
fn display_name(&self) -> &str { match self.nickname.as_deref() { Some(nick) => nick, None => &self.name, } }}
fn describe(nick: &Option<String>) -> &str { match nick { Some(n) => n, None => "익명", }}
fn main() { let users = vec![ User { name: String::from("Ferris"), nickname: Some(String::from("crab")) }, User { name: String::from("Corro"), nickname: None }, ]; let first = &users[0]; println!("첫 번째 사용자: {}", first.name()); for user in &users { println!("표시 이름: {}", user.display_name()); } println!("별명: {}", describe(&users[1].nickname));}실행 결과:
첫 번째 사용자: Ferris표시 이름: crab표시 이름: Corro별명: 익명describe를 보세요. *nick이 아니라 nick(참조자)에 대해 match를 해요. 그러면 n은 &String, 즉 대여가 되고 아무것도 이동하지 않아요. match *nick { Some(n) => ... }이라고 쓰면 n에 소유한 String을 달라는 뜻이 되어 E0507이 나요. 컴파일러의 힌트도 같은 말을 해요. consider removing the dereference here(여기서 역참조를 빼 보라)라고요.
as_deref()는 &Option<String>을 Option<&str>로 바꿔 주고, as_ref()는 Option<&String>을 줘요. .unwrap(), .map(), match 전에 이걸 쓰면 소유한 값이 아니라 대여로 작업하게 돼요. r: &String에서 *r도 마찬가지예요. 보통은 *가 전혀 필요 없고, 그냥 r을 쓰면 돼요. Option과 대여를 참고하세요.
방법 2: 복제하기
섹션 제목: “방법 2: 복제하기”정말로 나만의 복사본이 필요할 때 이 방법을 고르세요. 예를 들어 원본은 그 자리에 두고 다른 곳에 저장해야 할 때예요. 컴파일러도 거의 모든 E0507 메시지에서 이걸 제안해요.
struct User { name: String,}
impl User { fn name(&self) -> String { self.name.clone() }}
fn main() { let names = vec![String::from("Ferris"), String::from("Corro")]; let first = names[0].clone(); println!("첫 번째는 {first}, 목록에는 아직 이름이 {}개 있어요", names.len());
let u = User { name: String::from("Ferris") }; let mut badge = u.name(); badge.push_str(" (관리자)"); println!("{badge} / {}", u.name);}실행 결과:
첫 번째는 Ferris, 목록에는 아직 이름이 2개 있어요Ferris (관리자) / Ferris이름이나 짧은 목록을 복제하는 건 비용이 거의 없고, 특히 배우는 동안에는 전혀 문제없어요. 큰 데이터를 자주 도는 반복문 안에서만 문제가 돼요. 대여(방법 1)로 충분하다면 그쪽을 먼저 쓰세요.
방법 3: 꺼내 가고 대신 뭔가 남겨 두기
섹션 제목: “방법 3: 꺼내 가고 대신 뭔가 남겨 두기”&mut 권한이 있고 값을 진짜로 꺼내 가고 싶을 때 이 방법을 고르세요. 예를 들어 완성된 로그나 다음 작업을 넘겨줘야 할 때예요. 구멍을 남길 수는 없지만, 대신할 값을 넣어 둘 수는 있어요.
std::mem::take(&mut x)는 값을 가져가고 그 자리에 기본값을 남겨요(String이면"",Vec이면 빈 벡터).option.take()는Option안의 값을 가져가고None을 남겨요.std::mem::replace(&mut x, new)는new를 넣고 예전 값을 돌려줘요.
use std::mem;
#[derive(Debug)]enum Status { Queued, Running,}
struct Job { log: String, next_step: Option<String>, status: Status,}
impl Job { fn take_log(&mut self) -> String { mem::take(&mut self.log) }
fn start_next(&mut self) -> Option<String> { self.next_step.take() }
fn set_status(&mut self, new: Status) -> Status { mem::replace(&mut self.status, new) }}
fn main() { let mut job = Job { log: String::from("build ok"), next_step: Some(String::from("deploy")), status: Status::Queued, }; let old = job.set_status(Status::Running); println!("상태: {old:?} -> {:?}", job.status); println!("다음 단계: {:?}", job.start_next()); println!("다음 단계 (한 번 더): {:?}", job.start_next()); println!("로그: {:?}", job.take_log()); println!("지금 로그: {:?}", job.log);}실행 결과:
상태: Queued -> Running다음 단계: Some("deploy")다음 단계 (한 번 더): None로그: "build ok"지금 로그: ""구조체에는 절대 구멍이 남지 않아요. take_log 뒤에 로그는 빈 문자열이고, start_next 뒤에 단계는 None이에요. “cannot move out of self.x which is behind a mutable reference”(가변 참조자 뒤에 있는 self.x를 이동시킬 수 없음)를 고치는 표준 방법이 바로 이거예요. mem::take는 타입이 Default를 구현해야 쓸 수 있어요. 다른 타입에는 mem::replace를 쓰세요.
방법 4: 컬렉션을 소유한 다음 꺼내기
섹션 제목: “방법 4: 컬렉션을 소유한 다음 꺼내기”컬렉션이나 구조체를 다 썼고 그 안의 내용물이 필요할 때 이 방법을 고르세요. Vec을 내가 소유하고 있다면 요소를 넘겨받을 수 있어요. remove(i), swap_remove(i), pop(), 그리고 전부 가져올 때는 into_iter()를 쓰면 돼요. &self가 아니라 self를 받는 메서드는 구조체를 소유하니까 필드를 꺼내 갈 수 있어요.
struct User { name: String,}
impl User { fn into_name(self) -> String { self.name }}
fn main() { let mut queue = vec![String::from("email"), String::from("backup"), String::from("report")]; let first = queue.remove(0); println!("{first} 하는 중, 남은 작업: {queue:?}");
let users = vec![User { name: String::from("Ferris") }, User { name: String::from("Corro") }]; let names: Vec<String> = users.into_iter().map(|u| u.into_name()).collect(); println!("{names:?}");}실행 결과:
email 하는 중, 남은 작업: ["backup", "report"]["Ferris", "Corro"]into_iter()는 벡터를 소비하기 때문에, 그 뒤에는 users를 쓸 수 없어요(쓰면 E0382가 나요). into_name의 into_ 접두사는 Rust의 이름 짓기 습관이에요. 읽는 사람에게 “이 메서드는 값을 다 써 버린다”고 알려 줘요. remove(0)은 나머지 요소를 모두 한 칸씩 당기니까, 순서가 상관없다면 swap_remove가 더 빨라요. 요소 삭제하기를 참고하세요.
자주 보게 되는 곳
섹션 제목: “자주 보게 되는 곳”&users를 도는 for 반복문에서 필드 모으기. for u in &users { names.push(u.name); }는 cannot move out of `u.name` which is behind a shared reference 에러가 나요. 해결: names.push(u.name.clone())을 쓰거나, 나중에 users가 필요 없다면 for u in users로 돌거나, 대여를 모으세요. users.iter().map(|u| u.name.as_str()).collect::<Vec<&str>>()처럼요.
참조자를 통해 Option 필드에 .unwrap() 호출하기. config: &Config일 때 config.title.unwrap()은 실패해요. unwrap은 Option을 값으로 받기 때문이에요. 컴파일러가 해결책을 제안해 줘요. config.title.as_ref().unwrap()(&String을 줘요)이나, Option<&str>이 필요하면 config.title.as_deref()를 쓰세요.
Fn 클로저 안에서 캡처한 변수 이동하기. 여러 번 호출될 수 있는 클로저는 캡처한 걸 남에게 줄 수 없어요. cannot move out of `report`, a captured variable in an `Fn` closure 에러가 나요. 해결: 클로저 안에서 빌리거나(&report), 클로저 안에서 복제하거나, 한 번만 실행된다면 바운드를 FnOnce로 바꾸세요. Fn, FnMut, FnOnce를 참고하세요.
Rc 역참조하기. let s: String = *shared;는 cannot move out of an `Rc` 에러가 나요. 다른 주인들이 아직 그 값을 쓰고 있을 수 있기 때문이에요. 해결: 빌리거나(&*shared 또는 그냥 &shared), 복제하거나, Rc::unwrap_or_clone(shared)를 쓰세요. Box는 달라요. Box는 내가 소유하니까 let s = *boxed;가 잘 돼요. 스마트 포인터를 참고하세요.
관련 에러
섹션 제목: “관련 에러”- E0382: 이동된 값을 사용했어요. 이동은 허락됐지만, 그 뒤에 값을 또 썼어요.
- E0594:
&참조자를 통해 값을 대입할 수 없어요. “빌리기만 했다”는 같은 규칙인데, 이동이 아니라 쓰기에 관한 거예요. - E0596: 가변으로 빌릴 수 없어요. 예를 들어
&self만 받는 메서드에서mem::take(&mut self.x)를 쓸 때 나요. - E0308: 타입이 맞지 않아요(
expected String, found &String). 대여로 바꾼 직후에 자주 나와요.&str을 반환하거나.clone()을 호출하세요. - E0515: 지역 변수의 참조자를 반환할 수 없어요. 대여로 고쳤는데 그 대상이 곧 해제될 데이터일 때 나와요.
- 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. names: Vec<String>에서 let first = names[0];이 E0507을 내요. first를 출력만 한다면 보통 가장 좋은 해결책은?
2. fn next(&mut self) -> Option<Task> 메서드가 self.pending(Option<Task>)을 넘겨줘야 해요. 컴파일되는 건?
3. nums: Vec<i32>에서는 let n = nums[0];이 되는데, Vec<String>에서는 왜 안 될까요?
점수: 0 / 3
더 알아보기
섹션 제목: “더 알아보기”- E0507 — Rust error codes index (영어):
mem::replace로 고치는 방법을 포함한 공식 설명이에요. - 소유권이 뭔가요? — The Rust Programming Language 한국어판: 이 에러 뒤에 있는 개념인 이동,
Clone,Copy를 설명해요. - 참조와 대여 — The Rust Programming Language 한국어판:
&와&mut로 할 수 있는 것과 없는 것을 알려 줘요. std::mem::take— 표준 라이브러리 문서 (영어):&mut에서 값을 꺼내고 기본값을 남겨요. 형제 함수인mem::replace로는 대신 넣을 값을 고를 수 있어요.Option— 표준 라이브러리 문서 (영어):as_ref,as_deref,take를 예제와 함께 설명해요.