오너십
러스트의 메모리 관리는 컴파일러가 몇 가지 규칙을 이용하여 컴파일 타임 체크를 함으로서 이루어짐. 여기서 중요한 개념이 오너십.
오너십의 규칙
- 러스트의 각 값은 각자의 변수를 가지며, 이 변수를 오너라 한다.
- 한 번에 하나의 오너만이 존재한다.
- 오너가 범위(스코프)를 벗어나면, 그 값은 폐기된다.
스트링 타입의 예
String 타입과 스트링 리터럴은 다름. 프로그램에 하드코딩된 문자열은 스트링 리터럴. 스트링 리터럴은 임뮤터블이며, 코드에 그대로 존재하기 때문에 컴파일 되면 바이너리에 그대로 입력되고, 고정된 값이기에 관리에 어려움이 없음.
반면 런타임에 사용자의 인풋으로 들어온 문자열은 String 타입. String 타입은 뮤터블이며, 그 크기가 어느 정도인지 컴파일 타임에 알 수 없음. String 타입은 힙에 위치함.
String 타임은 아래의 방법으로 생성할 수 있음.
let s = String::from("hello");
이렇게 하면 문자열 "hello" 는 String 타입으로, 힙에 위치하고, 변수 s 가 그 주소를 가짐. (포인터)
변수 s 가 유효 범위를 벗어나면, String 타입 "hello" 는 제거되어야 함. GC 가 있는 언어는 GC 가 그 역할을 담당하고, GC 가 없는 언어는 프로그래머가 일일이 메모리 해제 코드를 작성해야 함. 이는 피곤한 일이며, 실수로 메모리 해제를 잊게 되면 이는 메모리 낭비로 이어짐. (메모리 릭) 너무 일찍 해제하면 필요한 값이 사라져서 변수가 능력을 상실하게 되됨. 같은 메모리를 두 번 해제하면 프로그램 버그로 이어질 수 있음. 오직 한 번의 메모리 할당(allocate)에 정확히 한 번의 해제(free)가 수행되어야 함.
러스트의 접근 방식: 변수가 유효 범위를 벗어나면 메모리는 자동으로 반환(해제)됨. 러스트는 이 동작을 위해 drop 이라는 특별한 함수를 호출함. 하지만 한 변수가 범위를 벗어났다고 해서 무조건 그 메모리를 해제하면 곤란함. 프로그램의 런타임에는 다양한 상황이 고려되어야 하기에, 러스트는 각각의 상황에 맞는 대응을 함.
데이터 이동
let x = 5;
let y = x;
이 경우, 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 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)
}
'Rust' 카테고리의 다른 글
오너십(슬라이스) (0) | 2018.02.17 |
---|---|
오너십(레퍼런스와 대여) (0) | 2018.02.15 |
컨트롤 플로우(루프) (0) | 2018.02.15 |
컨트롤 플로우(if 조건문) (0) | 2018.02.15 |
함수의 동작 (0) | 2018.02.15 |