Rust에서 메모리 관리하는 방법이다. Rust는 컴파일러가 확인하는 Ownership 규칙을 활용하여 메모리를 관리한다. 하나의 규칙이라도 위반하는 순간 컴파일이 되지 않고, 이로 인한 프로그램의 속도 저하는 없다.
규칙
1. 러스트의 모든 값은 각각의 owner가 존재한다
2. owner는 하나밖에 없다.
3. owner가 범위(Scope)에서 벗어나면 값은 버려진다(dropped).
heap
에 데이터를 추가하는 경우, 요구하는 양큼의 공간을 찾고, 메모리 주소가 사용중임을 체크한 이후에 데이터의 위치를 저장하는 포인터를 반환한다. 해당 데이터를 가져오기 위해선 포인터를 찾고 포인터가 가르키는 위치를 방문해야 한다. 따라서 stack
이 heap
보다 데이터 추가(push), 추적하는 것이 더 빠르다.
그렇다면 i32
과 String
type을 예시로 stack에만 저장되는 type과 heap & stack에 저장되는 type의 차이를 살펴보자. i32
type은 Copy trait이 의 구현체가 있지만, String
type에는 존재하지 않다. 즉, stack에 있는 데이터는 Copy
되어 할당되기 때문에, 소유권의 이전이 일어나지 않지만, String
type은 Copy
의 구현체가 존재하지 않기 때문에 그대로 소유권의 이전이 일어난다.
여기서 변수 s
는 string literal(&str)
을 나타내고 그 값은 우리의 프로그램의 텍스트 내에 하드코딩되어 있다. 메모리는 변수(immutable & mutable)가 소속되어 있는 스코프 밖으로 벗어나는 순간 자동으로 반납된다.
{ // s는 유효하지 않다.
let s = "hello"; // s는 이 지점부터 유효하다.
// s를 가지고 뭔가 합니다.
} // 이 스코프는 이제 끝이므로, s는 더이상 유효하지 않다.
ownership은 어떻게 이전될까?
string literal(&str)
이 immutable(불변)인 것과 달리, String
은 가변적이다. immutable data는 stack
에 push하고 pop off 되는 것과 달리, mutable data는 heap
에 데이터가 할당되고 컴파일 할 때는 그 크기를 짐작할 수 없다. 즉, mutable data는 런타임에 memory allocator 에게 메모리를 요청을 해야 하고, 다시 반납해야 한다. 메모리를 적절하게 allocate 하고 free 하는 것은 프로그래머의 역할이었지만, 2번 free 하면 버그가 발생하는 등 어려움이 있다.
let x = 5;
let y = x;
위 코드에서 5
는 x
에 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에서 s1
을 s2
로 할당하면, String
데이터가 복사된다. 포인터가 가리키고 있는 heap
메모리 상의 데이터는 복사되지 않는다. 만약 s1
과 s2
가 범위에서 벗어난다면, 두 가지 모두 같은 메모리를 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하면 되기 때문에, 메모리 에러를 방지한다.
만약 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);
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
의 fieldname
,birth
의 소유권은 객체가 소유하고 있다.- vec
composer
의 element인 struct의 소유권은 모두composer
가 갖고 있다.
그렇다면 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이지만, String
은 i32
와 달리 복사되지 않아, 소유권이 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에 접근할 수 있게 된 경우이다.