Rust 프로그래밍

Rust - 참조 변수와 대여

plas 2020. 7. 6. 20:22

러스트 변수의 지정에서 변수와 객체의 오너십의 문제와 메모리 관리에 대해 Rust, 메모리를 자동으로 제때 해지한다 에서 살펴보았습니다. 여기서는 또하나의 변수에 관한 중요한 기능인 참조에 대해 살펴보려고 합니다. 러스트는 프로그래밍 언어의 개념들을 절묘하게 엮어 기존 언어와는 다른 새로운 규칙의 세계를 만들어 냅니다. 그래서 새로 익혀야 할 개념과 용어, 규칙이 많습니다.

변수는 자기가 가진 객체를 다른 변수에게 참조할 수 있게 빌려줄 수 있습니다. 대여(Borrow)는 말그대로 값에 대한 권한을 일부 빌려주는 것입니다. 그러면 참조 변수는 그 변수를 읽거나 쓸 수 있습니다. 러스트에서 변수와 객체의 관계는 다음과 같이 나누어집니다.

  • 객체에 대한 불변 오너십을 가지는 변수: let name = "lee";
  • 객체에 대해 가변 오너십을 가지는 변수: let mut mutname = String::new;
  • 객체에 대해 불변 참조를 가지는 변수: let othername = &name;
  • 객체에 대해 가변 참조를 가지는 변수: let othermut = &mut mutname;

러스트에서는 변수가 데이터에 대한 접근 창구이자 권한을 가집니다. 대여는 그 변수가 가리키는 값에 대한 권한을 빌려주어 읽거나 쓸 수 있게 허용하는 것입니다. 대여는 보통 중첩된 범위 내에서 변수를 새로 만들거나 함수 호출에서 매개변수로 넘겨주는 경우에 많이 사용합니다. 빌려준 오너십은 그 변수의 범위가 끝나면 자동으로 돌려받습니다.

러스트에서 변수의 권한 대여에는 여러 가지 규칙이 있습니다. 러스트는 프로그램이 동시에 실행될 가능성을 보장하고자 합니다. 그래서 여러 변수가 동일한 데이터를 접근할 때 동시에 수행되더라도 일관성을 보장할 수 있는 규칙을 지키도록 요구합니다. 이러한 조건들이 Data race 상황을 방지해 줍니다.

  • 가변 변수는 가변 참조 변수에 빌려주고 나면 같은 범위에서는 사용할 수 없습니다. 동일 범위 안에 같은 객체에 대한 두 개 이상의 가변 변수가 생기면 안 됩니다. 이것을 write-write 충돌이 금지되는 것으로 이해할 수 있습니다.
  • 변수에 대해 불변 참조는 여러 개 생길 수 있습니다. 그러나 불변 참조가 생기고 나면 그 변수를 바꾸거나 가변 참조를 생성할 수 없습니다. read-wirte 충돌에 해당합니다. 즉 다른 변수가 읽기 권한을 가지고 있는데 데이터를 바꾸면 dirty-read가 될 수 있다는 것입니다.
  • 가변 참조가 동시에 여러 개 생길 수 없습니다. 가변 참조가 생기면 그것의 범위가 종료하기 전에는 가변 참조를 또 만들 수 없습니다.
  • 가변 변수에 대해 불변 참조를 만드는 것은 문제 없지만, 불변 변수에 대해 가변 참조를 만들 수는 없습니다.

참조는 변수에 대한 포인터라고 볼 수 있습니다. 아래 그림처럼 s1이 "hello"를 가지는 변수인데, s는 그 변수를 가리키는 참조입니다.  let s = &s1;이라고 선언하게 됩니다. C의 포인터와 비슷한 개념으로 이해할 수 있으나 역참조할 때 러스트에서는 *을 붙이지 않아도 자동으로 됩니다. 즉 s를 통해 "hello"를 접근하기 위해 s를 써도 되고 *s를 써도 됩니다. 러스트는 값을 접근하는 경우(r-value로 쓰일 때) 역참조를 몇 번이고 묵시적으로 알아서 해줍니다.

