Borrow
Rust에서 소유권을 이전하지 않고 변수를 전달하기 위해 레퍼런스(reference)를 사용하는데, 이러한 레퍼런스를 생성하는 것을 Borrow(빌림)라고 부른다.
레퍼런스(reference)
앞의 Ownership (소유권) 아티클에서 변수를 재할당하거나 함수의 파라미터로 전달할 때 혹은 함수에서 리턴할 때 소유권이 이전(move)되는 예들을 살펴 보았다. 그러면 소유권을 이전하지 않으면서 이러한 일들을 할 수 있을까? Rust에서 이렇게 소유권 이전 없이 변수를 전달/할당하기 위해 & 레퍼런스(reference)를 사용한다. 예를 들어, String 타입의 변수 s를 함수의 파라미터로 전달할 때, 아래와 같이 레퍼런스를 사용하면 소유권을 move 하지 않고도 변수를 전달할 수 있다.
fn main() { let s = String::from("HELLO"); // 변수 s가 문자열의 owner가 됨 print_data(&s); // &s와 같이 s의 레퍼런스를 전달함 } fn print_data(data: &String) { // 파라미터 data는 &String 레퍼런스 타입임 println!("{}", data); // data 사용 } // 여기에서 data는 Scope를 벗어나서 소멸. // 그러나, data는 단지 레퍼런스이므로 Heap 메모리 해제는 없음.
위 예제에서 변수 s는 let 문에서 String 타입의 데이타에 대한 Owner가 되는데, print_data() 함수를 호출할 때, &s 와 같이 변수 s의 레퍼런스로 전달된다. 이때 레퍼런스는 Stack에 있는 변수 s에 대한 포인터만을 갖는 것으로, 레퍼런스가 Scope를 벗어나더라도 변수 s와 Heap 상에 있는 실제 String 데이타는 소멸되지 않는다. print_data() 함수는 변수 s의 레퍼런스를 data라는 파라미터명으로 받아들이는데, 함수 내에서 이 파라미터를 사용하고 함수를 벗어날 때 레퍼런스 파라미터 data는 소멸된다. 하지만, 레퍼런스가 가리켰던 변수 s와 이 변수가 가리키는 메모리 공간을 소멸되지 않는다. 레퍼런스가 소멸할 때는 Heap 메모리를 없애는 drop 함수를 호출하지 않는다.
위 예제의 문자열 레퍼런스는 읽기 전용 즉 Immutable 레퍼런스이다. Rust의 모두 타입이 디폴트로 Immutable 이듯이, 레퍼런스(&)로 기본적으로 Immutable이다. 만약 변수 s가 가리키는 String 문자열의 데이타를 변경하는 레퍼런스를 갖고 싶다면, &mut 키워드를 써서 아래와 같이 Mutable 레퍼런스로 바꾸면 된다.
fn main() { let mut s = String::from("HELLO"); //변수 s를 mut로 선언 print_data(&mut s); // &mut 즉 Mutable Reference로 전달 println!("s={}", s); // "HELLO WORLD" 출력 } fn print_data(data: &mut String) { // &mut 로 정의 data.push_str(" WORLD"); // 여기서 문자열 변경 가능함 println!("data={}", data); // "HELLO WORLD" 출력 }
위의 예에서 변수 s가 수정되는 것이므로, 변수 s가 mut 로 선언되어야 한다. 변수 s에 대해 "&s"은 Immutable 레퍼런스이고, "&mut s"은 Mutable 레퍼런스이다. 즉, 변수 s가 "mut String" 타입일 때, 이 변수 s에 대해 Immutable 레퍼런스를 표현하기 위해서는 &s 라고 쓰고, Mutable 레퍼런스를 표현하기 위해서는 &mut s 라고 쓴다. 그리고, 파라미터를 전달하는 쪽 뿐만 아니라, 함수 선언에서도 마찬가지로 &mut 를 사용한다. Mutable 레퍼런스를 사용하여 Heap 데이타를 수정하면, 소유권을 가진 변수 s의 데이타가 변경된다.
Rust에서 Mutable 레퍼런스는 한가지 제약조건이 있는데, 그것은 한번에 "하나의 Mutable 레퍼런스만"을 가질 수 있다는 것이다. 이는 프로그램 상에서 하나의 변수에 대해 2개 이상의 Mutable 레퍼런스를 만들 수 없다는 것을 의미하며, 또한 Mutable 레퍼런스와 다른 Immutable 레퍼런스를 동시에 가질 수 없다는 것을 의미한다. 만약 Mutable 레퍼런스가 없다면, Immutable 레퍼런스는 여러 개 가질 수 있다. 즉, Rust 프로그램은 한 싯점에 하나의 Mutable 레퍼런스를 갖거나 혹은 하나 이상의 Immutable 레퍼런스를 가질 수 있다.
아래 예제에서 s1과 s2는 변수 s에 대한 Immutable 레퍼런스이고, 변수 s3는 Mutable 레퍼런스이다. 이들 s1과 s3가 println!() 에서 동시에 사용되고 있으므로, 에러가 발생한다. 만약 println!()을 코멘트로 막아 버린다면, (Warning은 발생하지만) 에러가 발생하지 않는다. 즉, s1, s2, s3가 아예 사용되지 않으면, 굳이 에러를 발생시키지 않는다. 하지만, 코드에서 이들이 함께 된다면, Mutable 레퍼런스와 Immutable 레퍼런스가 동시에 사용되기 때문에 에러로 처리한다.
fn main() { let mut s = String::from("HELLO"); let s1 = &s; let s2 = &s; let s3 = &mut s; println!("{}, {}", s1, s3); // 에러 }
만약 레퍼런스를 사용할 때, 그 레퍼런스가 가리키는 Heap 메모리가 없는 경우가 있을 수 있을까? C/C++ 에서 경험할 수 있는 이러한 문제는 Dangling Pointer 문제라고 한다. 즉, 포인터를 통해 메모리를 엑세스했을 때, 이미 다른 포인터가 메모리를 해제해 버린 것이다. 이러한 문제는 Rust에서 절대 발생하지 않는다. 컴파일러는 이러한 문제(Dangling Reference)를 컴파일시에 미리 판단하여 에러를 발생시키기 때문이다.
아래 예제를 Dangling Reference 에러를 예시한 것으로, get_data() 함수에서 변수 s가 생성되고 이것의 레퍼런스를 리턴하는데, 실제 변수 s는 get_data() 함수가 끝날 때, 메모리 해제를 하므로 레퍼런스가 가리키는 메모리가 이미 해제된 상태가 된다. 컴파일러는 이를 체크하여 에러를 발생시키고 빌드를 중지한다.
fn main() { let mydata = get_data(); // 레퍼런스 받아오지만 메모리 이미 해제됨 println!("{}", mydata); } fn get_data() -> &String { let s: String = "Data".to_owned(); // String 타입의 owner 생성 &s // 변수 s의 레퍼런스 리턴 } // 여기서 변수 s가 Scope를 벗어나 메모리 해제됨