Rust 소유권 설명해드림 2편
소유권 이전 없이 주소만 가져오는 레퍼런스 (References)
Rust 소유권 설명해드림 1편 마지막에 서술했듯이, Rust에서 힙 메모리에 저장한 값을 다른 변수, 심지어 파라미터에 보낼 때도 항상 이동(move)하여 기존 변수는 더이상 사용하지 않습니다.
이렇게 되면, 단순한 개발을 위해서도 부가적인 코드 작성이 필요하게 되는데요. 이를 방지하기 위해 Rust는 References 기능을 제공합니다.
References(이하 레퍼런스) 는 C, C++의 포인터와 유사한 개념으로, 다른 변수가 소유한 값에 대한 메모리 주소만을 나타낼 때 사용합니다. 포인터와의 차이점은, 레퍼런스의 경우 특정 타입에 해당하는 값을 확실히 가리키고 있다고 규칙을 통해 보장한다는 점입니다. 이를 통해, 에러를 방지할 수 있죠.
레퍼런스는 익숙힌 &
접두사를 사용해서 명시하며, 아래와 같이 아규먼트로 보내는 변수(&s1
), 파라미터 타입(&String
) 둘 대에게 적어줍니다.
여기서 s
는 s1
의 복사본이 아니라 s1
을 가리키는 또 하나의 포인터라고 이해해야 합니다.
레퍼런스는 소유권을 가져오지 않기 때문에 메모리 반환에 영향을 주지 않는데요. 이렇게 레퍼런스를 생성하는 것을 Borrowing(이하 빌림) 이라고 합니다.
레퍼런스로 값을 빌려온 경우에도 기본적으로 해당 값을 수정할 수 없습니다.
수정할 수 있는 레퍼런스 (Mutable References)
만약 빌려온 값을 수정하고자 하면 (1) 소유권을 가진 변수가 당연히 수정가능해야 하며,
(2) 레퍼런스 변수와 타입에도 아래와 같이 &mut
를 명시해야 합니다.
또한, 수정가능한 레퍼런스는 값 하나에 스코프당 오직 하나씩만 정의해야 합니다.
아래와 같은 코드는 에러가 발생합니다.
let mut s = String from;
let r1 = &mut s;
let r2 = &mut s;
println!;
만약 한 스코프에 수정가능한 레퍼런스와 수정불가능한 레퍼런스를 함께 정의했다면, 규칙이 살짝 복잡합니다.
- 우선, 수정가능한 레퍼런스를 정의하기 이전에 정의한 모든 수정가능한 레퍼런스는 한 번 이상 사용되어야 합니다.
- 수정가능한 레퍼런스를 정의한 이후에 정의한 수정불가능한 레퍼런스들도 사용가능합니다.
- 수정가능한 레퍼런스를 정의하기 이전에 정의한 레퍼런스 중 사용하지 않은 것이 있다면 에러가 발생합니다.
let mut s = String from;
let r1 = &s;
let r2 = &s;
let r3 = &mut s; // 에러 발생!
println!;
위 코드에서는 r3
를 정의하기 전에 r1
, r2
를 사용하지 않았기 때문에 에러가 발생합니다.
위 코드에서는 r1
을 사용했기 때문에 에러가 발생하지 않습니다.
이런 식으로 작동하는 이유는, 이미 정의한 수정불가능한 레퍼런스가 가리키는 값과 그 뒤에 정의한 수정가능한 레퍼런스가 가리키는 값이 다를 수 있다는 가능성 때문입니다.
같은 원리로, 아래와 같은 코드도 사용할 수 없습니다.
r2.push_str("!!");
라인을 통해 변수 s
가 소유한 문자열 값은 변했고, 이는 r3
레퍼런스가 가리키는
값과 달라졌습니다. Rust 컴파일러는 이와 같은 모든 경우를 컴파일 전에 찾아내서 알려줍니다.
포인터 vs 레퍼런스
서두에 잠깐 언급했지만, Rust 레퍼런스는 다른 언어의 포인터와 달리 모든 레퍼런스가 정해진 타입의 값을 가리키고 있다는 것을 보장합니다. 이게 지켜지지 않으면 컴파일을 하지 않으니까요.
아래와 같이, 가리키는 값이 없는 레퍼런스를 만들어내려고 시도하면, 에러가 발생합니다.
더구나, 어느 코드에서 에러가 발생하는지 친절하게 알려주니 언제나 Rust 컴파일러와 함께 코드를 작성한다고 생각하며 에러를 맞이합시다!
여기까지가 레퍼런스와 빌림을 설명한 2편이었습니다. 마지막 3편에서는 또다른 레퍼런스 종류인 Slice에 대해 설명합니다.