앞서 Rust의 소유권 개념에 대해 정리하기도 했고, Rust로 과제를 할 때 쓰면서도 확실하게 이해하고 쓰지 못하고 있다는 느낌이 들었던 참조(reference)와 대여(borrowing)에 대한 내용을 정리하면서 확실하게 파악해보려 합니다.
함수에 파라미터를 전달하고, 값을 반환할 때는 대입과 똑같이 소유권의 이동 혹은 복사가 일어난다고 생각하면 이를 이해하는 것 자체에는 큰 문제가 없을 것입니다. 다만, 모든 함수가 매번 소유권을 가졌다가 반납하는 것은 조금 번거롭습니다.
함수에 넘겨줄 값을 함수 호출 이후에도 쓰고 싶은데, 그렇다고 해서 함수로부터 얻고자 하는 결과에 더해서 이후 다시 쓰고 싶은 변수까지 같이 반환받아야 한다면 본말전도나 다름없죠.
교재에서 설명하는 위와 같은 경우를 생각해보면 매번 이를 신경쓰는 것은 매우 귀찮은 작업이 될 것 같습니다.
그럼, 함수가 값을 사용할 수 있도록 하되 소유권은 가져가지 않도록 하고 싶다면 어떻게 해야 할까요?
Rust에서는 예제 4-5에서처럼 튜플을 사용하여 여러 값을 반환하는 것이 가능합니다:
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()은 String의 길이를 반환합니다
(s, length)
}
예제 4.5: 매개변수의 소유권을 되돌려주는 방법
다만, 이런 방식도 거추장스럽고, 많은 작업량이 수반되는 것은 마찬가지입니다.
다행히도, Rust에는 소유권 이동 없이 값을 사용할 수 있는 참조자 (reference) 라는 기능을 가지고 있습니다.
다음은 참조자를 매개변수로 받도록 구현한 calculate_length 함수의 정의 및 용례입니다.
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 대신 &s1을 전달하고, 함수 정의에 String 대신 &String을 사용했네요. 이 앰퍼센드(&) 기호가 참조자를 나타내고, 어떤 값의 소유권을 가져오지 않고 해당 값을 참조할 수 있도록 해 줍니다.
let s1 = String::from("hello");
let len = calculate_length(&s1);
함수 호출부를 보면, s1에 &를 붙인 &s1 구문은 s1 값을 참조하지만 해당 값을 소유하지 않는 참조자를 생성합니다. 값을 소유하지 않으므로 이 참조자가 가리킨 값은 참조자가 사용되지 않을 때까지 버려지지 않습니다.
마찬가지로 함수 시그니처에도 &를 사용하여 매개변수 s가 참조자 타입임을 나타내줍니다.
fn calculate_length(s: &String) -> usize { // s는 String의 참조자입니다
s.len()
} // 여기서 s가 스코프 밖으로 벗어납니다. 하지만 참조하는 것을 소유하고 있진 않으므로,
// 버려지지는 않습니다.
변수 s가 유효한 스코프는 여타 함수의 매개변수에 적용되는 스코프와 동일합니다. 하지만 s에는 소유권이 없으므로 s가 더 이상 사용되지 않을 때도 이 참조자가 가리킨 값이 버려지지 않습니다.
함수가 실제 값 대신 참조자를 매개변수로 쓴다면 애초에 소유권이 없으니까 이 소유권을 돌려주기 위한 값 반환도 필요 없어집니다. 거추장스러운 작업이 하나 사라지는 셈이 됩니다.
이처럼 참조자를 만드는 행위를 Rust에서는 대여(borrow) 라고 합니다.
fn main() {
let s = String::from("hello");
change(&s);
}
fn change(some_string: &String) {
some_string.push_str(", world");
}
이 코드는 컴파일 에러를 발생시킵니다. 변수가 기본적으로 불변성을 지니는 것과 마찬가지로, 참조자 또한 기본적으로 참조하는 값을 수정할 수 없습니다.
$ 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
가변 참조자 (mutable reference) 를 사용하는 식으로 코드를 살짝만 수정해 주면 에러를 없앨 수 있습니다.
fn main() {
let mut s = String::from("hello");
change(&mut s);
}
fn change(some_string: &mut String) {
some_string.push_str(", world");
}
에러 메시지 내용에서도 자세히 알려주듯이, mut 키워드를 붙여주면 해결됩니다.
(Rust의 가장 강력한 장점은 컴파일러가 에러 메시지를 매우 친절하게 써 주는 것이라 생각합니다. 그대로 따라하면 손쉽게 문제를 해결할 수 있을 정도로 굉장히 직관적이고, 에러 메시지를 보면서 스스로 잘못한 점을 피드백하고 공부까지 할 수 있을 정도입니다.. Rust의 에러 메시지는 볼 때마다 마치 선생님 같다는 생각이 듭니다. 급 주저리 주저리 ..)
가변 참조자는 한 가지 큰 제약사항이 있습니다: 어떤 값에 대한 가변 참조자가 있다면, 그 값에 대한 참조자는 더 이상 만들 수 없습니다. 아래의 코드는 s에 대한 두 개의 가변 참조자 생성을 시도하는 코드로, 작동하지 않습니다:
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
대부분의 언어들이 언제든 값 변경을 허용하고 있기 때문에, Rust를 처음 사용할 경우에는 이러한 제약이 장애물처럼 다가올 수 있습니다. 하지만, 이 제약 덕분에 러스트에서는 컴파일 타임에 데이터 경합 (data race) 을 방지할 수 있습니다.
데이터 경합이란 다음 세 가지 상황이 겹칠 때 일어나는 특정한 경합 조건 (race condition) 입니다:
가변 참조자와 불변 참조자를 혼용할 때도 유사한 규칙이 적용됩니다. 다음 코드는 컴파일 에러가 발생합니다:
let mut s = String::from("hello");
let r1 = &s; // 문제없음
let r2 = &s; // 문제없음
let r3 = &mut s; // 큰 문제
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; // 문제없음
let r2 = &s; // 문제없음
println!("{} and {}", r1, r2);
// 이 지점 이후로 변수 r1과 r2는 사용되지 않습니다
let r3 = &mut s; // 문제없음
println!("{}", r3);
불변 참조자 r1, r2의 스코프는 자신들이 마지막으로 사용된 println! 이후로 종료되고, 해당 println!은 가변 참조자 r3가 생성되기 전이니 서로 스코프가 겹치지 않아서 이 코드는 문제가 없는 것이죠: 컴파일러는 이 참조자가 어떤 지점 이후로 스코프 끝까지 사용되지 않음을 알 수 있습니다.

강의자료에서 손글씨로 필기한 내용 중, scope를 표시한 부분을 한번 유심히 확인해보면 좋을 것 같습니다! (예시코드는 똑같은 코드입니다~)
스코프가 겹치지 않도록 하는 또 하나의 방법으로, 중괄호로 새로운 스코프를 만들어 가변 참조자를 여러 개 만들면서 동시에 존재하는 상황을 회피하는 방법이 있습니다:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // 여기서 r1이 스코프 밖으로 벗어나며, 따라서 아무 문제없이 새 참조자를 만들 수 있습니다.
let r2 = &mut s;
참조와 대여에 대한 내용을 정리하면서, 이런 질문이 떠올랐습니다.
Q. 참조자로 참조를 했는데, 원본 값이 갑자기 사라지면 어떡해 …?
→ A.
이 질문에 대한 답은 ‘댕글링 참조’라는 내용으로 친절하게 설명이 되어 있는데, 내용이 너무 길어지니 다음 글로 나누도록 하겠습니다 .!!
읽어주셔서 감사합니다 🙇♂️
https://doc.rust-kr.org/ch04-02-references-and-borrowing.html