소유권 규칙 자세히 알아보기

소유권을 요약하자면 다음 세 가지 규칙으로 정리할 수 있습니다.

  • 모든 "값"들은 해당 값을 "소유"하고 있는 소유자(Owner)가 존재합니다.
  • 한 번에 하나의 소유자만 존재할 수 있습니다. 하나의 값에 두 개의 소유자가 동시에 존재할 수 없습니다.
  • 소유자가 현재 코드의 스코프에서 벗어나면, 값은 메모리에서 할당 해제됩니다.

안타깝게도 소유권 모델은 파이썬 뿐만 아니라 다른 프로그래밍 언어에는 없는 러스트만의 고유한 특징이기 때문에, 여기서 파이썬과 비교하며 소유권을 설명하기는 조금 어렵습니다.

값에 대한 소유권

프로그래밍에서 메모리 관리가 필요한 이유는 더이상 사용되지 않는 "값"을 처리하지 않으면 스택과 힙 메모리 영역이 가득 차기 때문입니다. 러스트에서는 어떤 값이 더이상 사용되지 않는지를 소유권을 사용해 판단합니다. 모든 값에 소유자를 지정하고, 이 값을 소유하고 있는 소유자가 없게 되면 즉시 값이 메모리에서 할당 해제되는 원리입니다. 아래 예제를 보겠습니다.

fn main() {
    let x = 1;
    // x is dropped
}

이 예제에서, x 라는 변수에 담긴 1 이라는 값은 main() 함수를 벗어나게 되면 더 이상 사용되지 않습니다. 따라서 x 는 즉시 메모리에서 지워지게 됩니다. 마찬가지로 같은 함수 내에서라도 스코프를 벗어나면 즉시 값은 사라집니다.

fn main() {
    let x = 1;
    {
        let y = x;
        println!("{} {}", x, y);
        // y is dropped
    }
    println!("{} {}", x, y); // This line won't compile
    // x is dropped
}

이번엔 코드 중간에 있는 {} 에 의해 스코프가 추가되었고, 이 안에서 y가 선언되었습니다. 이 스코프를 벗어나면 y는 더이상 사용되지 않으므로 즉시 할당 해제됩니다. 마찬가지로 함수에 파라미터로 변수를 전달하는 경우에도 같은 원리가 적용됩니다. 여기서 String::from("Hello")는 러스트에서 문자열을 선언하는 방법으로, 문자열에 대한 자세한 내용은 다음 챕터에서 설명하겠습니다.

fn dummy(x: String) {
    println!("{}", x);
  	// x is dropped
}

fn main() {
    let x = String::from("Hello");
    dummy(x);
    println!("{}", x);  // This line won't compile
}

함수 dummy 에 문자열이 전달된 다음, 함수를 벗어나면 그 즉시 x 는 할당 해제됩니다. 그런데 이미 할당 해제된 x를 9번 라인에서 참조하고 있기 때문에 오류가 발생합니다. 그러면 모든 값은 다른 함수에 전달하면 영원히 사용하지 못하는 걸까요? 이런 경우 사용할 수 있는 두 가지 방법이 있습니다.

소유권 돌려주기

먼저 함수에서 해당 변수의 소유권을 되돌려줄 수 있는 방법이 있습니다. 아래 예제를 보겠습니다.

fn dummy(x: String) -> String {
    println!("{}", x);
    x
}

fn main() {
    let x = String::from("Hello");
    let x = dummy(x);
    println!("{}", x);
}

실행 결과

Hello
Hello

함수 dummy에서 입력 변수 x는 함수 내부에서 사용된 다음 리턴됩니다. 그 다음 함수의 리턴값을 재선언한 변수 x에 할당함으로써 소유권이 x로 되돌아옵니다. 좀더 이해하기 쉽도록 변수명을 아래와 같이 바꿔보겠습니다. 결론적으로, "Hello"라는 값을 소유하고 있는 변수만 xyz 순서로 바뀌고, 값은 그대로 있게 됩니다. 하지만 이 방법은 매번 함수의 리턴값을 변수로 재선언해주어야 하기 때문에 코드의 가독성이 떨어지고, 값이 어느 변수로 이동하는지를 알기 어려운 단점이 있습니다.

fn dummy(y: String) -> String {
    println!("{}", y);
    y
}

fn main() {
    let x = String::from("Hello");
    let z = dummy(x);
    println!("{}", z);
}

실행 결과

Hello
Hello

레퍼런스와 소유권 빌리기

러스트에는 값의 소유권을 잠시 빌려줄 수 있는 개념인 대여(borrow)가 있습니다. 변수 앞에 & 키워드를 사용하면 되는데, 해당 변수의 레퍼런스(reference)를 선언한다는 의미입니다. 레퍼런스란 소유권을 가져가지 않고 해당 값을 참조할 수 있는 방법입니다. 아래 예제를 보겠습니다.

