Skip to content

4.1. What is Ownership?


소유권(Ownership)은 Rust 프로그램이 메모리를 관리하는 방법을 규정하는 일련의 규칙입니다. 모든 프로그램은 실행 중 컴퓨터의 메모리를 사용하는 방식을 관리해야 합니다. 일부 언어에는 프로그램이 실행되는 동안 더 이상 사용되지 않는 메모리를 정기적으로 찾는 가비지 컬렉션이 있으며, 다른 언어에서는 프로그래머가 명시적으로 메모리를 할당하고 해제해야 합니다. Rust는 세 번째 접근 방식을 사용합니다. 메모리는 컴파일러가 검사하는 일련의 규칙이 있는 소유권 시스템을 통해 관리됩니다. 규칙 중 하나라도 위반되면 프로그램이 컴파일되지 않습니다. 소유권 기능 중 어떤 것도 프로그램이 실행되는 동안 속도를 저하시키지 않습니다.

소유권은 많은 프로그래머에게 새로운 개념이기 때문에 익숙해지는 데 시간이 다소 걸립니다. 좋은 소식은 Rust와 소유권 시스템의 규칙에 익숙해질수록 안전하고 효율적인 코드를 자연스럽게 개발하는 것이 더 쉬워진다는 것입니다.

소유권을 이해하면 Rust의 고유한 기능을 이해할 수 있는 탄탄한 기초를 다질 수 있습니다. 이 장에서는 매우 일반적인 데이터 구조인 문자열에 초점을 맞춘 몇 가지 예제를 통해 소유권을 배우게 됩니다.

The Stack and the Heap

많은 프로그래밍 언어에서는 스택과 힙에 대해 자주 생각하지 않아도 됩니다. 하지만 Rust와 같은 시스템 프로그래밍 언어에서는 값이 스택에 있는지 힙에 있는지가 언어의 동작 방식과 특정 결정을 내려야 하는 이유에 영향을 미칩니다. 소유권의 일부는 이 장의 뒷부분에서 스택과 힙과 관련하여 설명할 것이므로, 여기서는 이에 대비하여 간략하게 설명합니다.

스택과 힙은 모두 코드에서 런타임에 사용할 수 있는 메모리의 일부이지만 구조는 서로 다릅니다. 스택은 값을 가져온 순서대로 값을 저장하고 반대 순서로 값을 제거합니다. 이를 선입선출(Last In, First Out)이라고 합니다. 접시 더미를 생각해보세요. 접시를 더 추가할 때 접시를 더미 위에 올려놓고, 접시가 필요하면 위에서 접시를 하나씩 떼어내면 됩니다. 중간이나 아래에서 접시를 추가하거나 제거하면 잘 작동하지 않습니다. 데이터를 더하는 것을 스택에 밀어 넣기, 데이터를 제거하는 것을 스택에서 꺼내기라고 합니다. 스택에 저장된 모든 데이터는 알려진 고정된 크기여야 합니다. 컴파일 시 크기를 알 수 없거나 크기가 변경될 수 있는 데이터는 힙에 저장해야 합니다.

힙은 덜 체계적입니다. 데이터를 힙에 저장할 때 일정량의 공간을 요청합니다. 메모리 할당자는 힙에서 충분히 큰 빈 자리를 찾아 사용 중인 것으로 표시하고 해당 위치의 주소인 포인터를 반환합니다. 이 프로세스를 힙에 할당하기라고 하며 그냥 할당하기라고 줄여서 부르기도 합니다(스택에 값을 푸시하는 것은 할당으로 간주되지 않음). 힙에 대한 포인터는 알려진 고정된 크기이므로 스택에 포인터를 저장할 수 있지만 실제 데이터를 원할 때는 포인터를 따라가야 합니다. 식당에 앉아 있다고 생각해보세요. 입장할 때 그룹 인원을 말하면 호스트가 모두에게 맞는 빈 테이블을 찾아서 그 자리로 안내합니다. 일행 중 누군가가 늦게 오면 호스트가 어디에 앉았는지 물어보고 찾을 수 있습니다.

