Chapter 4.2. 참조와 대여
이전 글에서 tuple을 사용했던 예제 4-5는 이런 문제가 있었다: String
의 ownership이 calculate_length
함수 안으로 이동했기 때문에, 함수 호출이 끝난 후에 그 String
을 다시 사용하기 위해서 ownership을 다시 내보내야 했던 것이다.
아래는 함수가 parameter로서 그 값의 ownership을 가져가는 대신, 참조값(reference)만 가져가는 방법을 보여준다.
// Filename: 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()
}
첫째로 알아둘 것은, 변수 선언과 함수 반환값을 위한 tuple 코드가 모두 사라졌다는 것이고, 둘째로는 calculate_length
에 &s1
을 건내줬고, 이 함수의 정의에서도 String
대신 &String
을 취한다는 것이다.
이 &
표시는 reference를 의미하는데, ownership을 가져가는 대신 그 주소만을 가리킬 수 있게 하는 것이다. 그림 4-5는 이를 설명한다.
그림 4-5: String s1을 가리키는 &String의 도표
참고:
&
를 이용해서 reference를 사용하는 것의 반대를 dereference라고 하고, dereference 연산자*
를 통해 사용할 수 있다. 챕터 8에서 이 dereference 연산자의 사용 예를 볼 것이고, 챕터 15에서 자세하게 이야기해 볼 것이다.
다시 함수 호출 부분을 보겠다.
let s1 = String::from("hello");
let len = calculate_length(&s1);
&s1
syntax을 통해서 우리는 s1
의 값을 소유허지 않고 그 주소를 가리키고만 있는 reference를 생성할 수 있다. 이 reference는 값의 소유권을 가져가지 않기 때문에, reference가 scope 밖으로 나가더라도 그 값은 제거되지 않는다.
마찬가지로, 함수의 signature도 &
를 사용해서 parameter s
의 타입이 reference라고 명시한다. 코드에 설명을 조금 덧붙여 보자.
fn calculate_length(s: &String) -> usize { // s는 String의 reference이다
s.len()
} // 여기서 s는 scope 밖으로 나온다. 하지만 s가 가리키는 String의 ownership을
// 가지고 있지 않으므로, 아무 일도 일어나지 않는다.
변수 s
가 살아있는 scope는 함수 parameter의 scope와 같다. 하지만 s
가 scope을 바깥을 나갈 때, s
는 ownership을 갖고 있지 않기 때문에 s
가 가리키는 것을 없애지 않는다.함수가 실제 값 대신 reference를 parameter로 가지면, ownership을 애초에 가져온 적이 없기 때문에 ownership을 되돌려 줄 필요가 없다.
그래서 함수 parameter로 reference를 사용하는 것을 대여(borrowing)한다고 부른다. 우리네 인생에서 누가 뭔가를 소유하고 있다면 나는 그것을 빌릴 수 있는 것과 같다. 빌려서 할 일이 끝나면, 당연하게도 다시 되돌려드려야 한다.
그래서 우리가 빌려간 것을 변경하려고 한다면 어떻게 될까? 아래 예제 4-6을 보자. 스포일러 경고: 에러가 난다!
예제 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");
}
아래는 에러 내용이다:
error[E0596]: cannot borrow immutable borrowed content `*some_string` as mutable
--> error.rs:8:5
|
7 | fn change(some_string: &String) {
| ------- use `&mut String` here to make mutable
8 | some_string.push_str(", world");
| ^^^^^^^^^^^ cannot borrow as mutable
Rust의 변수가 기본적으로 immutable했던 것처럼 reference도 마찬가지다. 우리는 reference가 가리키는 것을 변경하도록 허락받지 않은 것이다.
가변적인 참조
예제 4-6의 에러를 약간의 코드 수정을 통해 고칠 수 있다:
// 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");
}
우선 s
가 mut
하도록 바뀌었다. 그리고 &mut s
를 통해 mutable reference를 만들었고, 함수는 some_string: &mut String
을 통해 mutable reference를 받아들일 수 있다.
하지만 mutable reference는 하나의 큰 제약점이 있다: 한 scope에서 하나의 데이터 당 오직 하나의 mutable reference만을 가질 수 있다. 따라서 아래 에러를 유발하는 코드이다:
// Filename: src/main.rs
let mut s = String::from("hello");
let r1 = &mut s;
let r2 = &mut s;
에러 내용은 다음과 같다:
error[E0499]: cannot borrow `s` as mutable more than once at a time
--> borrow_twice.rs:5:19
|
4 | let r1 = &mut s;
| - first mutable borrow occurs here
5 | let r2 = &mut s;
| ^ second mutable borrow occurs here
6 | }
| - first borrow ends here
위 제약점은 데이터 변경(mutation)을 굉장히 통제된 방식으로 가능하게 한다. 대부분의 다른 언어들은 바꾸고 싶은 것은 무엇이든지 바꿀 수 있게 해주기에, 이러한 방식은 새로운 Rustacean들을 힘들게 하곤 한다.
이 제약점의 이점을 말해보자면, Rust가 컴파일 시점에서 data race를 막아준다는 점이다. Data race는 race condition과 비슷한 의미를 가진 용어로, 아래 세 가지 행동이 일어날 때를 말한다.
- 2개 이상의 포인터가 동시에 같은 데이터에 접근한다.
- 그 중 최소 하나의 포인터가 그 데이터에 쓰기 작업을 하기 위해 사용된다.
- 데이터의 접근을 동시에 하게 할 수 있는 방법이 존재하지 않는다.
Data race는 예기치 못한 행동을 초래하고, 런타임에서 문제를 진단하고 고치고자 하는 것이 어려울 수 있다. Rust는 이런 문제가 애초에 일어나는 것 자체를 막는다, 왜냐하면 data race가 일어나는 코드는 컴파일 조차 되지 않기 때문이다!
항상 그랬듯이, 새로운 scope를 생성하기 위해서는 {}
를 사용한다. 그리고 새로운 scope는 여러 개의 mutable reference를 가능하게 해준다:
let mut s = String::from("hello");
{
let r1 = &mut s;
} // r1이 여기서 scope 밖으로 나가므로, 이제 새로운 reference를 만들어도 된다.
let r2 = &mut s;
Mutable reference와 immutable reference를 같이 쓰는 것에 있어서도 비슷한 규칙이 있다. 아래는 에러가 나는 코드이다:
let mut s = String::from("hello");
let r1 = &s; // no problem
let r2 = &s; // no problem
let r3 = &mut s; // BIG PROBLEM
에러가 이렇게 나온다:
error[E0502]: cannot borrow `s` as mutable because it is also borrowed as
immutable
--> borrow_thrice.rs:6:19
|
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 | }
| - immutable borrow ends here
Whew! Immutable reference를 가지고 있다면, 이와 동시에 mutable reference를 가지고 있을 수 없다. Immutable reference를 사용한다는 것은 그 데이터가 갑자기 어딘가에서 바뀔 것이라 예상하고 있지 않는다는 것이다! 또한 mutable reference의 경우와 반대로 여러 개의 immutable reference를 갖고 있어도 되는 것을 볼 수 있는데, 이는 reference들이 데이터를 읽는 서로의 동작에 영향을 전혀 끼칠 수 없기 때문이다.
지금까지 본 에러들이 때때로 우리에게 좌절을 줄 수도 있다. 하지만 그럴 때마다 Rust 컴파일러가 런타임 시점이 아닌 컴파일 시점에서 잠재적인 버그를 일찍 지적해주는 것이고, 그 버그가 어떤 문제 때문인지 똑똑하게 알려주는 것임을 기억하길 바란다. Rust를 사용하면 당신이 예상했던 데이터가 왜 안 나오는지 일일이 추적할 필요가 없다.
주인없는 참조
Pointer를 사용하는 언어를 쓸 때면, 주인을 잃어버린 포인터(dangling pointer)를 실수로 만들기가 쉽다. Dangling pointer는 이미 다른 곳에 할당되었을 수 있는 메모리 주소를 가리키는 reference를 의미하는데, 그 메모리의 pointer가 살아있는 와중에 일정 메모리를 제거해버리면 이런 경우가 발생한다. Rust 컴파일러는 반면에 reference는 절대 dangling reference가 되도록 두지 않는다: 어떤 데이터의 reference가 존재한다면, 컴파일러는 reference가 사라지기 전까지는 해당 데이터가 scope 밖으로 나가지 않는 지를 보장해 줄 것이다.
Dangling reference를 한 번 만들어보자. 그러면 Rust가 컴파일 에러로 이를 막을 것이다:
// Filename: src/main.rs
fn main() {
let reference_to_nothing = dangle();
}
fn dangle() -> &String {
let s = String::from("hello");
&s
}
에러가 이렇게 나온다:
error[E0106]: missing lifetime specifier
--> dangle.rs:5:16
|
5 | fn dangle() -> &String {
| ^ expected 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 giving it a 'static lifetime
위 에러 메세지는 아직 다루지 않은 개념인 lifetime을 언급한다. Lifetime은 챕터 10에서 이야기할 것이다. 하지만 lifetime과 관련된 내용을 제외하고도 에러 메세지에서는 코드의 문제에 대한 핵심을 말해주고 있다:
this function's return type contains a borrowed value, but there is no value
for it to be borrowed from.
위 dangling code의 각 단계에서 무슨 일이 일어나는 지 조금 더 상세하게 보겠다:
fn dangle() -> &String { // dangle은 String reference를 내보낸다
let s = String::from("hello"); // s는 새로운 String이다
&s // String reference인 s를 내보낸다
} // 여기서 s는 scope 밖으로 나가므로, drop되고, 그 메모리는 청소된다.
// Danger!
s
는 dangle
함수 안에서 만들어졌기 때문에, dangle
함수가 끝나면 s
의 메모리 할당도 해제된다. 하지만 우리는 여전히 그 reference를 반환하려고 했다. 그 reference는 이미 죽은 String
을 가리키고 있음을 의미한다. 이는 절대 좋은 일이 아니다! Rust는 이런 일이 일어나도록 우리를 내버려두지 않는다.
문제의 해답은 String
을 직접 반환하는 것이다:
fn no_dangle() -> String {
let s = String::from("hello");
s
}
이렇게 하면 어떤 문제도 없이 잘 작동한다. Ownership이 옮겨질 것이고, 어떤 메모리도 해제되지 않는다.
참조의 규칙
이번 섹션에서 reference에 대해 이야기했던 것을 정리해보겠다:
- 어떤 시점에서든지 하나의 mutable reference를 가지거나, 여러 개의 immutable reference를 가질 수 있지만, 이 두 가지를 동시에 가질 수는 없다.
- Reference가 가리키는 데이터는 항상 살아있어야 한다. (References must always be valid)
다음 섹션에서는 또 다른 종류의 reference인 slice의 개념에 대해 살펴보겠다.
'Programming > Rust' 카테고리의 다른 글
쪼잔한 Rust 16. 두렵지 않은 Concurrency (1) | 2018.09.11 |
---|---|
쪼잔한 Rust 6. Enum과 패턴 맞추기 (1) | 2018.09.03 |
쪼잔한 Rust 4.3. Slice 타입 (0) | 2018.08.19 |
쪼잔한 Rust 4.1. 소유권이 무엇이냐 (2) | 2018.08.09 |
쪼잔한 Rust 3. 일반적인 프로그래밍 개념들 (0) | 2018.07.20 |