Rust — 컴파일러가 막는 것들 · 소유권과 빌림 · 이론
왜 두 번 쓰면 안 되는가
한 줄 요약
Rust 는 값의 주인을 하나로 강제한다. 다른 곳에 넘기면 원래 자리에서는 못 쓴다. 그 규칙 하나가 GC 없이 메모리 안전을 만든다.
왜 이게 필요했나
C 계열의 대표적 사고 세 가지가 있다.
- use after free — 해제한 메모리를 다시 쓴다
- double free — 두 번 해제한다
- data race — 두 스레드가 같은 값을 동시에 바꾼다
셋 다 "누가 이 값의 주인인가" 가 불분명해서 생긴다. GC 는 첫 두 개를 없애지만 멈춤 시간과 메모리 대가가 있고, 세 번째는 여전히 남는다.
Rust 는 다른 답을 골랐다 — 주인을 컴파일 시점에 추적한다. 실행 중 비용이 0이다.
규칙은 셋뿐이다
1. 모든 값에는 주인(owner)이 하나 있다
2. 주인이 사라지면 값도 사라진다
3. 값을 다른 곳에 넘기면(move) 원래 주인은 잃는다
let s = String::from("hi");let t = s; // 주인이 t 로 옮겨졌다println!("{}", s); // ❌ 컴파일 오류: s 는 이미 넘겨졌다오류 메시지가 정확히 그렇게 말한다.
error[E0382]: borrow of moved value: `s` = note: move occurs because `s` has type `String`, which does not implement the `Copy` traitCopy 를 구현한 타입은 복사된다. 정수·불리언·문자 같은 것들이다. 그래서 let a = 5; let b = a; 는 문제가 없다. 크기가 고정이고 힙을 안 쓰기 때문이다.
빌리기
넘기지 않고 잠깐 쓰려면 빌린다(borrow).
fn len(s: &String) -> usize { s.len() } // 읽기만 빌림let s = String::from("hi");println!("{} {}", len(&s), s); // ✅ s 는 여전히 내 것빌림에는 규칙이 하나 더 있다.
> 읽기 빌림은 여러 개 동시에 되지만, 쓰기 빌림은 하나뿐이고 그동안 읽기도 못 한다.
let mut v = vec![1, 2, 3];let first = &v[0]; // 읽기 빌림v.push(4); // ❌ 쓰기 빌림이 필요한데 읽기가 살아 있다println!("{}", first);이게 막는 것은 실제 사고다. push 가 벡터를 재할당하면 first 가 가리키던 주소는 무효가 된다 — C++ 에서는 그게 조용히 통과하고 나중에 이상한 값을 읽는다.
수명
빌림은 원본보다 오래 살 수 없다. 컴파일러가 그걸 검사한다.
fn dangle() -> &String { // ❌ let s = String::from("hi"); &s // s 는 함수가 끝나면 사라진다}대부분의 경우 수명은 추론된다. 명시적으로 'a 를 쓰는 일은 생각보다 드물고, 주로 여러 참조 중 어느 것과 묶이는지를 알려 줄 때다.
흔한 착각
"clone 하면 되잖아" — 된다. 그리고 그게 맞는 답일 때도 많다. 소유권과 싸우느라 하루를 쓰느니 clone() 하고 넘어가는 편이 낫다. 성능이 문제가 되면 그때 프로파일러를 보고 고친다. 처음부터 참조로 최적화하려는 것이 Rust 를 어렵게 만드는 가장 흔한 이유다.
"unsafe 는 나쁘다" — unsafe 는 "여기서는 컴파일러가 검사할 수 없으니 내가 보증한다" 는 표시다. 표준 라이브러리 안에도 많다. 나쁜 것은 unsafe 를 쓰는 것이 아니라 왜 안전한지 적지 않는 것이다.
실무에서 진짜 중요한 것
오류 메시지를 끝까지 읽는 습관이 Rust 학습의 절반이다. rustc 는 대개 고치는 방법까지 알려 준다.
help: consider cloning the value if the performance cost is acceptable |5 | let t = s.clone(); | ++++++++그리고 cargo clippy 는 컴파일은 되지만 더 나은 방법이 있는 곳을 짚어 준다. 배우는 동안에는 clippy 를 켜 두는 것이 책을 한 권 더 읽는 것과 비슷하다.