스택으로 푸시하는 것이 힙에 할당하는 것보다 빠른 이유는 할당자가 새 데이터를 저장할 장소를 찾을 필요가 없고, 해당 위치가 항상 스택의 맨 위에 있기 때문입니다. 이에 비해 힙에 공간을 할당하려면 할당자가 먼저 데이터를 저장할 수 있는 충분한 공간을 찾은 다음 다음 할당을 준비하기 위해 부기 작업을 수행해야 하므로 더 많은 작업이 필요합니다.

힙의 데이터에 액세스하는 것은 포인터를 따라가야 하기 때문에 스택의 데이터에 액세스하는 것보다 느립니다. 최신 프로세서는 메모리를 덜 뛰어다니기 때문에 더 빠릅니다. 비유를 계속 이어서, 여러 테이블에서 주문을 받는 레스토랑의 서버를 생각해 보겠습니다. 다음 테이블로 이동하기 전에 한 테이블에서 모든 주문을 받는 것이 가장 효율적입니다. A 테이블에서 주문을 받은 다음 B 테이블에서 주문을 받고, 다시 A 테이블에서 주문을 받고, 다시 B 테이블에서 주문을 받는 것은 훨씬 느린 프로세스가 될 것입니다. 마찬가지로 프로세서는 다른 데이터에 가까운 데이터에 대해 작업하는 경우 작업을 더 잘 수행할 수 있습니다.

코드가 함수를 호출하면 함수에 전달된 값(힙에 있는 데이터에 대한 포인터 포함)과 함수의 로컬 변수가 스택에 푸시됩니다. 함수가 끝나면 해당 값은 스택에서 사라집니다.

코드의 어떤 부분이 힙의 어떤 데이터를 사용하는지 추적하고, 힙의 중복 데이터를 최소화하고, 힙에서 사용하지 않는 데이터를 정리하여 공간이 부족해지지 않도록 하는 것은 모두 소유권으로 해결할 수 있는 문제입니다. 소유권을 이해하면 스택과 힙에 대해 자주 생각할 필요는 없지만, 소유권의 주된 목적이 힙 데이터를 관리하는 것임을 알면 소유권이 왜 그런 식으로 작동하는지 설명하는 데 도움이 될 수 있습니다.

소유권 규칙

먼저 소유권 규칙을 살펴보겠습니다. 이 규칙을 설명하는 예제를 살펴보면서 이 규칙을 염두에 두세요:

  • Rust의 각 값에는 소유자(Owner)가 있습니다.

  • 소유자는 한 번에 한 명만 있을 수 있습니다.

  • 소유자가 범위를 벗어나면 해당 값은 삭제됩니다.

변수 범위

