소유권 — Ruby의 GC가 해주던 일을 직접 하기
Rubyist가 가장 어려워하는 Rust의 핵심 개념
Ruby에서 메모리 관리를 신경 쓴 적이 있는가? 아마 없을 것이다. GC가 다 해주니까. 객체를 만들고, 변수에 넣고, 메서드에 넘기고, 더 이상 안 쓰면 GC가 알아서 지운다.
Rust에는 GC가 없다. 대신 소유권이라는 규칙 세 가지로 메모리를 관리한다.
규칙 1: 모든 값에는 소유자가 하나
let s1 = String::from("hello");
let s2 = s1; // s1의 소유권이 s2로 이동(move)
// println!("{}", s1); // 컴파일 에러! s1은 더 이상 유효하지 않음
Ruby에서 s2 = s1을 하면 둘 다 같은 객체를 가리킨다. 둘 다 쓸 수 있다. Rust에서는 s1이 사라진다. s2만 남는다.
이게 Rubyist에게 가장 충격적인 부분이다. "변수에 넣었을 뿐인데 원본이 사라진다고?"
왜 이렇게 했나
Ruby의 GC는 런타임에 "이 객체를 아직 누가 쓰고 있나?"를 추적한다. 이게 CPU 시간과 메모리를 먹는다. Rust는 이 추적을 컴파일 타임에 하고, 런타임 비용을 0으로 만든다.
규칙 2: 빌림(borrow) — 참조로 넘기기
소유권을 넘기지 않고 빌려줄 수 있다.
fn print_len(s: &String) { // &로 빌림
println!("{}", s.len());
}
let s = String::from("hello");
print_len(&s); // &로 빌려줌
println!("{}", s); // s 여전히 사용 가능!
&는 "읽기만 할게, 소유권은 네가 가지고 있어"라는 뜻. Ruby에서는 이 구분이 필요 없었다. 모든 객체 전달이 참조 전달이니까.
규칙 3: 가변 빌림은 하나만
let mut s = String::from("hello");
let r1 = &s; // 불변 참조 — OK
let r2 = &s; // 불변 참조 하나 더 — OK
// let r3 = &mut s; // 가변 참조 — 에러! 불변 참조가 있는 동안 불가
"여러 명이 읽는 건 OK, 한 명이 쓰는 것도 OK, 근데 읽는 사람이 있는데 누가 고치면 안 된다." 이 규칙이 data race를 컴파일 타임에 방지한다.
Clone — Ruby의 .dup
소유권을 넘기고 싶지 않고 빌림도 적절하지 않으면 복사한다.
let s1 = String::from("hello");
let s2 = s1.clone(); // 깊은 복사
println!("s1: {}, s2: {}", s1, s2); // 둘 다 OK
Ruby의 .dup이나 .clone과 같다. 근데 성능 비용이 있으니 필요할 때만.
Copy — 스택 값은 예외
정수, 부울, 부동소수점 같은 작은 값은 자동 복사된다.
let x = 42;
let y = x; // 복사됨 (move 아님)
println!("{}", x); // OK! 정수는 Copy trait 구현
스택에 있는 작은 값은 복사 비용이 거의 없으니 자동으로 복사한다. String은 힙 데이터라 자동 복사 안 됨.
실전 패턴
대부분의 경우 이 순서로 생각하면 된다:
- 참조(&)로 빌려줄 수 있나? → 대부분 이걸로 충분
- 소유권을 넘겨야 하나? → 함수가 값을 소유해야 할 때만
- clone이 필요한가? → 위 둘 다 안 되면 복사
핵심 포인트
모든 값에 소유자 하나 — let s2 = s1;으로 소유권이 이동(move)
&로 빌려주면 소유권 유지 — 대부분 이걸로 충분
불변 참조 여러 개 OK, 가변 참조는 하나만 — data race 방지
clone()으로 깊은 복사 가능 — Ruby의 .dup
장점
- ✓ GC 없이 메모리 안전 — 런타임 비용 제로
- ✓ data race가 컴파일 타임에 잡힌다
단점
- ✗ Ruby에 없는 개념이라 초반 학습 곡선이 가파르다
- ✗ 컴파일러와 싸우는 시간이 처음엔 꽤 걸린다