Skip to content

4.2. References and Borrowing


목록 4-5의 튜플 코드의 문제는 Stringcalculate_length로 이동했기 때문에 calculate_length를 호출한 후에도 String을 계속 사용할 수 있도록 호출 함수에 String을 반환해야 한다는 것입니다. 대신 String 값에 대한 참조를 제공할 수 있습니다. 참조(Reference)는 해당 주소에 저장된 데이터에 액세스하기 위해 따라갈 수 있는 주소라는 점에서 포인터와 비슷하지만, 해당 데이터는 다른 변수에 의해 소유됩니다. 포인터와 달리 참조는 해당 참조의 라이프타임 동안 특정 유형의 유효한 값을 가리키도록 보장됩니다.

다음은 값의 소유권을 갖는 대신 객체에 대한 참조를 매개변수로 사용하는 calculate_length 함수를 정의하고 사용하는 방법입니다:

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

    let len = calculate_length(&s1);

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

fn calculate_length(s: &String) -> usize {
    s.len()
}

먼저, 변수 선언과 함수 반환 값의 모든 튜플 코드가 사라진 것을 확인합니다. 둘째, calculate_length&s1을 전달하고 그 정의에서 String이 아닌 &String을 취하고 있음을 주목하세요. 이러한 앰퍼샌드는 참조(References)를 나타내며, 이를 통해 소유권을 가지지 않고도 어떤 값을 참조할 수 있습니다. 그림 4-5는 이 개념을 설명합니다.

001

그림 4-5: A diagram of &String s pointing at String s1

Note

&를 사용하여 참조하는 것과 반대되는 것이 역참조(Dereferencing)이며, 역참조 연산자 *를 사용하여 수행됩니다. 8장에서 역참조 연산자의 몇 가지 용도를 살펴보고 15장에서 역참조에 대한 자세한 내용을 설명하겠습니다.

여기서 함수 호출을 자세히 살펴보겠습니다:

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

    let len = calculate_length(&s1);

&s1 구문을 사용하면 s1의 값을 참조하지만 소유하지 않는 참조를 만들 수 있습니다. 값을 소유하지 않기 때문에 참조가 더 이상 사용되지 않을 때 참조가 가리키는 값이 삭제되지 않습니다.

마찬가지로 함수의 시그니처에는 &를 사용하여 매개변수 s의 유형이 참조임을 나타냅니다. 몇 가지 설명 주석을 추가해 보겠습니다:

fn calculate_length(s: &String) -> usize { // s is a reference to a String
    s.len()
} // Here, s goes out of scope. But because it does not have ownership of what
  // it refers to, it is not dropped.

변수 s가 유효한 범위는 함수 매개변수의 범위와 동일하지만, 참조가 가리키는 값은 s가 더 이상 사용되지 않을 때 삭제되지 않는데, 이는 s에 소유권이 없기 때문입니다. 함수가 실제 값 대신 참조를 매개변수로 사용하는 경우, 소유권이 없으므로 소유권을 반환하기 위해 값을 반환할 필요가 없습니다.

우리는 참조를 생성하는 행위를 참조 차용(Borrowing)이라고 부릅니다. 실생활에서와 마찬가지로 어떤 사람이 무언가를 소유하고 있다면 그 사람에게서 빌릴 수 있습니다. 빌린 후에는 돌려주어야 합니다. 소유권이 없는 것이죠.

그렇다면 빌린 것을 수정하려고 하면 어떻게 될까요? 목록 4-6의 코드를 시도해 보세요. 작동하지 않을 것입니다.

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

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

목록 4-6: Attempting to modify a borrowed value

오류는 다음과 같습니다:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0596]: cannot borrow `*some_string` as mutable, as it is behind a `&` reference
 --> src/main.rs:8:5
  |
7 | fn change(some_string: &String) {
  |                        ------- help: consider changing this to be a mutable reference: `&mut String`
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable

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

변수가 기본적으로 불변인 것처럼 참조도 마찬가지입니다. 참조가 있는 것을 수정할 수 없습니다.

변경 가능한 참조

목록 4-6의 코드를 수정하여 변경 가능한 참조를 대신 사용하는 몇 가지 작은 조정만으로 차용한 값을 수정할 수 있습니다:

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

    change(&mut s);
}

