소유권과 수명

소유권은 러스트의 혁신적인 기능입니다. 이것은 러스트가 쓰레기 수집을 피하면서, 완전히 메모리 안전과 효율성을 챙길 수 있게 해 줍니다. 소유권 시스템을 깊게 살펴보기 전에, 이런 설계의 동기를 생각해 보겠습니다.

우리는 당신이 쓰레기 수집(GC)이 항상 최선의 해결책은 아니고, 어떤 상황에서는 메모리를 수동으로 관리하는 것이 낫다는 것을 수긍한다고 가정하겠습니다. 만약 이것을 수긍하지 않는다면, 다른 언어를 찾아보시는 건 어떨까요?

GC에 대한 당신의 생각과 상관없이, 코드를 안전하게 만드는 것은 꽤나 확실하게 엄청난 축복입니다. 절대로 값들이 너무 빨리 없어지는 것을 걱정하지 않아도 되니까요 (그래도 그 값을 가리키고 싶었는지 여부는 조금 다른 문제이지만 말이죠...). 이것은 C와 C++ 프로그램들이 해결해야 하는, 프로그램 어디에서나 볼 수 있는 문제입니다. 비 GC 언어를 사용해본 사람들이라면 한번쯤은 저질러 보았을 이런 간단한 실수를 생각해 보세요:

#![allow(unused)]
fn main() {
fn as_str(data: &u32) -> &str {
    // String 만들기
    let s = format!("{}", data);

    // OH NO! 이 함수 안에서만 존재하는
    // 값의 레퍼런스를 반환했군요!
    // 달랑거리는 포인터 발생! 해제 후 사용! 이런!
    // (러스트에서는 컴파일되지 않습니다)
    &s
}
}

바로 이런 문제를 해결하기 위해서 러스트의 소유권 시스템이 만들어졌습니다. 러스트는 &s가 살아있는 범위를 알고 있고, 이것으로 이 레퍼런스의 탈출을 막을 수 있습니다. 그러나 이 문제는 C 컴파일러라도 그럴듯하게 잡을 수 있는 간단한 경우입니다. 코드가 커지고 포인터들이 다양한 함수들에 인자로 넘겨지면 상황은 더 복잡해집니다. 결국 C 컴파일러는 과부하를 일으키게 되고, 당신의 코드가 불건전하다는 것을 증명하기에 충분한 포인터 분석을 하지 못하게 됩니다. 이렇게 되면 당신의 프로그램이 그냥 맞다고 생각하고 받아들여야만 하겠죠.

러스트에서는 이런 일이 절대 일어나지 않을 것입니다. 컴파일러에게 모든 것이 건전하다는 것을 증명하는 것은 프로그래머의 몫입니다.

물론, 러스트의 소유권 시스템은 그냥 레퍼런스들이 그 본체의 범위를 벗어나지 않는 것을 검증하는 것보다 훨씬 복잡합니다. 그것은 포인터들이 언제나 유효하도록 보장하는 것은 이것보다 훨씬 복잡하기 때문입니다. 예를 들어 이 코드에서,

#![allow(unused)]
fn main() {
let mut data = vec![1, 2, 3];
// 내부 값의 레퍼런스를 받아옵니다
let x = &data[0];

// OH NO! `push`는 `data`의 포인터가 재할당되도록 유도합니다.
// 달랑거리는 포인터! 해제 후 사용! 이런!
// (러스트에서는 컴파일되지 않습니다)
data.push(4);

println!("{}", x);
}

이 버그를 잡기 위해서는 단순한 범위 분석으로는 부족한데, 그것은 data가 우리가 필요한 만큼 오래 살기 때문입니다. 그러나 우리가 레퍼런스를 가지고 있는 동안 바뀌었지요. 그래서 러스트는 레퍼런스가 본체와 다른 레퍼런스들을 수정하지 못하게 막는 것입니다.