참조와 빌림

Beginner

This tutorial is from open-source community. Access the source code

소개

References and Borrowing에 오신 것을 환영합니다. 이 랩은 Rust Book의 일부입니다. LabEx 에서 Rust 기술을 연습할 수 있습니다.

이 랩에서는 Rust 에서 소유권을 가져가는 대신 값을 빌리기 위해 참조 (reference) 를 사용하는 방법을 배웁니다. 이를 통해 소유권을 호출 함수로 다시 반환할 필요 없이 데이터를 전달하고 조작할 수 있습니다.

References and Borrowing

Listing 4-5 의 튜플 코드의 문제는 calculate_length 호출 후에도 String을 계속 사용할 수 있도록 String을 호출 함수로 반환해야 한다는 것입니다. 왜냐하면 Stringcalculate_length로 이동했기 때문입니다. 대신, String 값에 대한 참조를 제공할 수 있습니다. *참조 (reference)*는 포인터와 유사하며, 해당 주소에 저장된 데이터에 접근하기 위해 따라갈 수 있는 주소입니다. 해당 데이터는 다른 변수가 소유합니다. 포인터와 달리, 참조는 해당 참조의 수명 동안 특정 유형의 유효한 값을 가리키도록 보장됩니다.

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

파일 이름: src/main.rs

fn main() {
    let s1 = String::from("hello");

    let len = calculate_length(&s1);

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

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

먼저, 변수 선언과 함수 반환 값의 모든 튜플 코드가 사라진 것을 확인하십시오. 둘째, &s1calculate_length에 전달하고, 정의에서 String 대신 &String을 사용한다는 점에 유의하십시오. 이 앰퍼샌드는 *참조 (references)*를 나타내며, 소유권을 가져가지 않고도 일부 값을 참조할 수 있도록 합니다. 그림 4-5 는 이 개념을 보여줍니다.

그림 4-5: String s1을 가리키는 &String s의 다이어그램

참고: &를 사용하여 참조하는 것의 반대는 *역참조 (dereferencing)*이며, 역참조 연산자 *로 수행됩니다. 8 장에서 역참조 연산자의 사용법을 살펴보고, 15 장에서 역참조에 대한 자세한 내용을 논의할 것입니다.

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

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

let len = calculate_length(&s1);

&s1 구문을 사용하면 s1의 값을 *참조 (refer)*하지만 소유하지 않는 참조를 만들 수 있습니다. 소유하지 않기 때문에, 참조가 사용을 멈출 때 가리키는 값은 삭제되지 않습니다.

마찬가지로, 함수의 시그니처는 &를 사용하여 매개변수 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, the String is not dropped

변수 s가 유효한 범위는 모든 함수 매개변수의 범위와 동일하지만, 참조가 소유권을 갖지 않기 때문에 s가 사용을 멈출 때 참조가 가리키는 값은 삭제되지 않습니다. 함수가 실제 값 대신 참조를 매개변수로 갖는 경우, 소유권을 돌려주기 위해 값을 반환할 필요가 없습니다. 왜냐하면 우리는 소유권을 갖지 않았기 때문입니다.

참조를 만드는 행위를 *빌림 (borrowing)*이라고 부릅니다. 실제 생활에서와 마찬가지로, 어떤 것을 소유한 사람이 있다면, 그 사람에게서 빌릴 수 있습니다. 다 사용하면 돌려줘야 합니다. 당신은 그것을 소유하지 않습니다.

그렇다면 빌리고 있는 것을 수정하려고 하면 어떻게 될까요? Listing 4-6 의 코드를 시도해 보십시오. 스포일러 경고: 작동하지 않습니다!

파일 이름: src/main.rs

fn main() {
    let s = String::from("hello");

    change(&s);
}

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

Listing 4-6: 빌린 값을 수정하려는 시도

다음은 오류입니다.

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

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

가변 참조 (Mutable References)

Listing 4-6 의 코드를 수정하여 빌린 값을 수정할 수 있도록 하려면 *가변 참조 (mutable reference)*를 사용하는 몇 가지 작은 조정만 하면 됩니다.

파일 이름: 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}");

다음은 오류입니다.

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

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

