Victoree's Blog

[2] Rust의 꽃 : Memory Management와 Ownership 본문

Rust

[2] Rust의 꽃 : Memory Management와 Ownership

victoree 2023. 2. 16. 00:28
728x90

메모리 관리

C/C++은 '개발자가 직접 메모리를 할당&해제 하여 메모리를 관리'하는 방식이고, Java나 C#의 경우 '가비지 콜렉터가 메모리를 관리'하는 방식으로 동작하는데,
Rust의 경우는 언어 자체에 제약을 걸어서 메모리를 컴파일 타임에 싹 다 추적하는 방식을 택하였다. 그 핵심 개념이 ownership(소유권)이다.

Stack 과 Heap

스트링으로 따지면, 스택에는 스트링 포인터와 길이(length), 필요 공간(capacity) 의 데이터를 가지고 있고, heap에는 실제 스트링 텍스트 데이터들이 보관된다.

* Stack

  • 로컬 변수들을 위한 지속적인 메모리 영역
  • Value들은 컴파일 타임에 정해 사이즈를 할당받는다.
  • 스택 포인터를 따라가면 되기 때문에, 극도로 빠르다
  • 함수 호출로 따라갈 수 있어 관리가 쉽다.

* Heap

  • 함수 호출 영역 밖의 저장 공간
  • Value들이 런타임 시에 동적 사이즈로 저장됨
  • 스택보다 약간 느리다.
  • 메모리 지역성을 보장하지 않는다.
  • Heap 메모리 영역을 할당하거나 해제하는 행위는 직접 호출해야한다.

소유권

  1. 러스트의 각각의 값은 해당값의 오너(owner)라고 불리우는 변수를 갖고 있다.
  2. 한번에 딱 하나의 오너만 존재할 수 있다.
  3. 오너가 스코프 밖으로 벗어나는 때, 값은 버려진다(dropped).

String VS String Literal
String Literal의 경우, immutable함. 실제 코드에 하드코딩 되어있음.
String 의 경우, Heap 영역에 할당되고, 컴파일 타임에는 우리가 알수없는 양의 텍스트를 저장할 수 있음. 변경이 가능하고 커질 수 있는 텍스트를 지원하기 위해 만들어졌음.

  1. 런타임에 운영체제로부터 메모리가 요청되어야 한다.
  • String::from 을 호출하면, 구현 부분에서 필요한 만큼의 메모리를 요청함
  1. String의 사용이 끝났을 때, 운영체제에게 메모리를 반납할 방법이 필요하다.

러스트는 변수가 속한 스코프 영역을 벗어나는 순간, 메모리를 반납한다. 러스트는 변수가 스코프 밖으로 나가게 되면, drop이라는 함수를 호출하고, String의 개발자가 메모리를 반환하도록 코드를 집어 넣을 수 있다.

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

s.push_str(", world!"); // push_str()은 해당 스트링 리터럴을 스트링에 붙여줍니다.

println!("{}", s); // 이 부분이 `hello, world!`를 출력할 겁니다.

더블 콜론(::)은 우리가 string_from과 같은 이름을 쓰기 보다는 String 타입 아래의 from 함수를 특정지을 수 있도록 해주는 네임스페이스 연산자입니다.

let s1 = String::from("hello");
let s2 = s1;

println!("{}, world!", s1);

위와 같은 상황에서, 러스트는 drop 함수가 두번 호출되어 double free가 되는 상황을 막거나, S1의 데이터를 복사하여 카피하는 과정이 느려지는 단계를 방지하기 위해, s1이 더이상 유효하지 않는다고 간주하고 무효화시킵니다.
얕은 복사와 개념이 비슷하다고 생각될지 모르지만, 러스트는 첫 변수를 무효화시키기도 하기에 이동이라고 표현(move)합니다.

변수와 데이터가 상호작용하는 방법 : 클론

String의 스택 데이터가 아니라 힙 데이터를 깊이 복사하고 싶다면, clone이라 불리는 공용 메소드를 사용할 수 있다.

정수형과 같이 컴파일 타임에 변수의 크기가 정해져있는 경우, 스택에 모두 저장되기 때문에 실제의 값이 빠르게 복사본이 생성될 수 있다. 이런 경우 clone을 호출하나 = 연산자로 복사를 하나 차이점이 없이 동작한다.

fn main() {
    let s = String::from("hello");  // s가 스코프 안으로 들어왔습니다.

    takes_ownership(s);             // s의 값이 함수 안으로 이동했습니다...
                                    // ... 그리고 이제 더이상 유효하지 않습니다.
    let x = 5;                      // x가 스코프 안으로 들어왔습니다.

    makes_copy(x);                  // x가 함수 안으로 이동했습니다만,
                                    // i32는 Copy가 되므로, x를 이후에 계속
                                    // 사용해도 됩니다.

} // 여기서 x는 스코프 밖으로 나가고, s도 그 후 나갑니다. 하지만 s는 이미 이동되었으므로,
  // 별다른 일이 발생하지 않습니다.

fn takes_ownership(some_string: String) { // some_string이 스코프 안으로 들어왔습니다.
    println!("{}", some_string);
} // 여기서 some_string이 스코프 밖으로 벗어났고 `drop`이 호출됩니다. 메모리는
  // 해제되었습니다.

fn makes_copy(some_integer: i32) { // some_integer이 스코프 안으로 들어왔습니다.
    println!("{}", some_integer);
} // 여기서 some_integer가 스코프 밖으로 벗어났습니다. 별다른 일은 발생하지 않습니다.

takes_ownership()에 s 파라미터를 넘김으로 소유권 move가 일어났고 함수 종료시 drop 함수가 실행되어, 해당 변수는 유효하지 않게 되었음...

참조자

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

    change(&s);
}

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

s의 참조만 넘겼기 때문에, some_string은 s를 직접 변경하는 것이 허용되지 않음. (immutable)

가변 참조자

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

    change(&mut s);
}

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

&mut 한 가변 참조자로 넘기면, 변경하는 것도 수행할 권한이 생김. BUT 특정 스코프 내에 특정 데이터 조각에 대한 가변 참조자는 딱 하나만 생성할 수 있음.
동시에 두 가변 참조자가 생길 경우, 데이터 레이스가 발생할 수 있기 때문..
또한 불변 참조자를 가지고 있을 때도, 가변 참조자를 만들 수 없다. 불변 참조자의 사용자는 사용중인 동안 값이 변할 거라 예상하지 않는데 그 와중에 가변 참조자가 무언가를 변경하거나 데이터를 해제하게 되었을때 영향을 받게 될 수 있기 때문.

정리

  • Rust는 오너십을 기반으로 메모리를 관리한다.
  • 하나의 값(메모리공간)의 오너십은 하나의 변수만이 가질 수 있으므로 중복해제 에러가 일어나지 않는다.
  • 오너십은 String 타입과 같이 할당 받을 메모리의 크기가 정해져 있지 않은 Compound들에 대해서만 적용된다.
728x90

'Rust' 카테고리의 다른 글

[4] 구조체(Struct)와 열거형(Enum)  (0) 2023.02.23
[3] 라이프 타임과 참조자 유효화  (0) 2023.02.16
[1] Rust의 데이터 타입과 함수  (0) 2023.02.16
[0] Rust 주요 특징을 알아보자  (0) 2023.02.15
Rust 를 시작하며..  (2) 2023.02.12
Comments