이제 기본 Rust 구문을 넘어섰으므로 예제에 모든 fn main() { 코드를 포함하지 않으므로, 따라 하려면 다음 예제를 main 함수 안에 수동으로 넣어야 합니다. 그 결과 예제가 좀 더 간결해져서 상용구 코드가 아닌 실제 세부 사항에 집중할 수 있습니다.

소유권의 첫 번째 예로 몇 가지 변수의 범위(Scope)를 살펴보겠습니다. 범위는 프로그램 내에서 항목이 유효한 범위입니다. 다음 변수를 예로 들어보겠습니다:

let s = "hello";

변수 s는 문자열 리터럴을 나타내며, 여기서 문자열 값은 프로그램의 텍스트에 하드코딩됩니다. 변수는 선언된 시점부터 현재 범위가 끝날 때까지 유효합니다. 목록 4-1은 변수 s가 유효할 수 있는 위치를 주석으로 표시한 프로그램을 보여줍니다.

    {                      // s is not valid here, it’s not yet declared
        let s = "hello";   // s is valid from this point forward

        // do stuff with s
    }                      // this scope is now over, and s is no longer valid

목록 4-1: A variable and the scope in which it is valid

다시 말해, 여기에는 두 가지 중요한 시점이 있습니다:

  • s가 범위에 들어오면 유효합니다.

  • 범위를 벗어날 때까지 유효합니다.

이 시점에서 범위와 변수가 유효한 시점의 관계는 다른 프로그래밍 언어의 관계와 유사합니다. 이제 이러한 이해를 바탕으로 문자열 타입을 소개해 보겠습니다.

String 타입

소유권 규칙을 설명하기 위해서는 3장의 "데이터 타입" 섹션에서 다룬 것보다 더 복잡한 데이터 타입이 필요합니다. 앞에서 다룬 타입은 크기가 알려져 있고, 스택에 저장했다가 해당 범위가 끝나면 스택에서 꺼낼 수 있으며, 코드의 다른 부분에서 다른 범위에서 동일한 값을 사용해야 하는 경우 빠르고 간단하게 복사하여 독립적인 새 인스턴스를 만들 수 있습니다. 하지만 우리는 힙에 저장된 데이터를 살펴보고 Rust가 언제 해당 데이터를 정리해야 하는지 알아내는 방법을 살펴보고자 하며, String 타입이 좋은 예입니다.

여기서는 소유권과 관련된 String 부분을 집중적으로 살펴보겠습니다. 이러한 측면은 표준 라이브러리에서 제공하든 사용자가 직접 만들든 다른 복잡한 데이터 타입에도 적용됩니다. 8장에서 String에 대해 더 자세히 설명하겠습니다.

이미 문자열 값을 프로그램에 하드코딩하는 문자열 리터럴에 대해 살펴봤습니다. 문자열 리터럴은 편리하지만 텍스트를 사용해야 하는 모든 상황에 적합하지는 않습니다. 한 가지 이유는 불변이기 때문입니다. 또 다른 이유는 코드를 작성할 때 모든 문자열 값을 알 수 없다는 것입니다. 예를 들어 사용자 입력을 받아 저장하려는 경우 어떻게 해야 할까요? 이러한 상황에 대비해 Rust에는 두 번째 문자열 타입인 String이 있습니다. 이 타입은 힙에 할당된 데이터를 관리하므로 컴파일 시점에 알 수 없는 양의 텍스트를 저장할 수 있습니다. 다음과 같이 from 함수를 사용하여 문자열 리터럴에서 문자열을 생성할 수 있습니다:

let s = String::from("hello");

이중 콜론 :: 연산자를 사용하면 string_from과 같은 이름을 사용하는 대신 String 타입 아래에서 이 특정 from 함수의 네임스페이스를 지정할 수 있습니다. 이 구문에 대해서는 5장의 "메서드 구문" 섹션과 7장의 "모듈 트리에서 항목을 참조하는 경로"에서 모듈을 사용한 네임스페이스에 대해 설명할 때 자세히 설명하겠습니다.

이러한 종류의 문자열은 변경할 수 있습니다:

    let mut s = String::from("hello");

    s.push_str(", world!"); // push_str() appends a literal to a String

    println!("{}", s); // This will print `hello, world!`

그렇다면 여기서 차이점은 무엇일까요? String은 변할 수 있지만 리터럴은 변할 수 없는 이유는 무엇일까요? 이 두 가지 타입의 차이점은 메모리를 처리하는 방식에 있습니다.

메모리 및 할당

문자열 리터럴의 경우 컴파일 시점에 내용을 알 수 있으므로 텍스트가 최종 실행 파일에 직접 하드코딩됩니다. 이것이 바로 문자열 리터럴이 빠르고 효율적인 이유입니다. 하지만 이러한 속성은 문자열 리터럴의 불변성에서 비롯된 것입니다. 안타깝게도 컴파일 시에는 크기를 알 수 없고 프로그램을 실행하는 동안 크기가 변경될 수 있는 각 텍스트 조각에 대해 바이너리에 메모리 덩어리를 넣을 수는 없습니다.

String 타입을 사용하면 변경 가능하고 크기가 커질 수 있는 텍스트를 지원하기 위해 컴파일 시점에 알 수 없는 양의 메모리를 힙에 할당하여 내용을 저장해야 합니다. 즉:

  • 메모리는 런타임에 메모리 할당자에게 요청해야 합니다.

  • String 작업이 끝나면 이 메모리를 할당자에게 반환하는 방법이 필요합니다.

첫 번째 부분은 우리가 처리합니다. String::from을 호출하면 해당 구현이 필요한 메모리를 요청합니다. 이는 프로그래밍 언어에서 거의 보편적인 방식입니다.

하지만 두 번째 부분은 다릅니다. 가비지 컬렉터(GC)가 있는 언어에서는 GC가 더 이상 사용되지 않는 메모리를 추적하고 정리하므로 우리가 신경 쓸 필요가 없습니다. GC가 없는 대부분의 언어에서는 메모리가 더 이상 사용되지 않는 시점을 식별하고 요청할 때와 마찬가지로 코드를 호출하여 명시적으로 메모리를 해제하는 것이 개발자의 책임입니다. 이 작업을 올바르게 수행하는 것은 역사적으로 어려운 프로그래밍 문제였습니다. 잊어버리면 메모리를 낭비하게 됩니다. 너무 일찍 해제하면 유효하지 않은 변수를 갖게 됩니다. 두 번 수행하면 그것도 버그가 됩니다. 정확히 하나의 allocate와 정확히 하나의 free가 짝을 이루어야 합니다.

Rust는 다른 경로를 사용합니다. 메모리를 소유한 변수가 범위를 벗어나면 메모리가 자동으로 반환됩니다. 다음은 문자열 리터럴 대신 String을 사용하는 목록 4-1의 범위 예제 버전입니다:

    {
        let s = String::from("hello"); // s is valid from this point forward

        // do stuff with s
    }                                  // this scope is now over, and s is no
                                       // longer valid

String에 필요한 메모리를 할당자에게 반환할 수 있는 자연스러운 시점이 있는데, 바로 s가 범위를 벗어날 때입니다. 변수가 범위를 벗어나면 Rust는 우리를 위해 특별한 함수를 호출합니다. 이 함수를 drop이라고 하며, String 작성자가 메모리를 반환하는 코드를 넣을 수 있는 곳입니다. Rust는 닫는 중괄호에서 자동으로 drop을 호출합니다.

Note

C++에서는 항목의 수명이 다할 때 리소스를 할당 해제하는 이 패턴을 리소스 획득 초기화(RAII)라고 부르기도 합니다. RAII 패턴을 사용해 본 적이 있다면 Rust의 drop 함수가 익숙할 것입니다.

이 패턴은 Rust 코드 작성 방식에 큰 영향을 미칩니다. 지금은 간단해 보일 수 있지만, 힙에 할당된 데이터를 여러 변수가 사용하게 하려는 복잡한 상황에서는 코드의 동작이 예상치 못한 결과를 초래할 수 있습니다. 이제 이러한 상황 중 몇 가지를 살펴보겠습니다.

이동(Move)과 상호작용하는 변수 및 데이터

Rust에서는 여러 변수가 동일한 데이터와 다양한 방식으로 상호 작용할 수 있습니다. 목록 4-2의 정수를 사용한 예제를 살펴보겠습니다.

    let x = 5;
    let y = x;

목록 4-2: Assigning the integer value of variable x to y

이것이 무엇을 하는지 짐작할 수 있을 것입니다: "값 5x에 바인딩한 다음 x에 있는 값의 복사본을 만들어 y에 바인딩합니다." 이제 xy라는 두 개의 변수가 생겼고 둘 다 5와 같습니다. 정수는 알려진 고정된 크기의 단순한 값이고, 이 두 개의 5 값이 스택에 푸시되기 때문에 실제로 이런 일이 일어나고 있습니다.

이제 String 버전을 살펴보겠습니다:

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

이것은 매우 유사해 보이므로 작동 방식이 동일할 것이라고 가정할 수 있습니다. 즉, 두 번째 줄이 s1의 값을 복사하여 s2에 바인딩하는 것입니다. 하지만 실제로는 그렇지 않습니다.

그림 4-1을 살펴보면 내부에서 String에 어떤 일이 일어나는지 알 수 있습니다. 왼쪽에 표시된 것처럼 String은 문자열의 내용을 담고 있는 메모리에 대한 포인터, 길이, 용량의 세 부분으로 구성됩니다. 이 데이터 그룹은 스택에 저장됩니다. 오른쪽은 힙에 있는 메모리로 내용을 담고 있습니다.

001

그림 4-1: Representation in memory of a String holding the value "hello" bound to s1

길이는 String의 콘텐츠가 현재 사용하고 있는 메모리 양(바이트)입니다. 용량은 문자열이 할당자로부터 받은 총 메모리 양(바이트)입니다. 길이와 용량의 차이는 중요하지만 이 컨텍스트에서는 중요하지 않으므로 지금은 용량을 무시해도 괜찮습니다.

s1s2에 할당하면 String 데이터가 복사되므로 스택에 있는 포인터, 길이, 용량이 복사됩니다. 포인터가 가리키는 힙의 데이터는 복사하지 않습니다. 즉, 메모리의 데이터 표현은 그림 4-2와 같습니다.

002

그림 4-2: Representation in memory of the variable s2 that has a copy of the pointer, length, and capacity of s1

그림 4-3은 Rust가 힙 데이터도 복사하는 경우 메모리가 어떻게 보일지 보여줍니다. Rust가 이 작업을 수행했다면, 힙의 데이터가 큰 경우 런타임 성능 측면에서 s2 = s1 연산이 매우 비쌀 수 있습니다.

003

그림 4-3: Another possibility for what s2 = s1 might do if Rust copied the heap data as well

앞서 변수가 범위를 벗어나면 Rust가 자동으로 drop 함수를 호출하고 해당 변수에 대한 힙 메모리를 정리한다고 설명했습니다. 하지만 그림 4-2는 두 데이터 포인터가 모두 같은 위치를 가리키고 있음을 보여줍니다. 이것이 문제입니다. s2s1이 범위를 벗어나면 둘 다 같은 메모리를 해제하려고 시도합니다. 이를 이중 해제(Double Free) 오류라고 하며 앞서 언급한 메모리 안전 버그 중 하나입니다. 메모리를 두 번 해제하면 메모리가 손상되어 잠재적으로 보안 취약점이 발생할 수 있습니다.

메모리 안전을 보장하기 위해 let s2 = s1; 줄 뒤에는 s1이 더 이상 유효하지 않은 것으로 간주합니다. 따라서 s1이 범위를 벗어날 때 Rust는 아무것도 해제할 필요가 없습니다. s2가 생성된 후 s1을 사용하려고 하면 어떻게 되는지 확인해 보세요:

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

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

Rust에서 유효하지 않은 참조를 사용할 수 없기 때문에 이와 같은 오류가 발생합니다:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:28
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("{}, world!", s1);
  |                            ^^ value borrowed here after move
  |
  = note: this error originates in the macro `$crate::format_args_nl` which comes from the expansion of the macro `println` (in Nightly builds, run with -Z macro-backtrace for more info)

For more information about this error, try `rustc --explain E0382`.
error: could not compile `ownership` due to previous error

다른 언어로 작업하면서 얕은 복사(Shallow Copy)깊은 복사(Deep Copy)라는 용어를 들어본 적이 있다면 데이터를 복사하지 않고 포인터, 길이, 용량을 복사하는 개념이 얕은 복사처럼 들릴 수 있습니다. 하지만 Rust에서는 첫 번째 변수도 무효화하기 때문에 얕은 복사라고 하는 대신 이동(Move)이라고 합니다. 이 예제에서는 s1s2이동되었다고 말할 수 있습니다. 실제로 어떤 일이 일어나는지는 그림 4-4에 나와 있습니다.

004

그림 4-4: Representation in memory after s1 has been invalidated

이제 문제가 해결되었습니다. s2만 유효하므로 범위를 벗어날 때 이 메모리만으로도 메모리를 확보할 수 있습니다.

또한, 여기에는 암시적인 설계 선택이 있습니다: Rust는 데이터의 "깊은" 복사본을 자동으로 생성하지 않습니다. 따라서 자동 복사는 런타임 성능 측면에서 비용이 적게 든다고 가정할 수 있습니다.

클론(Clone)과 상호작용하는 변수 및 데이터

스택 데이터뿐만 아니라 String의 힙 데이터까지 깊숙이 복사하고 싶다면 clone이라는 일반적인 메서드를 사용할 수 있습니다. 5장에서 메서드 구문에 대해 설명하겠지만, 메서드는 많은 프로그래밍 언어에서 흔히 볼 수 있는 기능이기 때문에 아마 한 번쯤은 보셨을 것입니다.

다음은 clone 메서드가 실제로 사용되는 예제입니다:

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

    println!("s1 = {}, s2 = {}", s1, s2);

이 방법은 정상적으로 작동하며 그림 4-3에 표시된 동작을 명시적으로 생성하며 힙 데이터가 복사됩니다.

clone에 대한 호출이 표시되면 임의의 코드가 실행되고 있으며 해당 코드가 비용이 많이 들 수 있음을 알 수 있습니다. 이는 뭔가 다른 일이 진행되고 있다는 시각적 지표입니다.

스택 전용 데이터: 복사(Copy)

아직 언급하지 않은 또 다른 것이 있습니다. 정수를 사용하는 이 코드(일부가 목록 4-2에 표시됨)는 작동하며 유효합니다:

    let x = 5;
    let y = x;

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

하지만 이 코드는 방금 배운 내용과 모순되는 것처럼 보입니다. clone 호출이 없지만 x는 여전히 유효하며 y로 이동되지 않았습니다.

그 이유는 컴파일 시 크기가 알려진 정수와 같은 타입은 스택에 완전히 저장되므로 실제 값의 복사본을 빠르게 만들 수 있기 때문입니다. 즉, 변수 y를 생성한 후 x가 유효하지 않게 할 이유가 없다는 뜻입니다. 즉, 여기서는 깊은 복사와 얕은 복사 사이에 차이가 없으므로 clone을 호출해도 일반적인 얕은 복사와 달라지는 것이 없으므로 생략해도 됩니다.

Rust에는 정수처럼 스택에 저장된 타입에 Copy 트레잇이라는 특수 어노테이션이 있습니다(10장에서 트레잇에 대해 자세히 설명하겠습니다). 타입이 Copy 트레잇을 구현하면 이를 사용하는 변수는 이동하지 않고 사소하게 복사되어 다른 변수에 할당된 후에도 여전히 유효합니다.

타입 또는 그 일부가 Drop 트레잇을 구현한 경우 Rust에서는 Copy로 타입에 주석을 달 수 없습니다. 값이 범위를 벗어날 때 타입에 특별한 일이 발생해야 하는데 해당 타입에 Copy 어노테이션을 추가하면 컴파일 타임 오류가 발생합니다. 타입에 Copy 어노테이션을 추가하여 특성을 구현하는 방법에 대해 알아보려면 부록 C의 "파생 가능한 트레잇"을 참조하세요.

그렇다면 어떤 타입이 Copy 트레잇을 구현할까요? 해당 타입에 대한 문서를 확인하여 확인할 수 있지만, 일반적으로 단순한 스칼라 값 그룹은 모두 Copy를 구현할 수 있으며 할당이 필요하거나 어떤 형태의 리소스인 것은 Copy를 구현할 수 없습니다. 다음은 Copy를 구현하는 몇 가지 타입입니다:

  • 모든 정수 타입(예: u32).

  • truefalse 값이 있는 부울 타입인 bool.

  • 모든 부동 소수점 타입(예: f64).

  • 문자 타입인 char.

  • Copy도 구현하는 타입만 포함된 경우의 튜플. 예를 들어 (i32, i32)Copy를 구현하지만 (i32, String)은 구현하지 않습니다.

소유권 및 함수

함수에 값을 전달하는 메커니즘은 변수에 값을 할당할 때의 메커니즘과 유사합니다. 변수를 함수에 전달하면 할당할 때와 마찬가지로 이동 또는 복사됩니다. 목록 4-3에는 변수의 범위 안팎을 보여주는 몇 가지 주석이 있는 예제가 있습니다.

src/main.rs
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

목록 4-3: Functions with ownership and scope annotated

takes_ownership을 호출한 후에 s를 사용하려고 하면 Rust는 컴파일 타임 오류를 발생시킵니다. 이러한 정적 검사는 실수로부터 우리를 보호합니다. sx를 사용하는 코드를 main에 추가하여 사용할 수 있는 곳과 소유권 규칙으로 인해 사용할 수 없는 곳을 확인해 보세요.

반환 값 및 범위

반환 값은 소유권을 이전할 수도 있습니다. 목록 4-4는 목록 4-3과 유사한 어노테이션이 있는 일부 값을 반환하는 함수의 예를 보여줍니다.

src/main.rs
fn main() {
    let s1 = gives_ownership();         // gives_ownership moves its return
                                        // value into s1

    let s2 = String::from("hello");     // s2 comes into scope

    let s3 = takes_and_gives_back(s2);  // s2 is moved into
                                        // takes_and_gives_back, which also
                                        // moves its return value into s3
} // Here, s3 goes out of scope and is dropped. s2 was moved, so nothing
  // happens. s1 goes out of scope and is dropped.

fn gives_ownership() -> String {             // gives_ownership will move its
                                             // return value into the function
                                             // that calls it

    let some_string = String::from("yours"); // some_string comes into scope

    some_string                              // some_string is returned and
                                             // moves out to the calling
                                             // function
}

// This function takes a String and returns one
fn takes_and_gives_back(a_string: String) -> String { // a_string comes into
                                                      // scope

    a_string  // a_string is returned and moves out to the calling function
}

목록 4-4: Transferring ownership of return values

변수의 소유권은 매번 동일한 패턴을 따르는데, 값을 다른 변수에 할당하면 변수가 이동합니다. 힙에 데이터가 포함된 변수가 범위를 벗어나면 데이터의 소유권이 다른 변수로 이동되지 않는 한 값이 drop 방식으로 정리됩니다.

이 방법은 작동하지만 모든 함수에서 소유권을 가져온 다음 소유권을 반환하는 것은 약간 지루합니다. 함수가 값을 사용하되 소유권을 가져가지 않도록 하려면 어떻게 해야 할까요? 함수를 다시 사용하려면 전달한 모든 데이터를 다시 전달해야 하고, 반환하려는 함수 본문에서 생성된 데이터도 함께 반환해야 한다는 점이 상당히 번거롭습니다.

Rust에서는 목록 4-5에 표시된 것처럼 튜플을 사용하여 여러 값을 반환할 수 있습니다.

src/main.rs
fn main() {
    let s1 = String::from("hello");

    let (s2, len) = calculate_length(s1);

    println!("The length of '{}' is {}.", s2, len);
}

fn calculate_length(s: String) -> (String, usize) {
    let length = s.len(); // len() returns the length of a String

    (s, length)
}

목록 4-5: Returning ownership of parameters

그러나 이것은 일반적인 개념에 비해 너무 많은 형식과 많은 작업이 필요합니다. 다행히도 Rust에는 소유권을 이전하지 않고 값을 사용할 수 있는 기능인 참조(References)가 있습니다.


References