Rust 소유권 시스템을 이해하기까지

배경

성능이 중요한 CLI 도구를 만들 일이 생겼다. Node.js로는 한계가 있어 보였고, 최근 팀 내에서 화제였던 Rust를 시도해보기로 했다.

첫 번째 벽: 소유권

let s1 = String::from("hello");
let s2 = s1;
println!("{}", s1); // 컴파일 에러

JavaScript에서는 당연히 되던 코드가 Rust에서는 안 됐다. value borrowed here after move라는 에러 메시지를 처음 봤을 때는 뭘 말하는지 전혀 이해가 안 갔다.

이해의 시작

Rust는 메모리를 GC 없이 관리하기 위해 소유권 시스템을 사용한다. 핵심 규칙 세 가지:

  1. 각 값은 소유자(owner)가 있다
  2. 한 번에 소유자는 하나만 존재한다
  3. 소유자가 스코프를 벗어나면 값은 해제된다

위 코드에서 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의 핵심 가치인 것 같다. 학습 곡선은 가파르지만, 런타임 에러 대신 컴파일 타임에 잡아주는 게 생산성 측면에서 장기적으로 이득일 것 같다는 생각이 들었다.