Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

참조와 대여

목록 4-5의 튜플 코드가 가진 문제는, Stringcalculate_length 로 이동되었기 때문에 함수 호출 뒤에도 그 String 을 계속 사용하려면 호출한 함수 쪽으로 다시 String 을 반환해야 한다는 점입니다. 대신 String 값에 대한 참조를 넘길 수 있습니다. 참조는 어떤 주소를 따라가 그곳에 저장된 데이터에 접근할 수 있다는 점에서 포인터와 비슷합니다. 다만 그 데이터는 다른 어떤 변수가 소유하고 있습니다. 포인터와 달리 참조는, 그 참조가 살아 있는 동안 특정 타입의 유효한 값을 가리킨다는 것이 보장됩니다.

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

Filename: 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()
}

먼저 변수 선언과 함수 반환값에 있던 튜플 코드가 모두 사라졌다는 점에 주목하세요. 둘째, calculate_length&s1 을 넘기고 있고, 함수 정의에서도 String 대신 &String 을 받고 있습니다. 이 앰퍼샌드들은 참조를 나타내며, 값의 소유권을 가져가지 않고도 그 값을 가리킬 수 있게 해 줍니다. 그림 4-6은 이 개념을 보여 줍니다.

Three tables: the table for s contains only a pointer to the table
for s1. The table for s1 contains the stack data for s1 and points to the
string data on the heap.

그림 4-6: String s1 을 가리키는 &String s 도식

Note: & 를 사용해 참조하는 것의 반대는 역참조(dereferencing) 이며, 이는 역참조 연산자 * 로 수행합니다. 역참조 연산자의 사용 예는 8장에서 일부 보게 되고, 자세한 내용은 15장에서 다룹니다.

여기서 함수 호출을 좀 더 자세히 봅시다.

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()
}

&s1 문법은 s1 의 값을 가리키는 참조를 만들게 해 주지만, 그 값을 소유하지는 않습니다. 참조는 소유권이 없기 때문에, 참조 자체가 더 이상 사용되지 않더라도 그 참조가 가리키는 값은 정리되지 않습니다.

마찬가지로 함수 시그니처에서도 & 를 써서 매개변수 s 의 타입이 참조라는 점을 나타냅니다. 설명을 위해 몇 가지 주석을 덧붙이면 다음과 같습니다.

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 is a reference to a String
    s.len()
} // Here, s goes out of scope. But because s does not have ownership of what
  // it refers to, the String is not dropped.

변수 s 가 유효한 스코프는 다른 함수 매개변수와 동일하지만, s 가 참조할 뿐 소유하지는 않기 때문에 s 가 더 이상 사용되지 않는다고 해서 그 참조 대상 값이 삭제되지는 않습니다. 함수가 실제 값을 받는 대신 참조를 매개변수로 받으면, 소유권을 가졌던 적이 없으므로 나중에 값을 반환해 소유권을 돌려줄 필요도 없습니다.

참조를 만드는 행위를 대여(borrowing) 라고 부릅니다. 현실에서 어떤 사람이 물건을 소유하고 있을 때 우리가 그것을 빌려 쓰는 것과 같습니다. 다 쓰고 나면 돌려줘야 합니다. 우리는 그것을 소유하는 것이 아닙니다.

그렇다면 빌린 값을 수정하려 하면 어떻게 될까요? 목록 4-6의 코드를 시도해 봅시다. 미리 말하면, 동작하지 않습니다!

Filename: 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: 빌린 값을 수정하려 시도하기

오류는 다음과 같습니다.

$ 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
  |
8 |     some_string.push_str(", world");
  |     ^^^^^^^^^^^ `some_string` is a `&` reference, so the data it refers to cannot be borrowed as mutable
  |
help: consider changing this to be a mutable reference
  |
