콘텐츠로 이동

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은 힙 메모리를 소유하기 때문에 복사가 아니라 이동만 할 수 있어요.

친구의 책장을 오후 동안 빌렸다고 생각해 보세요. 책장에 있는 책은 무엇이든 읽을 수 있어요. 하지만 책을 집에 가져갈 수는 없어요. 책장은 내 것이 아니고, 친구가 돌아오면 빈칸만 보게 될 테니까요.

모든 값에는 주인이 딱 하나 있고, 주인이 사라질 때 값을 정리해요(소유권 참고). 빌린 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과 대여를 참고하세요.

정말로 나만의 복사본이 필요할 때 이 방법을 고르세요. 예를 들어 원본은 그 자리에 두고 다른 곳에 저장해야 할 때예요. 컴파일러도 거의 모든 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