콘텐츠로 이동

Rust 에러 E0382: borrow of moved value (이동된 값 사용)

E0382는 값을 다른 곳에 넘겨준 다음(값을 이동(move)시킨 다음) 그 값을 또 쓰려고 했다는 뜻이에요. Rust를 시작하고 첫 주에 가장 많이 만나는 에러이고, 거의 항상 한 줄만 고치면 해결돼요. 이 페이지에서는 고치는 방법을 먼저 보여 주고, 그다음에 무슨 일이 일어나는지 설명할게요. 이 시리즈의 다른 에러는 Rust 컴파일러 에러 해설에서 볼 수 있어요.

error[E0382]: borrow of moved value: `name`
--> src/main.rs:8:25
|
6 | let name = String::from("Ferris");
| ---- move occurs because `name` has type `String`, which does not implement the `Copy` trait
7 | greet(name); // 여기서 name이 greet로 이동해요
| ---- value moved here
8 | println!("Goodbye, {name}!"); // ❌ name은 이제 없어요
| ^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `greet` to borrow instead if owning the value isn't necessary
--> src/main.rs:1:16
|
1 | fn greet(name: String) {
| ----- ^^^^^^ this parameter takes ownership of the value
help: consider cloning the value if the performance cost is acceptable
|
7 | greet(name.clone()); // 여기서 name이 greet로 이동해요
| ++++++++

“borrow of moved value”는 “이동된 값을 빌렸다”는 뜻이에요. note는 “값을 소유할 필요가 없다면 greet의 매개변수를 대여로 바꿔 보라”고 제안하고, help는 “성능 비용이 괜찮다면 값을 복제(clone)해 보라”고 제안해요.

이 에러를 내는 프로그램은 다음과 같아요.

fn greet(name: String) {
println!("Hello, {name}!");
}
fn main() {
let name = String::from("Ferris");
greet(name); // 여기서 name이 greet로 이동해요
println!("Goodbye, {name}!"); // ❌ name은 이제 없어요
}

“use of moved value”(이동된 값 사용)라는 문구를 볼 때도 있어요. 같은 에러예요. 뒤에 오는 줄이 값을 읽기만 하면(println!처럼) “borrow”라고 하고, 값을 또 이동시키려고 하면(let other = name;처럼) “use”라고 해요.

세 개의 라벨을 위에서 아래로 읽어 보세요. 이것만으로 전체 이야기를 알 수 있어요.

  1. 6번째 줄: name은 String이고, String은 Copy가 아니에요. 그래서 어딘가에 넘기면 값이 이동해요.
  2. 7번째 줄: greet(name)이 문자열을 함수 안으로 이동시켜요. 이제 greet가 주인이고, greet가 끝날 때 문자열도 해제돼요.
  3. 8번째 줄: name을 다시 읽으려고 하지만, main은 더 이상 그 값의 주인이 아니에요.

하나뿐인 집 열쇠를 친구에게 건네준 것과 비슷해요. 한번 건네주고 나면, 예전에 내가 열쇠를 갖고 있었다는 걸 기억하더라도 그 열쇠로 문을 열 수는 없죠.

Rust의 모든 값에는 주인(owner)이 딱 하나 있어요. 주인이 사라지면 값도 정리돼요. Rust는 이렇게 가비지 컬렉터 없이 메모리를 해제해요. 자세한 규칙은 소유권(ownership) 페이지에서 설명해요.

만약 두 변수가 같은 String을 함께 소유한다면, 마지막에 둘 다 같은 메모리를 해제하려고 할 거예요. 이걸 “이중 해제”(double free)라고 하는데, C와 C++ 프로그램을 실제로 망가뜨리는 버그예요. Rust는 대입이나 함수 호출 때 값을 이동시켜서 주인이 늘 하나만 있게 만들어, 이 문제를 막아요.

i32, bool, char처럼 작고 단순한 타입은 Copy예요. 넘길 때 값싸게 복사되기 때문에 이런 타입에서는 E0382를 볼 일이 없어요. String, Vec, 그리고 대부분의 구조체처럼 힙 메모리를 소유하는 타입은 복사되지 않고 이동해요.

함수가 값을 보기만 하면 될 때 이 방법을 고르세요. 대부분은 이게 정답이에요. 매개변수를 참조자로 바꾸고 &name을 넘기면 돼요.

fn greet(name: &str) {
println!("Hello, {name}!");
}
fn main() {
let name = String::from("Ferris");
greet(&name); // 주지 않고 빌려주기만 해요
println!("Goodbye, {name}!"); // ✅ 아직 우리 거예요
}

실행 결과:

Hello, Ferris!
Goodbye, Ferris!
▶ 직접 실행해 보기

&String으로 써도 되지만, 보통은 &str을 써요. 문자열 리터럴도 받을 수 있기 때문이에요. 대여와 문자열을 참고하세요. 함수가 값을 바꿔야 한다면 &mut를 쓰면 돼요.

다른 코드가 정말로 자기만의 복사본이 필요하고, 나도 원본을 계속 써야 할 때 이 방법을 고르세요. 예를 들어 구조체나 Vec에 저장하는 경우예요. .clone()은 완전히 독립된 복사본을 만들어요.

struct Player {
name: String,
score: u32,
}
fn main() {
let name = String::from("Ferris");
let player = Player { name: name.clone(), score: 0 };
println!("{}의 시작 점수: {}점", player.name, player.score);
println!("원래 이름: {name}");
}

실행 결과:

Ferris의 시작 점수: 0점
원래 이름: Ferris
▶ 직접 실행해 보기

솔직하게 말하면, 짧은 문자열이나 작은 Vec을 복제하는 건 비용이 거의 없어요. 특히 배우는 동안에는 전혀 문제없어요. 큰 데이터를 자주 도는 반복문 안에서 복제할 때만 신경 쓰면 돼요. 대여(방법 1)로 해결된다면 그쪽을 먼저 쓰세요.

방법 3: 작은 타입에는 Copy 붙이기

섹션 제목: “방법 3: 작은 타입에는 Copy 붙이기”

좌표, 색상, ID처럼 단순한 값으로만 이루어진 작은 구조체라면 이 방법을 고르세요. #[derive(Clone, Copy)]를 붙이면 구조체가 이동하지 않고 복사돼요.

#[derive(Debug, Clone, Copy)]
struct Point {
x: i32,
y: i32,
}
fn show(p: Point) {
println!("({}, {})", p.x, p.y);
}
fn main() {
let start = Point { x: 0, y: 3 };
show(start); // start를 복사해요
show(start); // ✅ 또 써도 돼요
println!("{:?}", start);
}

실행 결과:

(0, 3)
(0, 3)
Point { x: 0, y: 3 }
▶ 직접 실행해 보기

Copy는 모든 필드가 Copy일 때만 쓸 수 있어요. String이나 Vec 필드가 있는 구조체는 Copy가 될 수 없으니, Clone을 붙이고 .clone()을 호출하세요. 자세한 내용은 자주 쓰는 트레이트에 있어요.

함수가 값을 받아서 바꾸고, 그 결과를 내가 다시 써야 할 때 이 방법을 고르세요. 함수가 값을 반환하고, 그걸 다시 변수에 담으면 돼요.

fn add_signature(mut letter: String) -> String {
letter.push_str("\n-- Ferris 드림");
letter // 다시 돌려줘요
}
fn main() {
let letter = String::from("러스타시안 여러분께,");
let letter = add_signature(letter); // 다시 돌려받아요
println!("{letter}");
}

실행 결과:

러스타시안 여러분께,
-- Ferris 드림
▶ 직접 실행해 보기

이 방식은 “빌더” 스타일 함수에 잘 어울려요. 간단히 그 자리에서 바꾸기만 할 거라면 fn add_signature(letter: &mut String)이 읽기 더 쉬울 때가 많아요.

Vec을 for 반복문으로 돌 때. for task in tasks는 벡터 전체를 반복문 안으로 이동시켜요. 그래서 반복문이 끝난 뒤에는 tasks를 쓸 수 없어요.

for task in tasks {
println!("하는 중: {task}");
}
println!("{}개 완료", tasks.len()); // ❌ `tasks` moved due to this implicit call to `.into_iter()`

컴파일러는 “눈에 보이지 않는 .into_iter() 호출 때문에 tasks가 이동했다”고 알려 줘요. 해결: 빌려서 도세요. for task in &tasks처럼요. for 반복문과 반복자를 참고하세요.

값을 Vec이나 구조체에 넣을 때. names.push(name)과 Order { customer }는 둘 다 값을 안으로 이동시켜요. 그 뒤에 name이나 customer를 쓰면 에러가 나요. 해결: 새 집에서 다시 꺼내 읽거나(names[0], order.customer), 다른 작업을 이동 전에 끝내거나, .clone()을 넣으세요.

반복문 안에서 이동할 때. log.push(message)가 반복문 안에 있으면, 첫 번째 바퀴에서 message가 이동하고 두 번째 바퀴에는 남은 게 없어요. 컴파일러는 value moved here, in previous iteration of loop(이전 반복에서 값이 여기로 이동함)라고 알려 줘요. 해결: message.clone()을 넣거나, 값을 반복문 안에서 만들어서 바퀴마다 새 값이 생기게 하세요.

스레드와 move 클로저. thread::spawn(move || ...)는 캡처한 변수를 전부 새 스레드로 이동시켜요. 해결: 스레드를 만들기 전에 데이터를 복제하거나, Arc로 공유하세요. 동시성과 공유 상태를 참고하세요.

  • E0502: 값을 빌려 둔 상태에서, 그 대여가 아직 쓰이는 동안 값을 바꾸려고 했어요.
  • E0499: 같은 값을 동시에 두 번 가변으로 빌렸어요.
  • E0106: 라이프타임 지정자가 빠졌어요. 이동을 피하려고 참조자를 반환할 때 자주 나와요.
  • E0308: 타입이 맞지 않아요. 예를 들어 대여로 바꾼 뒤 &str이 필요한 곳에 String을 넘길 때 나와요.
  • 전체 컴파일러 에러: 에러 페이지 전체 목록이에요.

이해도 점검

1. report가 String일 때 save(report)를 호출한 뒤 report를 출력하면 왜 E0382가 날까요?

2. 함수가 Vec<String>을 읽기만 하면 돼요. 보통 가장 좋은 해결책은 무엇일까요?

3. #[derive(Clone, Copy)]를 붙일 수 있는 구조체는 무엇일까요?

점수: 0 / 3