Rust - chapter 4-1(소유권)


공식 문서 - 번역글의 4번째 챕터 정리

소유권

러스트의 언어의 유니크한 특징이며, GC 없이 메모리의 안정성 보장을 해주는 역할을 한다.
프로그램이 실행하는 도중 GC가 사용하지 않는 메모리를 회수하거나 프로그래머가 직접 할당 및 해제를 하는 방법이 아닌 소유권을 통해 컴파일할 때 메모리의 안정성을 체크하며 보장한다.

소유권 규칙

  1. Rust의 각각의 값은 해당값의 소유자(owner)라고 불리우는 변수를 갖고 있다.
  2. 한 순간에 하나의 소유자가 존재할 수 있다.
  3. 오너가 스코프 밖으로 나갈 때, 그 값은 제거된다.

메모리 할당

Rust 의 String 타입을 가지고 이야기를 해보자면 String 타입은 가변적이고, 이는 컴파일 타임에는 크기를 알 수 없다. 이에 아래와 같은 기능이 필요하다.

  1. 런타임에 운영체제로부터 메모리가 요청되어야 한다.
  2. String 타입의 사용이 끝났을 경우 메모리를 반납할 방법이 필요하다.

첫 번째 항목의 경우 일반적으로 String::from("....") 를 호출할 경우 구현부분에서 필요한 만큼의 메모리를 요청하지만, 두 번째의 경우는 GC가 없을 경우 프로그래머가 직접 해제를 해야 한다.

러스트는 이것을 변수가 소속되어 있는 스코프 밖으로 벗어나는 순간 메모리를 반납하도록 하였다. 스코프를 벗어날 경우 러스트는 메모리를 반납하기 위한 함수를 호출하게된다. (이 함수를 drop 이라고 지칭)

{
    let s = String::from("hello"); // s는 여기서부터 유효합니다

    // s를 가지고 뭔가 합니다
}  // 이 스코프는 끝났고, s는 더 이상 유효하지 않습니다

📔 Note: C++에서는 이렇게 아이템의 수명주기의 끝나는 시점에 자원을 해제하는 패턴을 종종 자원 습득이 곧 초기화 (Resource Acquisition Is Initialization, RAII) 라고 부릅니다. 러스트의 drop 함수는 여러분이 RAII 패턴을 경험해본 적 있다면 익숙할 것입니다.

move

아래와 같은 코드에서는 무슨 일이 일어날까? JS의 얇은 복사와 비슷한 현상이 발생하면서도 s1을 무효화 시킨다. s1이 기존에 지녔던 메모리의 포인터, 길이, 용량에 대한 정보를 s2로 이동되고 s1은 무효화 되는 것이다. 파일시스템이 이동과 똑같다고 볼 수 있다. s1 이란 파일을 s2 로 이름을 바꿔 이동한 것이다.

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

여기서 왜 s1을 무효화 시켜야 하는가?
s1을 무효화 시키지 않을 경우 s1이 나가면서 참조하는 메모리를 해제하려 할 것이고 마친가지로 s2 역시 참조하는 메모리를 해제하려고 하려는데 두 메모리는 동일하므로 double free 로 알려진 오류가 발생하게 되고 이는 메모리 안정성 버그이며 이 오류는 메모리 손상의 원인이 되는데 이는 곳 보안 취약성의 문제를 일으킬 가능성이 있다.

copy

스택에 존재하는 데이터(컴파일 타임에 크기가 결정되어 있는 타입의 데이터)는 복사가 되므로 위와 같은 move 가 발생하지 않는다.

소유권과 함수

함수에게 값을 넘길 경우 넘기는 변수의 타입의 따라 이동 또는 복사 가 되며 반환할 경우에도 마찬가지이다.
아래의 원문 예제를 참조하자.

fn main() {
    let s = String::from("hello");  // s가 스코프 안으로 들어왔습니다.

    takes_ownership(s);             // s의 값이 함수 안으로 이동했습니다...
                                    // ... 그리고 이제 더이상 유효하지 않습니다.
    let x = 5;                      // x가 스코프 안으로 들어왔습니다.

    makes_copy(x);                  // x가 함수 안으로 이동했습니다만,
                                    // i32는 Copy가 되므로, x를 이후에 계속
                                    // 사용해도 됩니다.

} // 여기서 x는 스코프 밖으로 나가고, s도 그 후 나갑니다. 하지만 s는 이미 이동되었으므로,
  // 별다른 일이 발생하지 않습니다.

fn takes_ownership(some_string: String) { // some_string이 스코프 안으로 들어왔습니다.
    println!("{}", some_string);
} // 여기서 some_string이 스코프 밖으로 벗어났고 `drop`이 호출됩니다. 메모리는
  // 해제되었습니다.

fn makes_copy(some_integer: i32) { // some_integer이 스코프 안으로 들어왔습니다.
    println!("{}", some_integer);
} // 여기서 some_integer가 스코프 밖으로 벗어났습니다. 별다른 일은 발생하지 않습니다.
fn main() {
    let s1 = gives_ownership();         // gives_ownership은 반환값을 s1에게
                                        // 이동시킵니다.

    let s2 = String::from("hello");     // s2가 스코프 안에 들어왔습니다.

    let s3 = takes_and_gives_back(s2);  // s2는 takes_and_gives_back 안으로
                                        // 이동되었고, 이 함수가 반환값을 s3으로도
                                        // 이동시켰습니다.

} // 여기서 s3는 스코프 밖으로 벗어났으며 drop이 호출됩니다. s2는 스코프 밖으로
  // 벗어났지만 이동되었으므로 아무 일도 일어나지 않습니다. s1은 스코프 밖으로
  // 벗어나서 drop이 호출됩니다.

// gives_ownership 함수가 반환 값을 호출한 쪽으로 이동시킵니다.
fn gives_ownership() -> String {
    let some_string = String::from("hello"); // some_string이 스코프 안에 들어왔습니다.

    some_string // some_string이 반환되고, 호출한 쪽의 함수로 이동됩니다.
}

// takes_and_gives_back 함수는 String을 하나 받아서 다른 하나를 반환합니다.
fn takes_and_gives_back(a_string: String) -> String {
    // a_string이 스코프 안으로 들어왔습니다.
    a_string // a_string은 반환되고, 호출한 쪽의 함수로 이동됩니다.
}

개인이 참고하고자 작성한 글이며, 잘못된 정보가 있을 수 있습니다. 잘못된 정보는 메일로 보내주시면 감사하겠습니다. 🙏