참조와 빌림

리스트 4-5의 튜플 코드에서 문제는 calculate_length 함수를 호출한 후에도 String을 계속 사용할 수 있도록 String을 호출 함수로 반환해야 한다는 것이다. 왜냐하면 Stringcalculate_length로 이동했기 때문이다. 대신, 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() }

먼저, 변수 선언과 함수 반환 값에 있는 모든 튜플 코드가 사라졌다는 점을 확인한다. 두 번째로, &s1calculate_length에 전달하고, 함수 정의에서 String 대신 &String을 사용한다. 이 앰퍼샌드(&)는 참조를 나타내며, 값을 소유하지 않고도 값을 참조할 수 있게 한다. 그림 4-6은 이 개념을 보여준다.

세 개의 테이블: s 테이블은 s1 테이블에 대한 포인터만 포함한다. s1 테이블은 s1의 스택 데이터를 포함하고 힙에 있는 문자열 데이터를 가리킨다.

그림 4-6: &String sString s1을 가리키는 다이어그램

참고: &를 사용한 참조의 반대는 역참조(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 value 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에서 같은 데이터를 빌리는 또 다른 가변 참조를 생성하려고 시도했다.

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

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

데이터 경쟁은 정의되지 않은 동작을 유발하며, 런타임에 이를 추적하고 수정하는 것은 어려울 수 있다. Rust는 데이터 경쟁이 있는 코드를 컴파일하지 않음으로써 이 문제를 방지한다!

언제나 중괄호를 사용해 새로운 스코프를 생성할 수 있으며, 이를 통해 여러 가변 참조를 허용할 수 있다. 단, 동시에 사용할 수는 없다:

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; }

Rust는 가변 참조와 불변 참조를 결합하는 경우에도 비슷한 규칙을 적용한다. 다음 코드는 에러를 발생시킨다:

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!("{}, {}, 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` (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}"); }

불변 참조 r1r2의 스코프는 마지막으로 사용된 println! 이후에 끝나며, 이는 가변 참조 r3가 생성되기 전이다. 이 스코프들은 겹치지 않으므로 이 코드는 허용된다. 컴파일러는 스코프가 끝나기 전에 참조가 더 이상 사용되지 않는다는 것을 알 수 있다.

빌림 에러가 때로는 답답할 수 있지만, Rust 컴파일러가 잠재적인 버그를 조기에(런타임이 아닌 컴파일 타임에) 지적하고 문제가 있는 정확한 위치를 보여준다는 점을 기억하자. 그러면 데이터가 예상과 다를 때 그 이유를 추적할 필요가 없다.

댕글링 참조

포인터를 사용하는 언어에서는, 메모리를 해제한 후에도 그 메모리에 대한 포인터를 유지하는 실수를 통해 _댕글링 포인터_를 쉽게 만들 수 있다. 댕글링 포인터는 이미 다른 곳에 할당된 메모리 위치를 참조하는 포인터를 의미한다. 반면에 Rust에서는 컴파일러가 참조가 절대 댕글링 참조가 되지 않도록 보장한다. 즉, 어떤 데이터에 대한 참조가 있다면, 컴파일러는 그 데이터가 참조보다 먼저 스코프를 벗어나지 않도록 한다.

Rust가 어떻게 컴파일 타임 오류를 통해 댕글링 참조를 방지하는지 확인해보자:

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 { | error[E0515]: cannot return reference to local variable `s` --> src/main.rs:8:5 | 8 | &s | ^^ returns a reference to data owned by the current function Some errors have detailed explanations: E0106, E0515. For more information about an error, try `rustc --explain E0106`. error: could not compile `ownership` (bin "ownership") due to 2 previous errors

이 오류 메시지는 아직 다루지 않은 기능인 ’라이프타임’을 언급한다. 라이프타임에 대해서는 10장에서 자세히 설명할 것이다. 하지만 라이프타임에 대한 부분을 무시하더라도, 이 메시지는 왜 이 코드가 문제가 되는지에 대한 핵심을 담고 있다:

이 함수의 반환 타입은 빌린 값을 포함하지만, 빌릴 값이 존재하지 않습니다.

이제 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는 해제된다. 하지만 우리는 s에 대한 참조를 반환하려고 했다. 이는 이 참조가 유효하지 않은 String을 가리키게 된다는 것을 의미한다. 이는 좋지 않은 상황이다! Rust는 이런 상황을 허용하지 않는다.

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

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

이 코드는 아무런 문제 없이 동작한다. 소유권이 이동되고, 아무것도 해제되지 않는다.

참조의 규칙

지금까지 다룬 참조에 대한 내용을 다시 정리해 보자:

  • 특정 시점에 하나의 가변 참조를 가질 수 있거나, 여러 개의 불변 참조를 가질 수 있다.
  • 모든 참조는 항상 유효해야 한다.

다음으로, 다른 종류의 참조인 슬라이스에 대해 알아보자.