소유권이란?

완료됨

Rust에는 메모리를 관리하는 소유권 시스템이 포함되어 있습니다. 컴파일 시간에 소유권 시스템은 규칙 세트를 검사하여 소유권 기능을 통해 속도를 그대로 유지하면서 프로그램을 실행할 수 있는지 확인합니다.

소유권을 이해하기 위해 우선 Rust의 범위 지정 규칙 및 이동 의미 체계를 살펴보겠습니다.

범위 지정 규칙

대부분의 다른 프로그래밍 언어와 마찬가지로 Rust에서 변수는 특정 ‘범위’ 내에서만 유효합니다. Rust에서 범위는 대개 중괄호({})를 사용하여 표시됩니다. 공통 범위에는 함수 본문과 if, else, match 분기가 포함됩니다.

참고

Rust에서는 “변수”를 “바인딩”이라고 부르는 경우가 많습니다. Rust의 “변수”는 전형적인 변수가 아니기 때문입니다. 기본적으로 변경할 수 없기 때문에 자주 변경되지 않습니다. 일반적으로 이름이 데이터에 “바인딩”된다고 생각하므로 “바인딩”이란 이름을 대신 사용합니다. 이 모듈에서는 "변수" 및 "바인딩"이라는 용어가 혼용됩니다.

문자열인 mascot 변수가 범위 내에 정의되어 있다고 가정해 봅시다.

// `mascot` is not valid and cannot be used here, because it's not yet declared.
{
    let mascot = String::from("ferris");   // `mascot` is valid from this point forward.
    // do stuff with `mascot`.
}
// this scope is now over, so `mascot` is no longer valid and cannot be used.

범위 밖에서 mascot를 사용하려는 경우 이와 같은 오류가 발생합니다.

{
    let mascot = String::from("ferris");
}
println!("{}", mascot);
    error[E0425]: cannot find value `mascot` in this scope
     --> src/main.rs:5:20
      |
    5 |     println!("{}", mascot);
      |                    ^^^^^^ not found in this scope

Rust 플레이그라운드에서 온라인으로 이 예제를 실행할 수 있습니다.

변수는 선언된 지점부터 해당 범위의 끝까지 유효합니다.

소유권 및 삭제

Rust에서는 범위의 개념이 약간 변형됩니다. 개체가 범위를 벗어날 때마다 "삭제"됩니다. 변수를 삭제하면 연결된 모든 리소스가 해제됩니다. 파일 변수의 경우 파일은 결국 닫힙니다. 할당된 메모리가 연결된 변수의 경우 메모리가 해제됩니다.

Rust에서 바인딩을 삭제할 때 해제되는 항목이 “연결”되어 있는 바인딩은 해당 항목을 “소유”하고 있다고 간주됩니다.

이전 예제에서 mascot 변수는 연결된 String 데이터를 소유합니다. String 자체는 해당 문자열의 문자를 포함하는 힙 할당 메모리를 소유합니다. 범위가 끝나면 mascot가 “삭제”되고, 변수가 소유한 String이 삭제되며, 마지막으로 String이 소유한 메모리가 해제됩니다.

{
    let mascot = String::from("ferris");
}
// mascot is dropped here. The string data memory will be freed here.

이동 의미 체계

하지만 범위가 끝날 때 변수와 연결된 항목을 삭제하지 않으려는 경우도 있습니다. 대신, 항목의 소유권을 한 바인딩에서 다른 바인딩으로 이전하려고 합니다.

가장 간단한 예는 새 바인딩을 선언하는 경우입니다.

{
    let mascot = String::from("ferris");
    // transfer ownership of mascot to the variable ferris.
    let ferris = mascot;
}
// ferris is dropped here. The string data memory will be freed here.

유의해야 할 중요한 사항은 소유권이 이전되고 나면 이전 변수는 더 이상 유효하지 않다는 것입니다. 이전 예제에서 String의 소유권을 mascot에서 ferris로 이전한 후에는 더 이상 mascot 변수를 사용할 수 없습니다.

Rust에서는 “소유권 이전”을 “이동”이라고 합니다. 즉, String 값의 소유권은 mascot에서 ferris이동되었습니다.

Stringmascot에서 ferris로 이동된 후 mascot를 사용하려고 하면 컴파일러가 코드를 컴파일하지 못합니다.

{
    let mascot = String::from("ferris");
    let ferris = mascot;
    println!("{}", mascot) // We'll try to use mascot after we've moved ownership of the string data from mascot to ferris.
}
error[E0382]: borrow of moved value: `mascot`
 --> src/main.rs:4:20
  |
