[Rust] Ownership (소유권)에 대해

고승우·2023년 6월 2일
0

Rust

목록 보기
3/16
post-thumbnail

Ownership (소유권)이란?

Rust에서 메모리 관리하는 방법이다. Rust는 컴파일러가 확인하는 Ownership 규칙을 활용하여 메모리를 관리한다. 하나의 규칙이라도 위반하는 순간 컴파일이 되지 않고, 이로 인한 프로그램의 속도 저하는 없다.

규칙
1. 러스트의 모든 값은 각각의 owner가 존재한다
2. owner는 하나밖에 없다.
3. owner가 범위(Scope)에서 벗어나면 값은 버려진다(dropped).


Stack and the Heap

heap에 데이터를 추가하는 경우, 요구하는 양큼의 공간을 찾고, 메모리 주소가 사용중임을 체크한 이후에 데이터의 위치를 저장하는 포인터를 반환한다. 해당 데이터를 가져오기 위해선 포인터를 찾고 포인터가 가르키는 위치를 방문해야 한다. 따라서 stackheap보다 데이터 추가(push), 추적하는 것이 더 빠르다.
그렇다면 i32String type을 예시로 stack에만 저장되는 type과 heap & stack에 저장되는 type의 차이를 살펴보자. i32 type은 Copy trait이 의 구현체가 있지만, String type에는 존재하지 않다. 즉, stack에 있는 데이터는 Copy 되어 할당되기 때문에, 소유권의 이전이 일어나지 않지만, String type은 Copy의 구현체가 존재하지 않기 때문에 그대로 소유권의 이전이 일어난다.


Variable Scope(변수의 스코프) - Resource Acquisition Is Initialization (RAII)

여기서 변수 sstring literal(&str)을 나타내고 그 값은 우리의 프로그램의 텍스트 내에 하드코딩되어 있다. 메모리는 변수(immutable & mutable)가 소속되어 있는 스코프 밖으로 벗어나는 순간 자동으로 반납된다.

{                      // s는 유효하지 않다.
    let s = "hello";   // s는 이 지점부터 유효하다.

    // s를 가지고 뭔가 합니다.
}                      // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않다.

Process of Ownership moving

ownership은 어떻게 이전될까?

Memory and Allocation (메모리와 할당)

string literal(&str) 이 immutable(불변)인 것과 달리, String 은 가변적이다. immutable data는 stack에 push하고 pop off 되는 것과 달리, mutable data는 heap에 데이터가 할당되고 컴파일 할 때는 그 크기를 짐작할 수 없다. 즉, mutable data는 런타임에 memory allocator 에게 메모리를 요청을 해야 하고, 다시 반납해야 한다. 메모리를 적절하게 allocate 하고 free 하는 것은 프로그래머의 역할이었지만, 2번 free 하면 버그가 발생하는 등 어려움이 있다.


Variable and Data Interacting with move (이동을 통한 변수와 데이터 상호작용)

let x = 5;
let y = x;

위 코드에서 5x에 bind 되고, x의 복사본을 만들어 y에 bind한다. 5는 고정된 메모리를 필요로 하기 때문에 stack에 push된다.


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

위 코드도 유사해 보이지만, String은 메모리를 동적으로 할당해야 하기 때문에 다르게 작동한다. String 뿐만 아니라 동적으로 작동하는 vec와 같은 데이터 타입도 적용된다.


Figure 4-1을 보면 String은 3가지 부분으로 만들어져 있다. 왼쪽에 있는 부분은 stack에 저장되고, 오른쪽에 있는 부분은 heap에 저장된다.


Figure 4-2에서 s1s2로 할당하면, String 데이터가 복사된다. 포인터가 가리키고 있는 heap 메모리 상의 데이터는 복사되지 않는다. 만약 s1s2가 범위에서 벗어난다면, 두 가지 모두 같은 메모리를 free 하려고 하기 때문에, 흔히 알려진 double free error가 발생하게 된다.


Figure 4-3과 같이 작동하진 않지만, 이 그림과 같이 작동한다면 s2 = s1 연산은 런타임 상에서 매우 느려질 것이다.

let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
// compile error 


rust에선 shallow copy를 한 이후에, 첫 번째 변수를 무효화하기 때문에 moved라는 개념을 사용한다. s2가 범위를 벗어날 경우 s2만 free하면 되기 때문에, 메모리 에러를 방지한다.


Variables and Data Interacting with Clone (clone을 활용해 변수와 데이터 상호작용하기)

만약 String과 같은 heap data를 deeply copy하기 위해서, clone 메소드를 활용할 수 있다. clone을 활용하면 위에 나온 Figure 4-3과 같은 상황을 만들 수 있다. stack data는 실제 데이터를 만드는 것이 빨라 이러한 작업이 필요하지 않다.

let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);

Example

struct Person { name: String, birth: i32 }

let mut composers = Vec::new();

composers.push(Person { name: "Palestrina".to_string(),
 						birth: 1525 });
composers.push(Person { name: "Dowland".to_string(),
 						birth: 1563 });
composers.push(Person { name: "Lully".to_string(),
 						birth: 1632 });
 
for composer in &composers {
	println!("{}, born {}", composer.name, composer.birth);
}

  • "Palestrina"의 소유권은 name이 갖고 있다.
  • struct Person의 field name, birth의 소유권은 객체가 소유하고 있다.
  • vec composer의 element인 struct의 소유권은 모두 composer가 갖고 있다.

Partialy move Ownership

그렇다면 struct에서 한 field의 소유권을 가져올 수 있을까?

#[derive(Debug)]
struct tmp{
    num: i32,
    string: String
}

fn main(){
    let t = tmp{
                num :1,
                string: String::from("1")};
    let a = t.num;
    println!("{:?}", t);
    
    let b = t.string;
    println!("{:?}", t);	// borrow of partially moved value
}


같은 struct의 field이지만, Stringi32와 달리 복사되지 않아, 소유권이 b에게 넘어가게 된다. 이러한 경우 struct t의 ownership이 b에게 partially move 한 것이다. t의 모든 field에 대한 소유권이 돌아오지 않는다면 t에는 접근할 수 없다. 하지만, 다른 field에 대한 소유권은 접근할 수 있다.

#[derive(Debug)]
struct tmp{
    num: i32,
    string: String
}

fn main(){
    let mut t = tmp{
                num :1,
                string: String::from("1")};
    let a = t.num;
    println!("{:?}", t);
    
    let b = t.string;
    t.string = String::from("Restored field");
    println!("{:?}", t);
    
    // tmp { num: 1, string: "1" }
    // tmp { num: 1, string: "Restored field" }
}

이 코드는 struct t가 모든 field가 소유권을 갖고 있어 다시 t에 접근할 수 있게 된 경우이다.

profile
٩( ᐛ )و 

0개의 댓글