소유권 (Ownership)

Ownership (소유권)

Rust는 메모리에 저장되는 데이타의 소유자(owner)를 트래킹하여 Heap 메모리를 관리하는 특별한 형태의 메모리 관리 방식을 사용한다. Heap 메모리 관리의 방식을 먼저 살펴 보면, 기존의 C/C++와 같은 언어들은 Heap 메모리를 할당한 후, 개발자가 항상 메모리를 해제하는 코드를 넣어 수작업으로 메모리를 관리하였다. 또한, Java나 C#과 같은 언어들은 가비지 컬렉터(garbage collector)를 사용하여 객체의 레퍼런스를 트래킹하면서 객체가 더이상 사용되지 않는 경우 런타임시에 Heap 메모리를 해제하는 방식을 사용하였다. Rust는 컴파일시에 Heap 메모리가 언제 해제되는 지를 알아내고, 컴파일러가 메모리 해제코드를 자동으로 추가하는 방식을 사용한다. 이러한 메모리 관리를 위해 Rust는 Ownership (소유권)이라는 개념을 사용한다.

Ownership 규칙

Rust는 다음과 같은 Ownership 규칙을 사용한다.

  • Rust에서 각각의 데이타(값)은 항상 소유자(Owner)라고 불리우는 하나의 변수를 갖는다.
  • Owner는 항상 한 싯점에서 하나만 존재한다.
  • Owner가 코드 Scope를 벗어날 때, 데이타(값)은 해제(drop)된다.

Ownership 규칙은 Stack에 저장되는 간단한 Primitive 데이타 타입(예: i32, bool 등)에는 적용되지 않고, Heap에 저장되는 String 타입과 같은 복잡한 데이타 타입에 적용된다.

예를 들어, 아래 예제에서 변수 s는 "HELLO" 라는 문자열을 소유하는 owner가 되고, 다음 문자에서 "HELLO WORLD"로 변경되며, main 블럭을 빠져나올 때 Scope를 벗어나므로 (컴파일러가 이곳에 해제함수(drop 함수)를 넣어) Heap 메모리를 해제하게 된다. 메모리를 해제한 후에는 변수 s는 더이상 데이타를 소유하지 않게 된다.

fn main() {
    let x: i32;
    x = 0;          // 변수 s가 아직 존재하지 않으므로 invalid

    let mut s = String::from("HELLO"); // 변수 s가 문자열의 owner가 됨

    s.push_str(" WORLD");  // 변수 s가 사용됨
    
    println!("{}", s);    // 변수 s가 사용됨
}   // *변수 s가 Scope를 벗어나므로, 여기에서 Drop 되어 invalid 하게 됨

변수 s는 실제데이타의 메타 데이타인 {포인터(ptr), 실제길이(len), 가능용량(capacity)} 등을 가지는데, 이를 Stack에 저장하고, 실제 데이타인 "HELLO" (혹은 "HELLO WORLD")는 Heap 상에 저장된다. 아래 그림은 이러한 관계를 표현한 것이다.

Rust 변수 소유권

Move (소유권 이전)

한 변수가 가지고 있던 소유권(ownership)이 다른 변수로 이동하는 것을 Rust에서는 "move"(소유권 이전)이라고 한다.

위에서 설명한 소유권의 규칙에 의하여, 소유권은 한 싯점에 하나의 변수만이 가질 수 있다. 만약 소유권을 가진 하나의 변수를 다른 변수에 할당하면, 그 소유권은 할당된 변수로 이동한다. 아래 예제에서, 변수 s는 처음 소유권을 가지고 있다가, "let s2 = s;" 문이 실행되면서 "HELLO" 문자열 메모리 영역에 대한 소유권을 s2로 넘기고, 자신(변수 s)은 더이상 그 문자열 영역을 엑세스하지 못하게 된다. 만약 이렇게 소유권을 이전한 후에, 변수 s를 사용하여 문자열 메모리에 엑세스하면 아래와 같이 에러가 발생한다.

fn main() {
    let s = String::from("HELLO"); // 변수 s가 문자열의 owner가 됨
    let s2 = s;   // 이제 변수 s2가 owner가 됨

    println!("{}", s);    // 변수 s가 사용됨: ERROR!
                // ^ value borrowed here after move
}

일반적으로 다른 언어(C/C++, Java, C# 등)에서는 이렇게 다른 변수에 할당을 하면, 레퍼런스를 복제하여 두 변수가 동일한 메모리를 가리키지만, Rust에서는 소유권의 이전(move)에 의해 첫번째 변수를 사용하지 못하게 된다.

Rust의 소유권 이전(move)은 위와 같이 할당에 의해 일어날 뿐만 아니라, Owner 변수를 다른 함수의 파라미터로 전달할 경우에도 일어나고, 또한, 함수에서 값을 리턴할 때도 일어난다.

아래 예제는 변수 s가 print_data() 함수의 파라미터로 전달되면서 소유권이 data 파라미터로 이전(move) 하는 것을 예시한 것이다. 이렇게 move 후에는 main 함수에서 변수 s를 사용할 수 없다. 또한, print_data() 함수가 종료될 때, 파라미터 data가 소유한 메모리가 Scope를 벗어나므로 메모리를 해제하는 drop 함수를 호출하게 된다.

fn main() {
    let s = String::from("HELLO"); // 변수 s가 문자열의 owner가 됨
    print_data(s);   // 변수 s가 파라미터로 전달되면서 소유권이 move됨.
    // 여기서부터는 변수 s 사용 못함.
}

fn print_data(data: String) {  // 파라미터 data가 문자열의 새 owner가 됨
    println!("{}", data);      // 문자열 사용
}                              // 여기서 문자열 메모리 drop 함

다음 예제는 함수에서 리턴할 때 소유권 이전(move)이 일어는 것을 표현한 것인데, get_data() 함수에서 String 타입의 변수 s를 생성할 때, 변수 s가 Owner가 되지만 이를 리턴하면서 소유권이 main() 함수의 변수 mydata로 이전되게 된다.

fn main() {
    let mydata = get_data();   // 함수 리턴값을 받으면서 소유권 받음
    println!("{}", mydata);
}

fn get_data() -> String {
    let s: String = "Data".to_owned(); // 변수 s가 owner
    s                                  // 리턴하면서 소유권 이전
}

This site is not affiliated with or endorsed by the Rust Foundation or Rust Project.