fn main() {
    let x = String::from("Hello");
    let y = &x;

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

실행 결과

Hello Hello

let y = &x;와 같이 선언하더라도 문자열 "Hello"의 값의 소유권은 여전히 x에 있고, y는 단순히 값을 참조만 합니다. 따라서 마지막에서 변수 xy를 모두 프린트해도 에러가 발생하지 않습니다.

아래 예제에서 dummy 함수의 파라미터 타입은 &String으로, 문자열의 레퍼런스 타입을 의미합니다. main함수에서 dummy를 실행할 때, 변수 x의 레퍼런스인 &x 가 전달되었습니다. 이건 소유권을 잠시 함수 내부의 y 파라미터에 빌려준다는 의미입니다. 소유권을 대여한 변수가 dummy함수의 스코프를 벗어나면, 그 즉시 소유권은 원래 소유자인 x 에게 되돌아갑니다. 따라서 dummy 함수에서 x에 저장된 문자열 값을 사용하더라도 이후에 x를 통해 문자열을 계속 사용할 수 있게 됩니다. 그래서 마지막에 x를 프린트해도 에러가 발생하지 않고 잘 컴파일됩니다.

fn dummy(y: &String) {
    println!("{}", y);
    // ownership returns to `x`
}

fn main() {
    let x = String::from("Hello");
    dummy(&x);
    println!("{}", x);
}

실행 결과

Hello

가변 레퍼런스

어떤 변수의 레퍼런스를 만들 때, 원래 변수가 불변이라면 레퍼런스를 사용해 원래 변수의 값을 바꿀 수 없습니다. 아래 예제에서는 변수 x를 함수 dummy에 레퍼런스로 전달합니다. 그리고 push_str 함수를 사용해 " world!"라는 문자열을 x의 뒤에 추가하고 있습니다. 그런데 코드를 실행하면 에러가 발생합니다.

fn dummy(y: &String) {
    y.push_str(" world!");
    println!("{}", y);
    // ownership returns to `x`
}

fn main() {
    let x = String::from("Hello");
    dummy(&x);
    println!("{}", x);
}

실행 결과

   Compiling rust_part v0.1.0 (/Users/code/temp/rust_part)
error[E0596]: cannot borrow `*y` as mutable, as it is behind a `&` reference
 --> src/main.rs:2:5
  |
1 | fn dummy(y: &String) {
  |             ------- help: consider changing this to be a mutable reference: `&mut String`
2 |     y.push_str(" world!");
  |     ^^^^^^^^^^^^^^^^^^^^^ `y` is a `&` reference, so the data it refers to cannot be borrowed as mutable

에러 내용을 읽어보면 y에서 소유권을 빌려왔지만, 가변 레퍼런스가 아니기 때문에 값을 수정할 수 없다고 합니다. 컴파일러의 조언에 따라서 y를 가변 레퍼런스로 수정해 보겠습니다. 여기서 총 3군데를 수정했습니다.

  1. dummy함수의 파라미터 y 의 타입이 &mut String 으로 변경
  2. 변수 x를 가변 변수로 선언
  3. dummy함수에 x를 전달할 때 가변 레퍼런스 &mut x로 전달
fn dummy(y: &mut String) {
    y.push_str(" world!");
    println!("{}", y);
    // ownership returns to `x`
}

fn main() {
    let mut x = String::from("Hello");
    dummy(&mut x);
    println!("{}", x);
}

실행 결과

Hello world!
Hello world!

가변 레퍼런스를 사용할 때 주의해야 하는 점은 소유권 규칙의 두 번째 규칙인 "한 번에 하나의 소유자만 존재할 수 있다" 입니다. 예를 들어 하나의 값에 대해서 두 개의 가변 레퍼런스를 만들어 보겠습니다. 변수 yz는 모두 변수 x의 가변 레퍼런스입니다.

fn main() {
    let mut x = String::from("Hello");
    let y = &mut x;
    let z = &mut x;

    println!("{} {}", y, z);
}

실행 결과

   Compiling rust_part v0.1.0 (/Users/code/temp/rust_part)
error[E0499]: cannot borrow `x` as mutable more than once at a time
 --> src/main.rs:4:13
  |
3 |     let y = &mut x;
  |             ------ first mutable borrow occurs here
4 |     let z = &mut x;
  |             ^^^^^^ second mutable borrow occurs here
5 |
6 |     println!("{} {}", y, z);
  |                       - first borrow later used here

실행 시 에러가 발생하는데, 변수 x의 소유권을 한 번 이상 대여할 수 없다고 합니다. 만일 하나의 소유권을 여러 개의 변수가 빌릴 수 있다면 큰 문제가 발생할 가능성이 있습니다. 하나의 메모리를 여러 곳에서 접근할 수 있기 때문에 버그가 발생할 수 있습니다. 예를 들어 어떤 가변 레퍼런스에서 값을 변경했는데, 다른 곳에서는 변경 전의 값을 필요로 한다면 예상치 못한 결과가 나올 수 있습니다. 따라서 러스트에서는 하나의 값에 대한 여러 개의 가변 레퍼런스를 허용하지 않습니다. 하지만 단순히 레퍼런스를 여러 개 만드는 것은 문제가 없습니다.

fn main() {
    let x = String::from("Hello");
    let y = &x;
    let z = &x;

    println!("{} {}", y, z);
}

실행 결과

Hello Hello

러스트의 소유권 개념은 처음 러스트를 배우는 사람의 입장에서 정말 어렵고 복잡하게 느껴집니다. 그렇지만 컴파일러가 소유권 규칙이 위반되는 경우, 에러를 발생시키고 그에 대한 해결책을 제시해주기 때문에 생각보다 금방 익숙해질 수 있습니다.