이러한 규칙을 이해하면 러스트 컴파일러가 변수에 대해 불평하는 온갖 것들을 이해할 수 있을 것입니다.

(1) 불변 변수의 값을 변경하려고 하는 오류

fn main() {
  let a = 5;
  a = 7;					// ERROR: 불변 변수 a를 변경해서 오류 발생
} 
/*
2 |     let a = 5;
  |         -
  |         |
  |         first assignment to `a`
  |         help: make this binding mutable: `mut a`
3 |     a = 7;
  |     ^^^^^ cannot assign twice to immutable variable
*/

러스트 컴파일러는 변수에 관한 오류 메시지를 정확히 알려줄 뿐 아니라 어떻게 고치면 좋겠다는 help도 제공합니다. 가변으로 선언된 변수를 변경하지 않으면 mut를 지우라고 알려주기도 합니다. 꼼꼼한 개인교사라고 할 수 있죠.

(2) 가변 참조에 빌려주고 나면 가변 변수 변경 불가

가변참조를 통해 값을 변경할 수 있습니다. 단, 가변 참조 변수의 값을 변경하려면(l-value로 사용할 때는) *를 통해 역참조해야 합니다. 그러나 가변참조가 생기고 나면 같은 범위 내에서는 원래 변수 i는 사용할 수 없습니다.

fn main() {
   let mut i:i32 = 1;
   let ref_i:&mut i32 = &mut i;
   *ref_i = 2;
   i = 3;							// 가변 참조를 빌려주었으므로 변경 불가
// ^^^ assignment to borrowed `i` occurs here
}

(3) 가변 참조를 두 번 이상 빌려주는 오류

동일한 범위에서 가변 참조는 한 번에 하나만 빌려줄 수 있습니다. 

fn main() {
   let mut i:i32 = 1;
   let ref_i = &mut i;
   let another_ref_i = &mut i;  // 가변 참조 빌려주기가 두 번 일어남
//                          ^ second mutable borrow occurs here
}

그러나 다른 가변 참조가 중첩 범위에서 생겼다면 그 범위가 끝나면 사라지므로 아래와 같은 코드는 가능합니다. 중첩 범위에서는 바깥 범위를 shadowing 하므로 문제가 없습니다.

fn main() {
   let mut i:i32 = 1;
   {
     let ref_i = &mut i;
   }
   let another_ref_i = &mut i;
}

(4) 참조를 다시 빌려주는 것 :  reborrow

가변 참조를 다시 대여할 수 있습니다. 이 때는 다음과 같이 *를 이용해 역참조를 해서 대여하면 원래 변수 i를 ref_i를 통해 재대여하는 효과를 얻습니다.

fn main() {
    let mut i:i32 = 1;
    let ref_i = &mut i;
    let another_ref_i = &mut *ref_i; //or &*ref_i
    *ref_i = 2;   // ref_i의 참조를 대여한 후 수정은 안됨
}

마찬가지로 가변 참조를 함수에 넘기는 것도 reborrow, 즉 다시 빌려주는 것입니다. 이 때는 함수가 하나의 범위이므로 함수가 끝나면 가변 참조 ref_i는 변경할 수 있습니다.  

fn test (i:&mut i32) {}
fn main() {
    let mut i:i32 = 1;
    let ref_i = &mut i;
    test (ref_i);
    *ref_i = 2;
}

참고문헌

(1) 가변참조에 대한 자세한 설명과 implicit dereferencing, borrowing은 다음 글을 참고했습니다. 
    Mutable References in Rust, https://medium.com/@vikram.fugro/mutable-reference-in-rust-995320366e22

(2) 참조의 사용과 자동 역참조 내용을 잘 설명한 글입니다.
    References in Rust, https://blog.thoughtram.io/references-in-rust/