본문 바로가기
Rust

Rust 설치부터 실행까지 (참조자) - 9

by lms0806 2025. 1. 5.
728x90
반응형

오늘은 Rust의 참조자(reference)에 대해 알아보고자 합니다.

참조와 대여

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)
}

이전 예제에서 Stringcalculate_length로 이동되어서, calculate_length를 호출한 함수에서 String도 반환하여, 함수 호출 이후에도 String을 사용할 수 있게 하였었습니다.

 

이렇게 하는 대신 해당 주소에 저장된 데이터에 접근할 수 있도록 해주는 주소값에 해당하는 포인터와 같은 참조자(reference)가 있습니다.

 

다음은 위에의 예제코드에 참조자를 넘겨주는 방법으로 구현된 예제입니다.

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을 사용했습니다.

앰퍼센드(&) 기호가 참조자를 나타내고, 어떤 값의 소유권을 가져오지 않고 해당 값을 참조할 수 있도록 해준다.

 

s1&를 붙인 &s1 구문은 s1 값을 참조하지만 해당 값을 소유하지 않은 참조라를 생성합니다. 값을 소유하지 않으므로 이 참조자를 가리킨 값은 참조자가 사용되지 않을 때까지 버려지지 않습니다.

 

이처럼 참조자를 만드는 행위를 대여(borrow)라고 합니다.

 

그럼, 빌린 값을 수정하면 어떻게 될까요?

fn main() {
    let s = String::from("hello");

    change(&s);
}

fn change(some_string: &String) {
    some_string.push_str(", world");
}

다음과 같인 &s로 값을 참조하여 받아오고, push_str로 값을 수정할려고 할때, rust는 다음과 같은 에러를 출력합니다.

   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 s로 가변 참조자를 생성하고, 함수에서 가변 참조자를 전달받도록 &mut String으로 수정합니다.

 

가변 참조자는 한 가지 큰 제약사항이 있습니다.


어떤 값에 대한 가변 참조자가 있다면, 그 값에 대한 참조자는 더 이상 만들 수 없습니다.

 

다음과 같은 코드에서는 에러가 발생합니다.

    let mut s = String::from("hello");

    let r1 = &mut s;
    let r2 = &mut s;

    println!("{}, {}", r1, r2);

s 를 가변으로 2번 빌려올 수 없기 때문에 코드가 유효하지 않습니다.

 

r1이 가변 참조자로 대여하고, println!이 있을 때까지 살아있으므로, r2s의 가변 참조자로 값을 빌려올 수 없습니다.

 

이러한 방법으로 인하여 데이터 경합(date race)를 방지할 수 있습니다.

 

데이터 경합이란 다음 3가지 상황이 겹칠 때 일어나는 특정한 경합 조건 입니다.

  1. 둘 이상의 포인터가 동시에 같은 데이터에 접근
  2. 포인터 중 하나 이상이 데이터에 쓰기 작업을 시행
  3. 데이터 접근 동기화 매커니즘이 없음

데이터 경합은 정의되지 않은 동작을 일으키며, 런타임에 추적하려고 할 때 문제 진단 및 수정이 어렵습니다.


하지만, rust에서는 데이터 경합이 발생할 가능성이 있는 코드의 컴파일을 거부하여 이 문제를 막아줍니다.

 

이 문제를 회피하는 방법으로는 다음과 같이 중괄호로 새로운 스코프를 만들어, 가변 참조자를 여러개 만들 수 있습니다.

    let mut s = String::from("hello");

    {
        let r1 = &mut s;
    }

    let r2 = &mut s;

다음과 같이 가변 참조자불변 참조자를 혼용할 때에도 유사한 규칙이 적용됩니다.

    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    let r3 = &mut s;

    println!("{}, {}, and {}", r1, r2, r3);

불변 참조자를 사용하는 쪽에서는 사용 중 값이 변경되지 않을 것이라고 생각되므로 불변 참조자가 있는 동안 같은 값의 가변 참조자를 만드는 것 또한 불가능합니다.

 

반면 데이터를 읽기만 하는 불변 참조자를 여러 개 만드는 것은 가능합니다.

 

참조자는 정의된 지점부터, 참조자가 마지막으로 사용된 부분까지 유효합니다.

 

그래서 다음과 같은 코드는 정상적으로 작동합니다.

    let mut s = String::from("hello");

    let r1 = &s;
    let r2 = &s;
    println!("{} and {}", r1, r2);

    let r3 = &mut s;
    println!("{}", r3);

불변 참조자 r1, r2println!이후로 종료되고, 해당 println!은 가변 참조자 r3가 생성되기 전이니, 서로 스코프가 겹치지 않아 이 코드는 문제가 없습니다.

 

이러한 제약때문에 프로그래밍을 하는데 골치가 아플 수 있습니다만, rust 컴파일러가 코드에 숨어 있는 버그를 런타임이 아닌 컴파일 타임에 찾아내어 어느 부분이 문제인지 정확히 집어주는 기능이라는 점을 생각해주시면 됩니다.

댕글링 참조

댕글링 포인터(dangling pointer)는 어떤 메모리를 가리키는 포인터가 남아있는 상황에서 일부 메모리를 해제하여, 다른 개체가 할당받았을지도 모르는 메모리를 참조하게 된 포인터를 말합니다.

 

포인터가 있는 언어에서는 자칫 잘못하면 댕글링 포인터를 만들기 쉽습니다.

 

그러나, rust에서는 어떤 데이터의 참조자를 만들면, 해당 참조자가 스코프를 벗어나기 전에 데이터가 먼저 스코프를 벗어나는지, 컴파일러에서 확인하여 댕글링 참조가 생성되지 않도록 합니다.

fn main() {
    let reference_to_nothing = dangle();
}

fn dangle() -> &String {
    let s = String::from("hello");

    &s
}

해당 코드의 에러 내용은 다음과 같습니다.

  = help: this function's return type contains a borrowed value, but there is no value for it to be borrowed from
help: consider using the `'static` lifetime

함수에서 빌린 값(&s)을 반환하고 있으나, 빌린 실제 값(s)이 존재하지 않습니다.

 

s가 함수의 스코프가 끝남과 동시에 해제가 되어, s의 참조자인 &s를 반환하지 못하여 발생합니다.

 

이럴 경우에는 String을 직접 반환하여야 합니다.

fn no_dangle() -> String {
    let s = String::from("hello");

    s
}

정리

  1. 참조자는 가변 참조자와 불변 참조자가 있다.
  2. 불변 참조자는 해당 값을 빌려와 읽기만 가능하고, 가변 참조자는 해당 값의 수정 권한도 주어진다.
  3. 불변 참조자는 읽기만 가능하여 여러개 만들 수 있지만, 쓰기가 가능한 가변 참조자는 1개만 선언이 가능하다.
  4. 불변 참조자가 선언되어 있을 때, 가변 참조자를 선언할 수 없다.
  5. 가변 참조자는 2개 이상 선언이 불가능하다.
  6. 다른 언어에서 발생할 수 있는, 댕글링 포인터를 컴파일 시점에 잡아준다.
728x90
반응형

댓글