Rust 소유권 시스템을 이해하기까지
배경
성능이 중요한 CLI 도구를 만들 일이 생겼다. Node.js로는 한계가 있어 보였고, 최근 팀 내에서 화제였던 Rust를 시도해보기로 했다.
첫 번째 벽: 소유권
let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 컴파일 에러
JavaScript에서는 당연히 되던 코드가 Rust에서는 안 됐다. value borrowed here after move라는 에러 메시지를 처음 봤을 때는 뭘 말하는지 전혀 이해가 안 갔다.
이해의 시작
Rust는 메모리를 GC 없이 관리하기 위해 소유권 시스템을 사용한다. 핵심 규칙 세 가지:
- 각 값은 소유자(owner)가 있다
- 한 번에 소유자는 하나만 존재한다
- 소유자가 스코프를 벗어나면 값은 해제된다
위 코드에서 s1의 소유권이 s2로 이동(move)했기 때문에 s1은 더 이상 사용할 수 없다.
해결 방법
값을 복사하거나 참조를 사용하면 된다.
// 복사
let s2 = s1.clone();
// 참조
let s2 = &s1;
println!("{}", s1); // 가능
함수와 소유권
함수에 값을 넘기는 것도 소유권 이동이다.
fn calculate_length(s: &String) -> usize {
s.len()
}
let s1 = String::from("hello");
let len = calculate_length(&s1);
println!("{}: {}", s1, len); // s1 사용 가능
참조(&)를 사용하면 소유권을 빌려주는(borrowing) 형태가 된다.
느낀 점
처음엔 답답했지만, 컴파일러가 메모리 안전성을 보장해준다는 점에서 안심이 됐다. GC 오버헤드 없이 안전한 코드를 작성할 수 있다는 게 Rust의 핵심 가치인 것 같다. 학습 곡선은 가파르지만, 런타임 에러 대신 컴파일 타임에 잡아주는 게 생산성 측면에서 장기적으로 이득일 것 같다는 생각이 들었다.