fn change(some_string: &mut String) {
    some_string.push_str(", world");
}

먼저 smut로 변경합니다. 그런 다음 change 함수를 호출하는 위치에 &mut s로 변경 가능한 참조를 생성하고, some_string: &mut String으로 변경 가능한 참조를 허용하도록 함수 시그니처를 업데이트합니다. 이렇게 하면 change 함수가 차용한 값을 변경한다는 것을 매우 명확하게 알 수 있습니다.

변경 가능한 참조에는 한 가지 큰 제한이 있습니다. 값에 대한 변경 가능한 참조가 있으면 해당 값에 대한 다른 참조를 가질 수 없다는 것입니다. s에 대한 두 개의 변경 가능한 참조를 만들려고 시도하는 이 코드는 실패합니다:

src/main.rs
    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

오류는 다음과 같습니다:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0499]: cannot borrow `s` as mutable more than once at a time
 --> src/main.rs:5:14
  |
4 |     let r1 = &mut s;
  |              ------ first mutable borrow occurs here
5 |     let r2 = &mut s;
  |              ^^^^^^ second mutable borrow occurs here
6 |
7 |     println!("{}, {}", r1, r2);
  |                        -- first borrow later used here

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

이 오류는 s를 한 번에 두 번 이상 가변으로 빌릴 수 없기 때문에 이 코드가 유효하지 않다고 말합니다. 첫 번째 가변 참조는 r1에 있으며 println!에서 사용될 때까지 지속되어야 하지만, 해당 가변 참조를 생성하고 사용하는 사이에 r1과 동일한 데이터를 차용하는 다른 가변 참조를 r2에 생성하려고 했습니다.

동일한 데이터에 대한 변경 가능한 참조를 동시에 여러 개 만들 수 없도록 하는 제한으로 인해 변경은 허용되지만 매우 제어된 방식으로 이루어집니다. 대부분의 언어에서는 원할 때마다 복제를 허용하기 때문에 새로운 러스타시언이 어려움을 겪을 수 있는 부분입니다. 이러한 제한의 장점은 컴파일 시 데이터 경합을 방지할 수 있다는 것입니다. 데이터 경합(Data Race)은 경쟁 조건과 유사하며 다음 세 가지 동작이 발생할 때 발생합니다:

  • 두 개 이상의 포인터가 동시에 동일한 데이터에 액세스하는 경우.

  • 포인터 중 하나 이상이 데이터에 쓰는 데 사용됩니다.

  • 데이터에 대한 액세스를 동기화하는 데 사용되는 메커니즘이 없습니다.

데이터 경합은 정의되지 않은 동작을 유발하고 런타임에 이를 추적하려고 할 때 진단 및 수정이 어려울 수 있습니다. Rust는 데이터 경합이 있는 코드의 컴파일을 거부하여 이 문제를 방지합니다.

항상 그렇듯이 중괄호를 사용하여 새 범위를 생성하면 동시 참조가 아닌 여러 개의 변경 가능한 참조를 허용할 수 있습니다:

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

    {
        let r1 = &mut s;
    } // r1 goes out of scope here, so we can make a new reference with no problems.

    let r2 = &mut s;

Rust는 변경 가능한 참조와 변경 불가능한 참조를 결합할 때 비슷한 규칙을 적용합니다. 이 코드는 오류를 발생시킵니다:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    let r3 = &mut s; // BIG PROBLEM

    println!("{}, {}, and {}", r1, r2, r3);

오류는 다음과 같습니다:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as immutable
 --> src/main.rs:6:14
  |
4 |     let r1 = &s; // no problem
  |              -- immutable borrow occurs here
5 |     let r2 = &s; // no problem
6 |     let r3 = &mut s; // BIG PROBLEM
  |              ^^^^^^ mutable borrow occurs here
7 |
8 |     println!("{}, {}, and {}", r1, r2, r3);
  |                                -- immutable borrow later used here

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

또한 동일한 값에 대한 불변 참조가 있는 동안 변경 가능한 참조를 가질 수 없습니다.

변경 불가능한 참조를 사용하는 사용자는 갑자기 값이 변경될 것이라고 예상하지 않습니다. 그러나 데이터를 읽기만 하는 사람은 다른 사람의 데이터 읽기에 영향을 줄 수 없기 때문에 여러 개의 불변 참조가 허용됩니다.

