오너십
러스트의 메모리 관리는 컴파일러가 몇 가지 규칙을 이용하여 컴파일 타임 체크를 함으로서 이루어짐. 여기서 중요한 개념이 오너십.
오너십의 규칙
- 러스트의 각 값은 각자의 변수를 가지며, 이 변수를 오너라 한다.
- 한 번에 하나의 오너만이 존재한다.
- 오너가 범위(스코프)를 벗어나면, 그 값은 폐기된다.
변수의 범위
변수는 기본적으로 아래와 같이 중괄호 블록 안에서 유효함.
{ // 변수 s 는 무효: 아직 선언되지 않음.
let s = "hello"; // 변수 s 는 유효: 여기부터 시작
} // 범위의 끝. 변수 s 는 이제 무효함.
스트링 타입의 예
String 타입과 스트링 리터럴은 다름. 프로그램에 하드코딩된 문자열은 스트링 리터럴. 스트링 리터럴은 임뮤터블이며, 코드에 그대로 존재하기 때문에 컴파일 되면 바이너리에 그대로 입력되고, 고정된 값이기에 관리에 어려움이 없음.
반면 런타임에 사용자의 인풋으로 들어온 문자열은 String 타입. String 타입은 뮤터블이며, 그 크기가 어느 정도인지 컴파일 타임에 알 수 없음. String 타입은 힙에 위치함.
String 타임은 아래의 방법으로 생성할 수 있음.
let s = String::from("hello");
이렇게 하면 문자열 "hello" 는 String 타입으로, 힙에 위치하고, 변수 s 가 그 주소를 가짐. (포인터)
변수 s 가 유효 범위를 벗어나면, String 타입 "hello" 는 제거되어야 함. GC 가 있는 언어는 GC 가 그 역할을 담당하고, GC 가 없는 언어는 프로그래머가 일일이 메모리 해제 코드를 작성해야 함. 이는 피곤한 일이며, 실수로 메모리 해제를 잊게 되면 이는 메모리 낭비로 이어짐. (메모리 릭) 너무 일찍 해제하면 필요한 값이 사라져서 변수가 능력을 상실하게 되됨. 같은 메모리를 두 번 해제하면 프로그램 버그로 이어질 수 있음. 오직 한 번의 메모리 할당(allocate)에 정확히 한 번의 해제(free)가 수행되어야 함.
러스트의 접근 방식: 변수가 유효 범위를 벗어나면 메모리는 자동으로 반환(해제)됨. 러스트는 이 동작을 위해 drop 이라는 특별한 함수를 호출함. 하지만 한 변수가 범위를 벗어났다고 해서 무조건 그 메모리를 해제하면 곤란함. 프로그램의 런타임에는 다양한 상황이 고려되어야 하기에, 러스트는 각각의 상황에 맞는 대응을 함.
데이터 이동
이 경우, 5 는 인티저 타입으로, 단순하며 값의 크기가 고정되어 있음. 변수 x 와 y 는 스택에 위치하여 자신의 값 자체를 자신이 가짐.
let s1 = String:from("hello");
let s2 = s1;
이 경우, s1 은 String 타입으로, 힙에 위치함. 변수 s1 와 s2 은 값을 가진 것이 아닌, 값의 포인터(주소)를 가짐.
len: 데이터의 길이
capacity: 데이터를 위해 할당된 메모리의 용량
변수 s1 에 s2 를 대입함은 값을 복사하는 것이 아닌, 포인터를 복사하는 것. 결과는 아래와 같음.
결국 두 변수가 같은 하나의 값을 공유하는 것. 이런 방식을 얕은 복사(shallow copy)라 함. 만약 값 자체를 복사(deep copy)한다면 아래와 같이 될 것. 러스트는 이렇게 동작하지 않음. (일반적으로 다른 프로그래밍 언어도 마찬가지) 값의 크기에 따라 이는 굉장히 비싼 작업이 될 수 있음.
여튼, 변수 s1 와 s2 는 하나의 값을 공유함. 그런데 두 변수가 모두 스코프를 벗어나면? 여기서 문제가 발생: 같은 메모리에 대해 두 번의 메모리 해제 요청이 발생하며 이는 프로그램의 안정성을 심각하게 떨어트릴 수 있음.
이 상황에 러스트가 취한 방식은, 변수 s1 을 완전히 무효화하여 s1 이 유효 범위를 벗어나도 메모리를 해제할 필요가 없도록 만드는 것.
let s1 = String::from("hello");
let s2 = s1;
println!("{}, world!", s1);
위 코드는 아래와 같은 컴파일 에러를 일으킴.
error[E0382]: use of moved value: 's1'
...
= note: move occurs because 's1' has type 'std::string::String', which does not implement the 'Copy' trait
러스트는 얕은 복사를 수행하면서 동시에 기존 변수를 무효화 하여 더 이상 사용 불가능하도록 함. 이러한 특성 때문에, 러스트는 이 동작은 '복사' 가 아닌, '이동(move)' 이라 칭함. 위 상황은 변수 s1 이 s2 으로 '이동' 한 것.
s1 의 포인터, len, capacity 정보를 s2 로 복사한 후, s1 자신은 폐기됨.
폐기된 변수에 메모리 해제 작업은 수행되지 않음. 두 변수가 유효 범위를 벗어나면 변수 s2 에만 메모리 해제 작업이 수행되고, 이렇게 메모리 중복 해제 문제는 해결됨.
데이터 복제
힙에 있는 데이터를 복사하고 싶을 경우, 함수 clone 을 호출.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
이제 힙의 데이터가 복제되어, 변수 s1, s2 은 각각 다른 데이터를 가리킴.
스택의 데이터
스택의 데이터는 값 자체가 복사되며, 기존 변수는 폐기되지 않음: 예로, 인티저 타입의 경우 데이터의 사이즈를 컴파일 타임에 알 수 있음. 때문에 값을 복사하는 일이 복잡하거나 많은 시간을 필요로 하지 않음. 그리고 다른 변수에 값을 복사한 뒤 기존 변수를 폐기하거나 할 이유가 없음.
러스트에는 Copy 라는 특별한 표식이 있어, 이 표식을 지닌 타입의 변수는 값 복사 + 폐기되지 않게 됨. (ex: 인티저) 만약 Copy 표식을 Drop 표식이 있거나, 부분적으로 Drop 표식을 가진 타입에 추가하면 러스트는 컴파일 에러를 일으킴.
어떤 타입이 Copy 대상인가? -일반적으로 아래 타입들이 있음
- 모든 인티저 (ex: u32)
- 불린 (boolean)
- 캐릭터 (char)
- 플로팅 포인트 (ex: f64)
- 튜플 -안에 Copy 타입의 데이터만 존재할 경우
오너쉽과 함수
함수 파라미터로 값을 넘기는 일도 변수에의 값 대입과 같은 동작이 수행됨. 때문에 지금까지 일반 변수로 다룬 내용이 여기에 그대로 적용됨.
fn main() {
let s = String::from("hello"); // String 타입 변수 s 생성
takes_ownership(s); // s 의 값이 함수 안으로 '이동'됨. 이제 변수 s 는 무효화되어 더 이상 사용될 수 없음.
let x = 5; // 인티저 타입 변수 s 생성
makes_copy(x); // x 의 값이 함수 안으로 '복사'됨. 변수 x 는 여전히 유효함.
}
fn takes_ownership(some_string: String) { // 파라미터로 값이 '이동'되어 들어옴.
println!("{}", some_string);
} // 파라미터 변수 some_string 는 유효 범위를 벗어남. 변수가 가리키던 힙 메모리는 해제됨. (drop 호출)
fn makes_copy(some_integer: i32) { // 파라미터로 값이 '복사'되어 들어옴.
println!("{}", some_string);
} // 파라미터 변수 some_integer 는 유효 범위를 벗어남. 아무 일도 일어나지 않음.
만약 변수 s 를 takes_ownership 호출 후에 사용하려 했다면 컴파일 에러가 발생할 것.
반환값과 범위
함수의 값 반환에도 똑같은 일이 발생함.
fn main() {
let s1 = gives_ownership(); // gives_ownership 함수가 반환값을 변수 s1 로 '이동'시킴.
let s2 = String::from("hello"); // String 타입 변수 s1 생성.
let s3 = takes_and_gives_back(s2); // 변수 s2 가 takes_and_gives_back 으로 '이동'하며 폐기되고,
// takes_and_gives_back 함수의 반환값이 변수 s3 로 '이동'됨.
}
fn gives_ownership() -> String { // String 반환값이 있음. 반환값은 '이동'된다는 의미.
let some_string = String::from("hello");
some_string // 생성된 some_string 을 반환 (이동)
}
fn takes_and_gives_back(a_string: String) -> String {
a_string // 파라미터로 넘겨받은 값을 다시 반환 (이동)
}
아래와 같이 동시에 다른 타입의 여러 값도 반환 가능.
fn main() {
let s1 = String:from("hello");
let (s2, len) = calculate_length(s1);
}
fn calculate_length(s: String) -> (String, usize) {
let length = s.len();
(s, length)
}