06. 소유권의 세 규칙 + move/copy
소유권(ownership) 은 Rust 가 GC 없이 메모리 안전을 보장하는 핵심 메커니즘입니다. 세 가지 규칙만 익히면 됩니다 — 각 값은 정확히 한 소유자, 소유자가 스코프를 벗어나면 값은 drop, 대입·인자 전달 시 소유권이 이동(move). 이 강의에서 move 와 copy 의 차이, Drop trait 의 의미를 짚고 다음 강의의 borrow 로 자연스럽게 이어집니다.
이 강의에서 배우는 것
- 1소유권 세 규칙을 자신의 말로 설명한다
- 2move 가 일어나는 시점을 코드에서 짚는다
- 3Copy 트레잇이 구현된 타입과 그렇지 않은 타입을 구분한다
- 4Drop 이 호출되는 시점을 이해한다
- 5use-after-move 컴파일 에러를 보고 원인을 짚는다
소개
다른 언어는 메모리 관리를 두 방향 중 하나로 풉니다 — **수동 (C/C++ malloc/free)** 또는 **자동 GC (Java/Python)**. 수동은 빠르지만 위험하고, GC 는 안전하지만 STW·예측 불가 지연이 단점. Rust 는 제3의 길을 택했습니다 — **소유권 규칙을 컴파일러가 검증**하여 GC 없이 안전을 얻습니다.
핵심 개념
1) 세 가지 규칙
- 각 값은 **정확히 하나의 소유자(owner)** 를 가진다
- 소유자는 한 번에 하나만 존재 가능
- 소유자가 **스코프를 벗어나면** 값은 자동으로 drop 된다
2) move — 소유권 이동
힙 데이터를 가진 값(예: String, Vec) 을 변수에 대입하거나 함수에 넘기면 **소유권이 이동** 합니다. 원본은 더 이상 유효하지 않습니다.
let s1 = String::from("hello");
let s2 = s1; // s1 의 소유권이 s2 로 이동 (move)
println!("{}", s1); // error[E0382]: borrow of moved value: `s1`3) Copy — 비트 단위 복사가 안전한 타입
i32 / f64 / bool / char / 고정 크기 배열 처럼 **스택에만 사는 값** 은 Copy 트레잇이 구현돼 있어 대입 시 move 가 아니라 복사가 일어납니다. 그래서 원본도 그대로 살아 있습니다.
let x = 5;
let y = x; // copy
println!("{} {}", x, y); // OK — 둘 다 살아있음4) Drop — 메모리 해제 후크
소유자가 스코프를 벗어나는 시점에 Drop::drop 이 자동으로 호출돼 힙 메모리·파일 핸들·소켓 등이 해제됩니다. 이걸 **RAII** (Resource Acquisition Is Initialization) 라 부릅니다.
| 타입 | 대입 시 동작 | 이유 |
|---|---|---|
| i32, bool, char | Copy | 스택 데이터, 비트 복사 안전 |
| String, Vec<T> | Move | 힙 포인터 — 두 소유자 → double-free 위험 |
| &T (참조) | Copy | 다음 강의 — 빌림은 소유권 없음 |
| (i32, i32) | Copy | 구성 요소가 모두 Copy |
| (i32, String) | Move | String 이 Move 이므로 전체 Move |
핵심 예제
함수 인자로 넘기면 move 가 일어납니다:
fn takes_ownership(s: String) {
println!("{}", s);
} // 여기서 s 가 drop
fn main() {
let s = String::from("hello");
takes_ownership(s);
// println!("{}", s); // error — s 는 이미 move 됨
}소유권을 돌려받는 패턴 (다음 강의의 borrow 가 더 편하지만, 비교용):
fn take_and_give(s: String) -> String {
println!("len={}", s.len());
s
}
fn main() {
let s1 = String::from("hello");
let s2 = take_and_give(s1);
println!("{}", s2); // OK — s2 가 새 소유자
}Drop 관찰 — 커스텀 타입에 Drop 구현:
struct Resource { name: String }
impl Drop for Resource {
fn drop(&mut self) { println!("drop: {}", self.name); }
}
fn main() {
let _r = Resource { name: "file.txt".into() };
println!("end of main");
} // 출력 순서: "end of main" → "drop: file.txt"자주 하는 실수
Q. String 을 함수에 넘기고 나서 다시 쓰고 싶은데 매번 돌려받아야 하나요?
A. 그것이 다음 강의의 **borrow(&)** 가 푸는 문제입니다. 소유권 이동 없이 잠깐 빌려주기. 이 강의에서는 일단 move 의 작동 원리만 익히세요.
Q. let s2 = s1.clone() 이면 두 변수 다 살아있던데요?
A. `.clone()` 은 **명시적 깊은 복사**. 새 힙 메모리를 할당해 데이터를 복제하므로 양쪽 다 독립적인 String 이 됩니다. 그 대신 비용(메모리·시간) 이 발생.
Q. struct 에 Copy 를 어떻게 붙이나요?
A. `#[derive(Copy, Clone)]` 어트리뷰트. 단, 구성 요소가 **모두 Copy** 여야 가능. String 을 필드로 가진 struct 는 Copy 불가.
정리
- 각 값은 한 소유자, 소유자 스코프 끝 = drop
- 힙 데이터 대입·전달 = move, 원본 무효화
- 스택만 사는 타입은 Copy 라 대입 시 복사
- Drop 트레잇이 RAII 자원 해제를 자동화
과제
- String 을 만들고 함수에 넘긴 뒤 호출자에서 다시 쓰려고 시도해 컴파일 에러 메시지를 확인
- Resource 구조체를 두 개 만들고 main 끝에서 drop 순서가 어떻게 되는지 출력으로 확인 (LIFO 인지)
- i32 만 들어 있는 struct 와 String 이 들어 있는 struct 두 개를 만들고 둘 다 Copy 가 되는지 derive 로 시도