[Rust] Ownership

suwonyoo·2025년 2월 2일

Rust Study | Special

목록 보기
1/4
post-thumbnail

Rust를 공부하면서 주의깊게 공부해야 할 부분은 아무래도 Ownership, 그리고 String &str의 차이라고 판단하였다. 기본 개념을 꽉 잡고 들어가지 않는다면 나중에 흔들릴 수 있기 때문에, 햇갈리거나 중요하다고 판단한 부분들을 차례대로 정리해 나가보겠다. 특히 메모리 할당 부분을 생각하면서 진행해보겠다.


What is Ownership?


1. GC가 없는 Rust

  • 변수가 자신이 소속된 스코프를 벗어나는 순간 자동으로 메모리를 해제하는 방식으로 메모리 문제를 해결하였습니다. 이를 Ownership이 해결합니다.

2. Ownership의 세가지 특성

  1. 러스트에서, 각각의 값은 소유자 (owner) 가 정해져 있습니다.
  2. 한 값의 소유자는 동시에 여럿 존재할 수 없습니다.
  3. 소유자가 스코프 밖으로 벗어날 때, 값은 버려집니다 (dropped).
  • 러스트는 변수가 스코프 밖으로 벗어나면 drop이라는 특별한 함수를 호출합니다

3. Ownership의 소유권 이동

  • 힙에 저장되는 데이터 (예:String)의 경우에는 아래와 같이, 대입을 한 경우 String Data(스택에 있는 데이터)가 복사되며, 힙 영역의 데이터는 복사되지 않는다. 메모리 안정성을 보장하기 위해서, 러스트는 s1이 더 이상 유효하지 않다고 판단하여, 소유권을 s2에게 넘긴다.
      let s1 = String::from("hello");
      let s2 = s1; // 오류 발생
  1. "hello"라는 문자열 데이터가 에 저장됨.
  2. s1 변수가 생성됨.
  3. s1은 힙을 가리키는 포인터, 길이, 용량을 스택에 저장한다.
  4. s1이 스코프를 벗어나면 Drop 트레잇이 실행되어 힙 메모리가 해제됨.

이렇게 사용하는 이유는 무엇일까요? 간단합니다. 만약에 s2, s1이 대입 이후에도 소유권을 모두 가지고 있는 경우, 만약 스코프 밖으로 벗어날 때 각각 메모리를 해제하게 되면 중복 해제 (double free) 에러가 발생할 겁니다.

  • 힙에 저장되는 데이터까지 복사하여 사용하고 싶은 경우, clone이라는 method를 사용할 수 있다.
      let s1 = String::from("hello");
      let s2 = s1.clone();
  • 스택에 저장되는 데이터는 간단합니다. 컴파일 타임에 크기가 고정되는 타입은 모두 스택에 저장되므로, 복사본을 빠르게 만들 수 있기 때문에, 힙에 저장되는 데이터와 같이 대입 이후에도 무효화할 필요가 없다. 즉 깊은 복사와 얕은 복사 간의 차이가 없다.

      let x = 5;
      let y = x;

      println!("x = {}, y = {}", x, y);

4. Refernce와 Borrowing

  • 함수의 인자를 넘길 때도, 일반적인 변수를 이용할 시 값의 소유권이 넘어갈 수 있기 때문에, 소유권을 넘기는 대신 개체의 참조자를 넘겨주는 방법을 이용한다.
      fn main() {
          let s1 = String::from("hello");

          let len = calculate_length(&s1);

          println!("The length of '{}' is {}.", s1, len);
      }

	  // 여기서 s는 함수 내에서만 사용할 수 있는 변수명이며, &String은 이 변수가 가질 타입입
      fn calculate_length(s: &String) -> usize {
          s.len()
      }

&String s는 String s1을 가리킴

  1. "hello"라는 문자열 데이터가 힙에 저장된다
  2. s1변수가 생성됨.
  3. s1은 힙을 가르키는 포인터, 길이, 용량을 스택에 저장한다.
  4. &s1이라는, s1값을 참조하지만, 해당 값을 소유하지 않는 참조자를 생성한다.
  5. calculate_length는, &String타입을 갖는 변수를 s인자로 받는다.
  6. calculate_length가 인자로 받은 변수는 해당 값을 소유하지 않으므로, s가 사용되지 않더라도, 참조자가 가리킨 값인 s1이 버려지지 않습니다.

이때, &T가 원본 타입의 메서드를 그대로 사용할 수 있는데, Rust의 자동 역참조(auto deref) 기능 덕분에, &T는 원본 T의 메서드를 그대로 사용할 수 있습니다.

  • 가변 참조자 (mutable reference) 를 사용하여, 참조자가 참조하는 것을 수정할수 있음, 다만 같은 데이터에 대하여 동시에 여러 가변 참조자의 사용을 막음.
    fn main() {
        let mut s = String::from("hello");

        change(&mut s);
    }

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

  • 댕글링 포인터(dangling pointer) : 어떤 메모리를 가리키는 포인터가 남아있는 상황에서 일부 메모리를 해제해 버림으로써, 다른 개체가 할당받았을지도 모르는 메모리를 참조하게 된 포인터
    fn main() {
        let reference_to_nothing = dangle();
    }

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

        &s
    }
  • sdangle 함수 내에서 생성됐기 때문에, 함수가 끝날 때 할당 해제됩니다. 하지만 코드에서는 &s를 반환하려 했고, 이는 유효하지 않은 String을 가리키는 참조자를 반환하는 행위이기 때문에 에러가 발생합니다.

아래 그림과 같이, &s가 참조하는 값인 s는 dangle()함수 스코프 밖에서 메모리에서 사라지므로, &s가 가리키는 값은 그대로 남아있는데, 해당 가리키는 값(주소)에 다른 개체가 할당받았을 경우, 모르는 메모리를 참조하게 될 가능성이 있기 때문에 이를 댕글링 포인터라고 한다.

  • 이에 대한 해결법은, 간단하다. 없어질 것 같은 변수의 소유권을 이동시키면 그만.
      fn no_dangle() -> String {
          let s = String::from("hello");

          s
      }

0개의 댓글