동일한 데이터에 대한 여러 가변 참조를 동시에 방지하는 제한 사항은 매우 제어된 방식으로 변경을 허용합니다. 이것은 새로운 Rust 사용자들이 어려움을 겪는 부분인데, 대부분의 언어에서는 원하는 때마다 변경할 수 있기 때문입니다. 이 제한 사항을 갖는 것의 장점은 Rust 가 컴파일 시간에 데이터 경합 (data race) 을 방지할 수 있다는 것입니다. *데이터 경합 (data race)*은 경쟁 조건 (race condition) 과 유사하며, 다음 세 가지 동작이 발생할 때 발생합니다.

  • 두 개 이상의 포인터가 동시에 동일한 데이터에 접근합니다.
  • 포인터 중 적어도 하나는 데이터를 쓰기 위해 사용됩니다.
  • 데이터에 대한 접근을 동기화하는 데 사용되는 메커니즘이 없습니다.

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

항상 그렇듯이, 중괄호를 사용하여 새 범위를 만들 수 있으며, 여러 가변 참조를 허용하지만 *동시적 (simultaneous)*인 것은 허용하지 않습니다.

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!("{r1}, {r2}, and {r3}");

다음은 오류입니다.

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!("{r1}, {r2}, and {r3}");
  |                -- immutable borrow later used here

휴! 동일한 값에 대한 불변 참조가 있는 동안 가변 참조를 가질 수도 없습니다.

불변 참조를 사용하는 사람은 값이 갑자기 자신 아래에서 변경될 것이라고 예상하지 않습니다! 그러나 여러 불변 참조는 허용됩니다. 왜냐하면 데이터를 읽기만 하는 사람은 다른 사람의 데이터 읽기에 영향을 미칠 수 있는 능력이 없기 때문입니다.

참조의 범위는 도입된 지점부터 해당 참조가 마지막으로 사용될 때까지 시작된다는 점에 유의하십시오. 예를 들어, 이 코드는 불변 참조의 마지막 사용, 즉 println!이 가변 참조가 도입되기 전에 발생하기 때문에 컴파일됩니다.

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

let r1 = &s; // no problem
let r2 = &s; // no problem
println!("{r1} and {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 References

포인터를 사용하는 언어에서는, 메모리를 해제하면서 해당 메모리에 대한 포인터를 보존함으로써 댕글링 포인터 (dangling pointer)---다른 사람에게 할당되었을 수 있는 메모리 위치를 참조하는 포인터---를 실수로 생성하기 쉽습니다. 반대로, Rust 에서는 컴파일러가 참조가 댕글링 참조가 되지 않도록 보장합니다. 즉, 어떤 데이터에 대한 참조가 있는 경우, 컴파일러는 데이터에 대한 참조가 데이터보다 먼저 범위를 벗어나지 않도록 보장합니다.

컴파일 시간 오류를 통해 Rust 가 댕글링 참조를 어떻게 방지하는지 확인하기 위해 댕글링 참조를 생성해 보겠습니다.

파일 이름: src/main.rs

fn main() {
    let reference_to_nothing = dangle();
}

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

    &s
}

다음은 오류입니다.

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 {
  |                ~~~~~~~~

이 오류 메시지는 아직 다루지 않은 기능인 수명 (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, so its memory goes away
  // Danger!

sdangle 내에서 생성되기 때문에, dangle의 코드가 완료되면 s는 할당 해제됩니다. 그러나 우리는 그것에 대한 참조를 반환하려고 했습니다. 즉, 이 참조는 유효하지 않은 String을 가리키게 됩니다. 좋지 않습니다! Rust 는 우리가 이것을 하도록 허용하지 않습니다.

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

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

    s
}

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

참조 규칙

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

  • 주어진 시점에서, 하나의 가변 참조 또는 임의의 수의 불변 참조를 가질 수 있습니다.
  • 참조는 항상 유효해야 합니다.

다음으로, 다른 종류의 참조인 슬라이스 (slice) 를 살펴보겠습니다.

요약

축하합니다! 참조 및 빌림 (Borrowing) 랩을 완료했습니다. LabEx 에서 더 많은 랩을 연습하여 실력을 향상시킬 수 있습니다.