참조의 범위는 참조가 도입된 위치에서 시작하여 해당 참조가 마지막으로 사용된 시점까지 계속된다는 점에 유의하세요. 예를 들어, 이 코드는 변경 가능한 참조가 도입되기 전에 불변 참조의 마지막 사용인 println!이 발생하기 때문에 컴파일됩니다:

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

    let r1 = &s; // no problem
    let r2 = &s; // no problem
    println!("{} and {}", r1, r2);
    // variables r1 and r2 will not be used after this point

    let r3 = &mut s; // no problem
    println!("{}", r3);

불변 참조 r1r2의 범위는 마지막으로 사용된 println! 이후, 즉 변경 가능한 참조 r3이 생성되기 전에 끝납니다. 이러한 범위는 겹치지 않으므로 이 코드는 허용됩니다. 컴파일러는 범위가 끝나기 전 지점에서 참조가 더 이상 사용되지 않는다는 것을 알 수 있습니다.

차용 오류는 때때로 실망스러울 수 있지만, Rust 컴파일러가 잠재적인 버그를 조기에(런타임이 아닌 컴파일 타임에) 지적하고 문제가 있는 위치를 정확히 알려준다는 점을 기억하세요. 그러면 데이터가 생각했던 것과 다른 이유를 추적할 필요가 없습니다.

댕글링 참조

포인터를 사용하는 언어에서는 해당 메모리에 대한 포인터를 유지하면서 일부 메모리를 해제하여 다른 사람에게 제공되었을 수 있는 메모리 내 위치를 참조하는 포인터인 댕글링 포인터(Dangling Pointer)를 실수로 생성하기 쉽습니다. 반면 Rust에서는 컴파일러가 참조가 댕글링 참조가 되지 않도록 보장합니다. 즉, 일부 데이터에 대한 참조가 있는 경우 컴파일러는 데이터에 대한 참조가 범위를 벗어나기 전에 데이터가 범위를 벗어나지 않도록 보장합니다.

댕글링 참조를 생성하여 Rust가 어떻게 컴파일 타임 오류로 이를 방지하는지 확인해 보겠습니다:

src/main.rs
fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

오류는 다음과 같습니다:

$ cargo run
   Compiling ownership v0.1.0 (file:///projects/ownership)
error[E0106]: missing lifetime specifier
 --> src/main.rs:5:16
  |
5 | fn dangle() -> &String {
  |                ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime
  |
5 | fn dangle() -> &'static String {
  |                 +++++++

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

이 오류 메시지는 아직 다루지 않은 기능인 라이프타임(Lifetimes)에 관한 것입니다. 라이프타임에 대해서는 10장에서 자세히 설명하겠습니다. 하지만 라이프타임에 대한 부분을 무시하더라도 이 메시지에는 이 코드가 왜 문제가 되는지에 대한 핵심이 포함되어 있습니다:

this function's return type contains a borrowed value, but there is no value
for it to be borrowed from

dangle 코드의 각 단계에서 정확히 어떤 일이 일어나는지 자세히 살펴봅시다:

src/main.rs
fn dangle() -> &String { // dangle returns a reference to a String

    let s = String::from("hello"); // s is a new String

    &s // we return a reference to the String, s
} // Here, s goes out of scope, and is dropped. Its memory goes away.
  // Danger!

sdangle 내부에 생성되므로 dangle 코드가 완료되면 s는 할당 해제됩니다. 하지만 우리는 그것에 대한 참조를 반환하려고 했습니다. 즉, 이 참조가 잘못된 String을 가리키고 있다는 뜻입니다. 이건 좋지 않기 때문에 Rust는 이 작업을 허용하지 않습니다.

여기서 해결책은 String을 직접 반환하는 것입니다:

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

문제없이 작동합니다. 소유권이 이전되고 아무것도 할당 해제되지 않습니다.

참조 규칙

참조에 대해 논의한 내용을 요약해 보겠습니다:

  • 언제든지 변경 가능한 참조를 하나 또는 불변 참조를 여러 개 가질 수 있습니다.

  • 참조는 항상 유효해야 합니다.

다음에는 다른 종류의 참조인 슬라이스를 살펴보겠습니다.


References