티스토리 뷰
모든 프로그램은 실행하는 동안 컴퓨터의 메모리를 사용하는 방법을 관리해야 한다. 러스트는 제 3의 접근법을 이용한다.
-> 메모리는 컴파일 타임에 컴파일러가 체크할 규칙들로 구성된 소유권 시스템을 통해 관리된다. 소유권 기능의 어떤 것도 런타임 비용이 발생하지 않는다.
스택과 힙
러스트와 같은 시스템 프로그래밍 언어에서는, 값이 스택에 있는지 힙에 있는지의 여부가 언어의 동작 방식과 우리의 결단에 더 큰 영향을 준다. 스택과 힙 둘 다 코드상에서 런타임에 사용할 수 있는 메모리의 부분이지만, 이들은 각기 다른 방식으로 구조화 되어 있다. 스택은 값을 받아들인 순서대로 값을 저장하고 반대 방향으로 값을 지운다. Last In First Out(LIFO)라고도 부른다. 데이터를 추가하는 것을 pushing on the stack이라고 하고, 데이터를 제거하는 것을 popping off the stack이라고 부른다.
스택은 데이터에 접근하는 방식 때문에 빠르다. 왜냐하면 스택은 새로운 데이터를 넣어두기 위한 공간 혹은 데이터를 가져올 공간을 검색할 필요가 전혀 없이 스택의 꼭대기(top)에만 접근하면 되기 때문이다. 스택을 빠르게 해주는 또 다른 특성은 스택에 담긴 모든 데이터가 결정되어 있는 고정된 크기를 갖고 있어야 한다는 점이다.
컴파일 타임에 크기가 결정되어 있지 않거나 크기가 변경될 수 있는 데이터를 위해서는 힙에 데이터를 저장할 수 있다. 데이터를 힙에 넣을 때 먼저 저장할 공간이 있는지 물어본다. 그러면 운영체제(OS)가 충분히 커다란 힙 안의 빈 어떤 지점을 찾아서 이 곳을 사용중이라고 표시하고, 해당 지점의 포인터를 사용자에게 돌려준다. 이 절차를 'allocating on the heap'이라고 부른다. 스택에 포인터를 푸시하는 것은 할당에 해당하지 않는다. 포인터는 결정되어 있는 고정된 크기의 값이므로, 스택에 포인터를 저장할 수 있다. 하지만 실제 데이터를 사용하고자 할 때는 포인터를 따라가야 한다.
힙에 저장된 데이터에 접근하는 것은 스택에 저장된 데이터에 접근하는 것보다 느리다. 왜냐하면 포인터가 가리킨 곳을 따라가야 하기 때문이다. 프로세서 멀리 떨어진 데이터(like 힙)보다는 붙어있는 데이터(like 스택)에 대한 작업을 하면 더 빠르다. 힙으로부터 큰 공간을 할당받는 것 또한 시간이 걸릴 수 있다.
코드의 어느 부분이 힙의 어떤 데이터를 사용하는지 추적하는 것, 힙의 중복된 데이터의 양을 최소화하는 것, 그리고 힙 내에 사용하지 않는 데이터를 제거하여 공간이 모자라지 않게 하는 것은 모두 소유권과 관련된 문제이다.
소유권 규칙
1. 러스트의 각각의 값은 해당 값을 오너(owner)라고 불리는 변수를 가지고 있다.
2. 한 번에 딱 하나의 오너만 존재할 수 있다.
3. 오너가 스코프 밖으로 벗어날 때, 값은 버려진다(dropped).
변수의 스코프
let s = "hello";
변수 s는 스트링 리터럴을 나타내는데, 스트링 리터럴의 값은 프로그램의 텍스트 내에 하드코딩되어 있다. 변수는 선언된 시점부터 현재의 스코프가 끝날 때까지 유효하다.
String 타입
이전에 본 모든 데이터 타입은 스택에 저장되었다가 스코프를 벗어날 때 스택으로부터 pop된다. 하지만 이제 힙에 저장되는 데이터를 러스트가 어떻게 비워내는지 알아야 한다.
앞에서 스트링 리터럴을 이미 봤는데, 이 값은 프로그램 안에 하드코딩 되어 있다. 문자열 값을 편리하지만, 텍스트를 필요로 하는 모든 경우에 대해 항상 적절하지는 않다. 그 중 한가지 이유로, 문자열 값은 immutable하다. 그리고 모든 문자열을 프로그래밍하는 시점에서 다 알 수 없다는 것이다.
이러한 경우를 위해 러스트는 두 번째 문자열 타입인 String을 제공한다. 이 타입은 힙에 할당되고 컴파일 타임에 알 수 없는 양의 텍스트를 저장할 수 있게 한다.
let mut s = String::from("hello"); // 문자열 s는 변경할 수 있음
s.push_str(", world!");
println!("{}", s); // hello, world! 를 출력함
String은 변할 수 있는데 스트링 리터럴은 안 되는 이유는 두 타입이 메모리를 쓰는 방식에 있다.
메모리와 할당
String 타입은 변경 가능하고 커질 수 있는 텍스트를 위해 만들어졌다. 이를 위해서는 힙에서 컴파일 타임에는 알 수 없는 어느정도 크기의 메모리 공간을 할당받아 내용물을 저장할 필요가 있다. 이 말은 아래의 두 줄을 의미한다.
1) 런타임에 운영체제로부터 메모리가 요청되어야 한다.
2) String의 사용이 끝났을 때 운영체제에게 메모리를 반납할 방법이 필요하다.
1번은 사용자가 직접 수행한다. String::from을 호출하면, 구현 부분에서 필요한 만큼의 메모리를 요청한다.
2번은 가비지 콜렉터(GC)가 없는 경우, 할당받은 메모리가 더 이상 필요없는 시점을 알아서 명시적으로 반납하는 코드를 호출하는 것이 프로그래머의 책임이다. 만약 반납을 잊는다면 메모리를 낭비하게 되고, 너무 빨리 반납하면 유효하지 않는 변수를 가지게 된다. 반납을 두 번 한다면 버그가 발생한다. 따라서 딱 한 번의 allocate와 free쌍을 사용해야 한다.
하지만 러스트에서 메모리는 변수가 소속되어 있는 스코프 밖으로 벗어나는 순간 drop 함수를 호출하며 자동으로 반납된다.
변수와 데이터가 상호작용하는 방법: 이동(move)
let s1 = String::from("hello");
let s2 = s1;
위의 코드는 단순히 s1 변수에 저장된 "hello" 문자열을 변수 s2에 저장하는 것처럼 보이나, 사실 그렇지 않다.
String은 그림의 왼쪽처럼 세 개의 부분으로 이루어져 있다. 문자열의 내용물을 담고 있는 메모리의 포인터, 길이, 용량이다. 왼쪽 데이터 그룹은 스택에 저장된다. 그리고 내용물을 담은 오른쪽 부분은 힙 메모리에 위치한다.
s2에 s1을 대입하면 String 데이터가 복사되는데, 이는 스택에 있는 포인터, 길이값, 그리고 용량값이 복사된다는 의미이다. 포인터가 가리키고 있는 힙 메모리 상의 데이터는 복사되지 않는다. 만약 러스트가 힙 메모리 상의 데이터까지 복사하게 되면 런타임 상에서 매우 느려질 가능성이 있다.
앞서 변수가 스코프 밖으로 벗어날 때 러스트는 자동적으로 drop 함수를 통해 힙 메모리를 제거한다고 했다. 하지만 위 그림처럼 두 데이터 포인터가 같은 곳을 가리키고 있다면 문제가 발생한다. s2와 s1이 스코프 밖으로 벗어나게 되면, 둘 다 같은 메모리를 해제하려 할 것이다. 이는 double free 오류로 메모리 안정성 버그 중 하나이다. double free는 메모리 손상(corruption)의 원인이 되며 보안 취약성 문제를 일으킬 가능성이 있다.
따라서 메모리 안정성 보장을 위해 러스트는 할당된 메모리를 복사하는 것 대신 s1이 더 이상 유효하지 않다고 간주해 s1이 스코프 밖으로 벗어났을 때 아무것도 해제할 필요가 없도록 만든다.
위 그림은 s1이 무효화된 후의 메모리 구조이다. 오직 s2만 유효한 상황에서 스코프 밖으로 벗어나면 혼자 메모리를 해제할 것이고 에러가 발생하지 않을 것이다.
변수와 데이터가 상호작용하는 방법: 클론
만일 String의 스택 데이터만이 아니라 힙 데이터를 깊이 복사하고 싶다면, clone이라 불리는 공용 메소드를 사용할 수 있다.
let s1 = String::from("hello");
let s2 = s1.clone();
println!("s1 = {}, s2 = {}", s1, s2);
clone을 호출하는 부분을 보면 어떤 비용이 많이 들어갈지도 모르는 코드가 실행 중이라는 것을 알 수 있다.
스택에만 있는 데이터: 복사
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
위의 코드는 clone을 호출하지 않았지만, x도 유효하며 y로 이동하지 않는다. 그 이유는 정수형과 같이 컴파일 타임에 결정되어 있는 크기의 타입은 모두 스택에 저장되기 때문에 실제 값의 복사본이 빠르게 만들어질 수 있다. 그러므로 y가 생성된 후에 x가 더 이상 유효하지 않도록 해야 할 이유가 없다.
소유권과 함수
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가 스코프 밖으로 벗어났습니다. 별다른 일은 발생하지 않습니다.
만약 s를 takes_ownership 함수 호출 이후에 사용하려 하면, 컴파일 타임 오류가 발생한다. 이러한 정적 확인은 여러 실수를 방지해준다.
반환 값과 스코프
fn main() {
let s1 = gives_ownership(); // gives_ownership은 반환값을 s1에게
// 이동시킵니다.
let s2 = String::from("hello"); // s2가 스코프 안에 들어왔습니다.
let s3 = takes_and_gives_back(s2); // s2는 takes_and_gives_back 안으로
// 이동되었고, 이 함수가 반환값을 s3으로도
// 이동시켰습니다.
} // 여기서 s3는 스코프 밖으로 벗어났으며 drop이 호출됩니다. s2는 스코프 밖으로
// 벗어났지만 이동되었으므로 아무 일도 일어나지 않습니다. s1은 스코프 밖으로
// 벗어나서 drop이 호출됩니다.
fn gives_ownership() -> String { // gives_ownership 함수가 반환 값을
// 호출한 쪽으로 이동시킵니다.
let some_string = String::from("hello"); // some_string이 스코프 안에 들어왔습니다.
some_string // some_string이 반환되고, 호출한 쪽의
// 함수로 이동됩니다.
}
// takes_and_gives_back 함수는 String을 하나 받아서 다른 하나를 반환합니다.
fn takes_and_gives_back(a_string: String) -> String { // a_string이 스코프
// 안으로 들어왔습니다.
a_string // a_string은 반환되고, 호출한 쪽의 함수로 이동됩니다.
}
값의 반환 또한 소유권을 이동시킨다.
Rust Reference
1) https://doc.rust-lang.org/reference/introduction.html
Introduction - The Rust Reference
This book is the primary reference for the Rust programming language. It provides three kinds of material: Chapters that informally describe each language construct and their use. Chapters that informally describe the memory model, concurrency model, runti
doc.rust-lang.org
2) https://rinthel.github.io/rust-lang-book-ko/foreword.html
들어가기에 앞서 - The Rust Programming Language
항상 그렇게 명확지는 않았지만, 러스트 프로그래밍 언어는 근본적으로 권한 분산에 관한 것입니다: 여러분이 어떠한 종류의 코드를 작성하는 중이던 간에, 러스트는 여러분에게 더 멀리 뻗어
rinthel.github.io
'개발 > Rust' 카테고리의 다른 글
2. 참조자(References)와 빌림(Borrowing) (0) | 2023.05.03 |
---|
- Total
- Today
- Yesterday
- 딕셔너리
- 파이썬
- 2805
- dp
- heapq
- 큐
- 수학
- 덱
- 자료구조
- 조합
- 1182
- 러스트
- 백준
- 17478
- 10971
- 10845
- 백트래킹
- 1759
- 빌림
- 10815
- 10816
- 브루트포스
- 싸피
- 1764
- 11051
- 1715
- 프로그래머스
- 스택
- 삼성청년소프트웨어아카데미
- 1358
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |