동작하는 코드
먼저 이 코드를 Rust Playground에서 실행해보세요.
fn main() {
let name = String::from("민수");
let greeting = format!("안녕, {name}!");
println!("{greeting}");
println!("이름: {name}");
}
잘 돌아가죠? 이제 살짝 바꿔볼게요.
fn main() {
let name = String::from("민수");
let name2 = name; // name을 name2에 대입
println!("이름: {name}"); // 에러!
}
error[E0382]: borrow of moved value: `name`
--> src/main.rs:4:24
|
2 | let name = String::from("민수");
| ---- move occurs because `name` has type `String`
3 | let name2 = name;
| ---- value moved here
4 | println!("이름: {name}");
| ^^^^ value borrowed here after move
해석: "name의 값이 name2로 이동했기 때문에, 더 이상 name을 사용할 수 없어요."
해결: 이동 후에는 새 주인(name2)을 사용하거나, clone()으로 복사본을 만드세요.
직접 수정하기
- 에러가 나는 코드에서
println!의{name}을{name2}로 바꿔보세요. 에러가 사라지나요? let name2 = name;대신let name2 = name.clone();으로 바꿔보세요. 이번에는name도name2도 사용할 수 있나요?
"왜?" — 소유권의 3대 규칙
Rust의 소유권 규칙은 딱 세 가지예요.
- 모든 값에는 주인(owner)이 있다
- 주인은 한 번에 하나만 존재한다
- 주인이 스코프를 벗어나면 값은 버려진다(drop)
"물건의 주인" 비유로 생각해보세요.
- 여러분의 노트북은 주인이 한 명이에요 (규칙 2)
- 노트북을 친구에게 주면(이동), 이제 그 친구가 주인이에요. 여러분은 더 이상 사용할 수 없어요
- 주인이 방을 나가면(스코프 종료), 노트북도 함께 치워져요 (규칙 3)
이동(move) — 택배 보내기
String 같은 힙 데이터를 다른 변수에 대입하면, 값이 이동(move) 해요. 택배를 보내는 것과 같아요 — 보낸 사람 손에는 더 이상 물건이 없어요.
fn main() {
let original = String::from("택배 내용물");
let moved = original; // 택배를 보냄!
// original은 이제 빈 손
// moved가 새 주인
println!("{moved}");
}
모듈 06에서 벡터를 함수에 넘긴 후 다시 쓰려고 했을 때 에러가 났던 거 기억나세요? 그때 clone()으로 해결했었죠. 이제 그 에러가 왜 발생했는지 보이시죠?
fn print_fruits(fruits: Vec<&str>) {
for f in fruits {
println!("{f}");
}
}
fn main() {
let fruits = vec!["사과", "바나나"];
print_fruits(fruits); // fruits의 소유권이 함수로 이동!
// println!("{:?}", fruits); // 에러! 이미 이동됨
}
함수에 값을 넘기는 것도 이동이에요. fruits의 주인이 main에서 print_fruits 함수의 매개변수로 바뀐 거예요.
Copy 타입 — 복사되는 값들
그런데 정수나 bool은 다르게 동작해요.
fn main() {
let x = 42;
let y = x; // 복사됨! (이동이 아님)
println!("x = {x}, y = {y}"); // 둘 다 사용 가능!
}
에러가 안 나요! 왜일까요?
스택에 저장되는 작은 값들은 복사해도 비용이 거의 없어요. 그래서 Rust는 이런 타입에 Copy 트레이트를 부여해서 이동 대신 복사가 일어나게 해요.
| Copy 타입 (복사됨) | Copy 아닌 타입 (이동됨) |
| ---------------------------- | --------------------------- |
| i32, f64, bool, char | String, Vec, HashMap |
| 스택에 저장, 크기 고정 | 힙에 데이터 저장, 크기 가변 |
더 알아보기: clone()은 왜 항상 쓰면 안 되나요?
clone()은 힙에 있는 데이터 전체를 복사해요. 작은 문자열이면 괜찮지만, 수만 개의 요소가 있는 벡터를 clone()하면 메모리와 시간이 많이 들어요.
fn main() {
// 10,000개의 숫자를 복사 — 비용이 크다!
let big_data = vec![0; 10_000];
let copy = big_data.clone();
println!("원본: {}, 복사본: {}", big_data.len(), copy.len());
}
다음 모듈에서 배울 빌림(borrowing) 을 쓰면, 복사 없이도 값을 사용할 수 있어요. clone()보다 훨씬 효율적이에요!
더 알아보기: 스코프와 drop
주인이 스코프를 벗어나면 Rust가 자동으로 값을 정리해요. 이걸 drop이라고 해요.
fn main() {
{
let s = String::from("잠깐!");
println!("{s}");
} // <- 여기서 s가 drop됨. 메모리 자동 정리!
// println!("{s}"); // 에러! s는 이미 사라졌어요
}
C에서는 free()를 직접 호출해야 했지만, Rust는 스코프가 끝나면 알아서 해줘요. 깜빡할 일이 없어요!
연습 1. 아래 코드가 컴파일되도록 수정해보세요. clone()을 사용하지 않고 해결해보세요.
fn main() {
let message = String::from("안녕하세요");
let message2 = message;
println!("{message}");
}
힌트: 이동 후에 원래 변수는 사용할 수 없어요. 어떤 변수를 출력해야 할까요?
연습 2. 아래 코드는 왜 에러가 나지 않을까요? 이유를 설명해보세요.
fn main() {
let a = 10;
let b = a;
println!("a = {a}, b = {b}");
}
힌트: i32는 어떤 특별한 성질이 있나요?
Q1. 소유권의 3대 규칙에 포함되지 않는 것은?
- A) 모든 값에는 주인이 있다
- B) 주인은 한 번에 하나만 존재한다
- C) 주인이 스코프를 벗어나면 값이 버려진다
- D) 모든 값은 자동으로 복사된다
Q2. 아래 코드의 결과는?
fn main() {
let s1 = String::from("hello");
let s2 = s1;
println!("{s2}");
}
- A) 컴파일 에러
- B) "hello" 출력
- C) 빈 문자열 출력
- D) 런타임 에러
Q3. i32 타입의 값을 다른 변수에 대입하면 어떻게 되나요?
- A) 이동(move)되어 원래 변수를 사용할 수 없다
- B) 복사(copy)되어 두 변수 모두 사용할 수 있다
- C) 참조(reference)가 생긴다
- D) 컴파일 에러가 발생한다