7 | fn change(some_string: &mut String) {
  |                         +++

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

변수가 기본적으로 불변인 것처럼, 참조도 기본적으로 불변입니다. 따라서 우리가 참조하고 있는 것을 수정하는 것은 허용되지 않습니다.

가변 참조

목록 4-6의 코드는 몇 가지 작은 수정을 통해, 가변 참조(mutable reference) 를 사용하도록 고치면 빌린 값을 수정할 수 있습니다.

Filename: 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 에 대해 두 개의 가변 참조를 만들려는 다음 코드는 실패합니다.

Filename: src/main.rs
fn main() {
    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` (bin "ownership") due to 1 previous error

이 오류는 같은 시점에 s 를 가변으로 두 번 이상 빌릴 수 없기 때문에 이 코드가 유효하지 않다고 말합니다. 첫 번째 가변 대여는 r1 에 있고, println! 에서 사용될 때까지 살아 있어야 합니다. 그런데 그 가변 참조가 만들어진 뒤 실제로 사용되기 전에, 우리는 r2 라는 또 다른 가변 참조를 만들어 r1 과 같은 데이터를 빌리려 했습니다.

같은 데이터에 대한 여러 가변 참조를 동시에 막는 이 제약은, 변경 자체는 허용하되 매우 통제된 방식으로만 허용한다는 뜻입니다. 대부분의 언어는 원하면 언제든 값을 바꿀 수 있게 해 주기 때문에, 이것은 새로운 Rustacean들이 종종 어려워하는 부분입니다. 하지만 이 제약의 장점은 러스트가 컴파일 시점에 데이터 경쟁을 막을 수 있다는 점입니다. 데이터 경쟁(data race) 은 레이스 컨디션과 비슷한 문제로, 다음 세 가지 조건이 동시에 성립할 때 발생합니다.

  • 두 개 이상의 포인터가 같은 데이터에 동시에 접근한다.
  • 그 포인터 중 적어도 하나는 데이터를 쓰는 데 사용된다.
  • 데이터 접근을 동기화하는 메커니즘이 없다.

데이터 경쟁은 정의되지 않은 동작을 일으키며, 런타임에 추적하려고 하면 진단과 수정이 매우 어렵습니다. 러스트는 데이터 경쟁이 있는 코드를 아예 컴파일하지 않음으로써 이 문제를 막습니다.

언제나 그렇듯, 중괄호로 새 스코프를 만들면 여러 가변 참조를 사용할 수 있습니다. 단, 동시에 존재하지만 않으면 됩니다.

fn main() {
    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;
}

러스트는 가변 참조와 불변 참조를 섞어 쓸 때도 비슷한 규칙을 강제합니다. 다음 코드는 오류를 발생시킵니다.

fn main() {
    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}");
}

오류는 다음과 같습니다.

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

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

후우. 같은 값에 대한 불변 참조가 있는 동안에는 가변 참조도 가질 수 없습니다.

불변 참조를 사용하는 사람은 값이 갑자기 바뀌리라고 기대하지 않기 때문입니다! 반면 여러 개의 불변 참조는 동시에 허용됩니다. 데이터를 읽기만 하는 사람은 다른 사람이 읽는 내용에 영향을 줄 수 없기 때문입니다.

참조의 스코프는 참조가 도입된 지점에서 시작해, 그 참조가 마지막으로 사용되는 지점까지 이어진다는 점에도 주의하세요. 예를 들어 다음 코드는, 불변 참조가 마지막으로 사용되는 곳이 println! 이고 그 뒤에 가변 참조가 도입되므로 컴파일됩니다.

fn main() {
    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}");
}

불변 참조 r1, r2 의 스코프는 그것들이 마지막으로 사용되는 println! 이후에 끝납니다. 이는 가변 참조 r3 가 만들어지기 이전 입니다. 따라서 스코프가 겹치지 않기 때문에 이 코드는 허용됩니다. 컴파일러는 스코프 끝보다 더 앞선 시점에서 참조가 이미 더 이상 사용되지 않는다는 점을 알 수 있습니다.

대여 관련 오류가 때로는 답답할 수 있지만, 이것은 러스트 컴파일러가 잠재적인 버그를 런타임이 아니라 컴파일 시점에 미리 알려 주고, 정확히 어디가 문제인지 보여 주고 있다는 뜻이라는 점을 기억하세요. 그러면 데이터가 왜 생각한 것과 다르게 되었는지 나중에 추적할 필요가 줄어듭니다.

댕글링 참조

포인터를 사용하는 언어에서는, 어떤 메모리를 해제한 뒤 그 메모리를 가리키는 포인터를 그대로 남겨 두는 실수를 통해 댕글링 포인터(dangling pointer) 를 만들기 쉽습니다. 댕글링 포인터는 이미 다른 것에 재할당되었을 수도 있는 메모리 위치를 참조합니다. 반면 러스트에서는 컴파일러가 참조가 절대 댕글링 참조가 되지 않도록 보장합니다. 어떤 데이터에 대한 참조가 있다면, 컴파일러는 그 참조가 살아 있는 동안 데이터가 먼저 스코프를 벗어나지 않도록 확인합니다.

러스트가 컴파일 시점 오류로 이를 어떻게 막는지 보기 위해, 댕글링 참조를 일부러 만들어 보겠습니다.

Filename: 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, but this is uncommon unless you're returning a borrowed value from a `const` or a `static`
  |
5 | fn dangle() -> &'static String {
  |                 +++++++
help: instead, you are more likely to want to return an owned value
  |
5 - fn dangle() -> &String {
5 + fn dangle() -> String {
  |

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

이 오류 메시지에는 아직 다루지 않은 기능인 라이프타임이 언급됩니다. 라이프타임은 10장에서 자세히 다룹니다. 하지만 라이프타임 부분을 잠시 무시하고 보면, 이 메시지는 왜 이 코드가 문제인지에 대한 핵심을 담고 있습니다.

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

dangle 코드의 각 단계에서 정확히 무슨 일이 일어나는지 좀 더 자세히 봅시다.

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

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 을 가리키게 됩니다. 좋지 않지요! 러스트는 이런 코드를 허용하지 않습니다.

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

fn main() {
    let string = no_dangle();
}

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

    s
}

이 코드는 아무 문제 없이 동작합니다. 소유권이 함수 밖으로 이동하므로, 아무 것도 미리 해제되지 않습니다.

참조 규칙

지금까지 참조에 대해 다룬 내용을 정리해 봅시다.

  • 어떤 시점이든, 가변 참조는 하나만 있거나, 아니면 불변 참조를 몇 개든 가질 수 있습니다.
  • 참조는 언제나 유효해야 합니다.

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