2 |     let mascot = String::from("ferris");
  |         ------ move occurs because `mascot` has type `String`, which does not implement the `Copy` trait
3 |     let ferris = mascot;
  |                  ------ value moved here
4 |     println!("{}", mascot);
  |                    ^^^^^^ value borrowed here after move

이 결과를 “이동 후 사용” 컴파일 오류라고 합니다.

중요

Rust에서는 한 번에 하나의 항목만 데이터를 ‘소유’할 수 있습니다.

함수의 소유권

문자열이 함수에 인수로 전달되는 예를 살펴보겠습니다. 함수에 인수로 항목을 전달하면 해당 항목이 함수로 이동됩니다.

fn process(input: String) {}

fn caller() {
    let s = String::from("Hello, world!");
    process(s); // Ownership of the string in `s` moved into `process`
    process(s); // Error! ownership already moved.
}

컴파일러는 s 값이 이동했다고 경고합니다.

    error[E0382]: use of moved value: `s`
     --> src/main.rs:6:13
      |
    4 |     let s = String::from("Hello, world!");
      |         - move occurs because `s` has type `String`, which does not implement the `Copy` trait
    5 |     process(s); // Transfers ownership of `s` to `process`
      |             - value moved here
    6 |     process(s); // Error! ownership already transferred.
      |             ^ value used here after move

앞선 코드 조각에서 확인할 수 있듯 process에 대한 첫 번째 호출 시 변수 s의 소유권을 이전합니다. 컴파일러는 소유권을 추적하므로 process에 대한 두 번째 호출 시에는 오류가 발생합니다. 리소스가 이동된 후에는 이전 소유권을 더 이상 사용할 수 없습니다.

이 패턴은 Rust 코드를 작성하는 방식에 큰 영향을 미칩니다. Rust에서 제안하는 메모리 보장에 있어 중요한 역할을 합니다.

다른 프로그래밍 언어에서 s 변수의 String 값은 함수로 전달되기 전에 암시적으로 복사될 수 있습니다. 그러나 Rust에서는 이 작업이 발생하지 않습니다.

Rust에서 소유권 이전(이동)은 기본 동작입니다.

이동 대신 복사

이전 예제에서는 (다소 유익한) 컴파일러 오류 메시지에서 Copy 특성에 대한 언급을 알아차렸을 수 있습니다. 특성에 대해서는 아직 설명하지 않았지만 Copy 특성을 구현하는 값은 이동되지 않지만 복사됩니다.

Copy 특성을 구현하는 값인 u32를 살펴봅시다. 다음 코드는 손상된 코드를 미러링하지만 문제없이 컴파일됩니다.

fn process(input: u32) {}

fn caller() {
    let n = 1u32;
    process(n); // Ownership of the number in `n` copied into `process`
    process(n); // `n` can be used again because it wasn't moved, it was copied.
}

숫자 같은 단순 형식은 형식을 복사합니다. 해당 형식은 Copy 특성을 구현합니다. 즉, 이동하는 대신 복사됩니다. 대부분의 단순 형식에 대해 동일한 작업이 수행됩니다. 숫자 복사는 비용이 저렴하므로 이 값을 복사하는 것이 좋습니다. 문자열, 벡터 또는 기타 복합 형식을 복사하는 경우에는 비용이 높을 수 있으므로 해당 형식은 Copy 특성을 구현하지 않고 이동됩니다.

Copy를 구현하지 않는 형식 복사

이전 예제에서 확인한 오류를 해결하는 한 가지 방법은 형식이 이동되기 전에 명시적으로 형식을 복사하는 것으로, Rust에서는 복제라고 합니다. .clone를 호출하면 메모리가 중복되고 새 값이 생성됩니다. 새 값이 이동되므로 이전 값을 계속 사용할 수 있습니다.

fn process(s: String) {}

fn main() {
    let s = String::from("Hello, world!");
    process(s.clone()); // Passing another value, cloned from `s`.
    process(s); // s was never moved and so it can still be used.
}

이 접근 방식은 유용할 수 있지만 clone을 호출할 때마다 전체 데이터 복사본이 생성되므로 코드 속도가 느려질 수 있습니다. 이 메서드에는 종종 메모리 할당 또는 기타 비용이 많이 드는 작업이 포함됩니다. ‘참조’를 사용하여 값을 “대여”하는 경우 이 비용을 피할 수 있습니다. 다음 단원에서 참조를 사용하는 방법을 알아봅니다.