1.4.0으로 업데이트

sarojaba authored
revision 59d0b93d7309cba28f8e4928fe77fc3a4e5a4302
the-stack-and-the-heap
# 4.1. 스택과 힙 (The Stack and the Heap) - 100%

시스템 언어로서, Rust는 낮은 레벨에서 동작합니다. 여러분이 높은 레벨의 언어에서 왔다면, 시스템 프로그래밍의 어떤 면들은 익숙하지 않을 수 있습니다. 중요한 것은 스택과 힙과 함께 메모리가 어떻게 동작하는지 이해하는 것입니다. 여러분이 C 계열의 언어로 스택 할당을 이용하는 방법에 익숙하다면, 이 단원은 복습이 될 것입니다. 그렇지 않다면, 이에 대한 더 일반적인 개념을 배울 것이지만 Rust에 초점이 맞춰질 것입니다.

## 메모리 관리 (Memory management)

이 두가지 용어는 모두 메모리 관리에 관한 것입니다. 스택과 힙은 우리가 메모리 할당, 해제를 알 수 있도록(이해할 수 있도록) 도와주는 추상화 개념입니다.

두 용어의 비교:

스택은 매우 빠르고 Rust 에서 기본적으로 메모리가 할당되는 곳입니다. 하지만 할당 장소(접근 범위)가 함수 호출 범위 내로 한정되고, 크기 또한 한정되어 있습니다. 힙은 느리고, 우리가 작성한 프로그램(코드)에 의해 명시적으로 할당됩니다. 하지만 힙은 사이즈가 제한이 없고, 전역적(globally)으로 접근할 수 있습니다.

# 스택 (The Stack)

한번 이 Rust 프로그램에 대하여 대화하여 봅시다.

```rust
fn main() {
let x = 42;
}
```

이 프로그램은 `x`라는 단 하나의 변수 바인딩을 가지고있습니다. 이것은 할당되기 위해 어딘가에 메모리 공간이 필요합니다. Rust 는 Stack 할당을 기본으로 한다고 하였습니다. 즉 기본적인 값은 스택으로 이동됩니다. 이것이 무엇을 의미할까요?

음, 함수가 호출되는 경우, 메모리는 이것의 로컬(지역) 변수들과 다른 정보들을 위해 할당됩니다. 이것을 '스택 틀(Stack Frame)' 이라고 부릅니다. 그리고 이 강좌의 목적입니다. 우리는 나머지(extra) 정보들을 무시하고 그저 우리가 할당한 지역 변수만 고려하도록 하겠습니다. 이 케이스에서 `main()` 이 실행될 때 (호출될 때), 우리는 '스택 프레임'에 32비트 정수 하나를 할당합니다. 이것들은 자동으로 우릴 위해 처리됩니다. 위 코드에서 볼 수 있듯이, 우리는 스택에 할당을 하기위해 특별한 Rust 코드 또는 다른 어느 특별한 것도 해주지 않아도 됩니다.

함수 호출이 끝날때, '스택 프레임'은 할당 해제가 됩니다. 이 작업 또한 자동으로 이루어집니다. 즉 스택의 할당 해제 또한 특별히 아무것도 해주지 않아도 됩니다.

그것이 이 간단한 프로그램의 전부입니다. 여기서 중요한 것은 스택 할당이 매우, 매우 빠르단 것 입니다. 우리는 모든 지역변수를 사전에 알고있기 때문에, 메모리를 할당할 때 한번에 할당하는 것이 가능합니다. 그리고 이후에 우리는 동시에 할당과 마찬가지로 제거 또한 매우 빠르게 할 수 있습니다.
단점으론 호출된 함수가 끝나게 되면 값을 유지할 수 없다는 것 입니다. 우리는 또한 ‘Stack’ 이란 이름용어에 대하여 설명하지 않았습니다. 이를 위해, 우리는 좀더 복잡한 예제를 살펴볼 필요가 있습니다:

```rust
fn foo() {
let y = 5;
let z = 100;
}

fn main() {
let x = 42;

foo();
}
```

이 프로그램은 총 3개의 변수를 가지고 있습니다: 두 개는 `foo()` 안에 있고, 나머지 하나는 `main()` 안에 있습니다. 이전과 마찬가지로, `main()`이 호출되면, 한 개의 정수가 스택 메모리에 할당됩니다. 하지만 `foo()`가 실행되었을 때를 살펴보기 전에, 메모리에서 무엇이 일어나고 있는지 그림으로 설명해보겠습니다.

OS가 프로그램에 제공하는 메모리의 모습은 단순합니다: 0부터 시작해서 큰 숫자까지 이어지는 거대한 주소들의 리스트입니다. 이 리스트는 컴퓨터가 가지고 있는 RAM이 얼마나 되는지를 나타냅니다. 예를 들어, 1기가 바이트의 RAM을 가지고 있다면, 이 주소들의 목록은 `0`부터 `1,073,741,8243`까지가 됩니다. 이 숫자는 230을 의미합니다.

이 메모리는 거대한 배열과 비슷합니다: 주소들은 0부터 시작해서, 마지막 숫자까지 올라갑니다.
우리의 첫 번째 스택 프레임을 다이어그램으로 만들어보았습니다.

| 주소 | 이름 | 값 |
|---------|------|-------|
| 0 | x | 42 |

우리는 `42`의 값을가지고 `0` 번째 주소에 위치한 `x`를 얻었습니다.

`foo()`가 호출될때, 새로운 스택 프레임이 할당 되었습니다:

| Address | Name | Value |
|---------|------|-------|
| 2 | z | 100 |
| 1 | y | 5 |
| 0 | x | 42 |

1번째 그리고 2번째 주소를 foo() 함수가 사용하였습니다, 왜냐하면 0번째 주소는 첫번째 프레임(`main()`)이 이미 사용하고 있기 때문입니다. 그것은 우리가 함수들을 호출하면 할수록 점점 자라나게 됩니다.

여기서 우리가 노트에 적어야 할 중요한 사실들이 있습니다. 위에서 주소로 사용한 0, 1 그리고 2는 예시설명을 목적으로 하기 때문에 실제 컴퓨터가 사용하는 단위와는 상관이 없습니다. 특히, 주소 세트는 실제로 몇 바이트단위로 분리하고, 이 분리된 크기가 저장된 값의 크기보다 클 수도 있습니다.

`foo()`가 종료되면, 프레임의 할당이 해제됩니다:

| Address | Name | Value |
|---------|------|-------|
| 0 | x | 42 |

그리고 `main()` 까지 종료되면 마지막남은 값(변수 `x`) 까지 사라지게 됩니다. 쉽지요!

이것을 ‘Stack’ 이라고 합니다 왜냐하면 이것은 마치 음식을 담는 접시처럼 쌓이는 것과 같이 동작하기 때문입니다.(당신이 내려놓은 첫번째 접시는 다시 접시를 가져갈 때 제일 마지막에 가져갈 수 있습니다.)
스택은 때때로 Last in, First Out(LIFO) Queues라고 불리는데 그 이유는 마지막에 들어간 값이 제일 처음으로 나오기 때문입니다.

자 이제 좀더 어려운 예제를 시도해보도록 합시다:

```rust
fn bar() {
let i = 6;
}

fn foo() {
let a = 5;
let b = 100;
let c = 1;

bar();
}

fn main() {
let x = 42;

foo();
}
```

이번에도 우리는 첫번째로 `main()` 함수를 호출합니다:

| Address | Name | Value |
|---------|------|-------|
| 0 | x | 42 |

다음으론, `main()` 에서` foo()`를 호출합니다:

| Address | Name | Value |
|---------|------|-------|
| 3 | c | 1 |
| 2 | b | 100 |
| 1 | a | 5 |
| 0 | x | 42 |

그 다음엔, `foo()` 에서 `bar()` 를 호출합니다:

| Address | Name | Value |
|---------|------|-------|
| 4 | i | 6 |
| 3 | c | 1 |
| 2 | b | 100 |
| 1 | a | 5 |
| 0 | x | 42 |

휴~ 우리의 스택이 무럭무럭 자라나고 있습니다!

`bar()` 이 종료된후, `foo()` 와 `main()` 을 남겨두고 `foo()` 의 프레임만 할당 해제가 됩니다:

| Address | Name | Value |
|---------|------|-------|
| 3 | c | 1 |
| 2 | b | 100 |
| 1 | a | 5 |
| 0 | x | 42 |

그리고 `foo()` 가 종료 되면, 오직 `main()` 만이 남게 됩니다.

| Address | Name | Value |
|---------|------|-------|
| 0 | x | 42 |

자 이제 끝났습니다. 이것의 요령이 보이시는가요? 이건 마치 제일위에 접시를 추가하고, 다시 제일 위의 접시를 가져오는 것과 같습니다.

# 힙(The Heap)

자 이제, 이것은 정말 잘 작동합니다, 하지만 모든 것이 이것처럼 작동하는 것은 아닙니다. 때때로, 우리는 다른 함수간에 메모리를 주고받아야 하거나 함수의 실행후에도 값을 유지하고 싶을때도 있습니다. 우리는 힙을 사용하여서 이것을 해결할수 있습니다.

러스트에선, 당신은 [`Box` type][box] 을 통해 힙에 메모리를 할당할수 있습니다.
아래는 예시입니다:

```rust
fn main() {
let x = Box::new(5);
let y = 42;
}
```

[box]: ../std/boxed/index.html

아래의 다이어그램은 `main()` 이 호출된후에 메모리에서 일어난 일을 나타냅니다.

| Address | Name | Value |
|---------|------|--------|
| 1 | y | 42 |
| 0 | x | ?????? |

우리는 스택에 2개의 변수를 할당했습니다. `y`는 명시된 값 `42`로. 그런데, `x`는 어떨까요? 음, `x`는 `Box` 타입이고, box는 힙에 할당된다고 했습니다. 사실, box의 값은 힙에 대한 포인터를 가지는 구조로 되어있습니다. 함수를 실행하기 시작했을 때, `Box::new()`가 실행되어 힙 주소 어딘가에 메모리를 할당하고 `5`를 넣습니다. 메모리는 다음과 같습니다:

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 5 |
| ... | ... | ... |
| 1 | y | 42 |
| 0 | x | 230 |

230는 1GB의 RAM을 가지는 컴퓨터라고 가정했기 때문입니다. 스택이 0부터 시작하기 때문에, 메모리를 가장 간단히 할당하는 방법은 반대편 끝에서부터 시작하는 것입니다. 그래서 우리의 첫번째 값이 메모리의 가장 높은 곳에 위치하고 있습니다.

요약하자면 [raw pointer][rawpointer]를 가지는 `x` 구조체의 값은 우리가 힙에 할당한 230가 되고, 이 값을 메모리 주소로 요청하여 실제 값을 받습니다.

[rawpointer]: raw-pointers.html

우리는 아직 진정한 의미에서의 할당(allocate)과 해제(deallocate)를 논하지 않았습니다. 이 주제를 심도있게 논하는 것은 튜토리얼의 범위를 벗어나지만, 여기에서 가장 강조하는 것은 힙은 단지 스택의 반대편에서부터 할당되는 것이 아니라는 겁니다.

차후 예제에서 다루겠지만 힙은 순서에 상관없이 해제될 수 있기 때문에 '구멍'이 생길 수 있게 됩니다. 우리가 실행하는 프로그램의 메모리 다이어그램을 살펴보면 이렇습니다:

| Address | Name | Value |
|----------------------|------|----------------------|
| 230 | | 5 |
| (230) - 1 | | |
| (230) - 2 | | |
| (230) - 3 | | 42 |
| ... | ... | ... |
| 3 | y | (230) - 3 |
| 2 | y | 42 |
| 1 | y | 42 |
| 0 | x | 230 |

여기서 우리는 4개의 메모리를 힙에 할당했지만, 2개의 메모리를 해제했습니다. 따라서 230 과 (230) - 3 사이에 사용되지 않는 틈이 존재하게 됩니다. 이러한 일이 발생하는 것은 당신이 힙을 어떻게 관리하고 사용하는지에 달려있습니다.

여러 프로그램은 각자 다른 '메모리 할당자'(이러한 일들을 대신 관리해주는 라이브러리)를 사용할 수 있습니다. 러스트 프로그램은 [jemalloc][jemalloc]을 사용합니다.

[jemalloc]: http://www.canonware.com/jemalloc/

어쨌든, 예제로 돌아와보죠. 메모리가 힙 위에 올라갔기 때문에 Box를 할당시켰던 함수보다 오래 존재할 수 있습니다. 이번 경우에는 좀 다르긴 하지만요. 함수가 완료되면, 우리는 `main()`의 스택 프레임을 해제시켜야만 해야하죠. 그렇지만 `Box`에는 한 가지 방법이 있습니다: [Drop][drop]. `Drop`을 사용하면, `x`가 사라질 때, 힙에 할당되어있던 메모리를 먼저 해제합니다. 좋군요!

| Address | Name | Value |
|---------|------|--------|
| 1 | y | 42 |
| 0 | x | ?????? |

[drop]: drop.html
[^moving]: 소유권을 전달함으로써 메모리를 보다 오래 살아있게 만들 수 있습니다.
때때로‘상자 옮기기(moving out of the box)’라고 불립니다. 복잡한 예시들이 차후에 이것을 다룰 것입니다.

그리고 나서 스택 프레임이 사라지고, 모든 메모리가 최종적으로 해제됩니다.

# 매개변수와 빌림 (Arguments and borrowing)

지금까지 스택과 힙에 대한 간단한 예제를 살펴보았지만, 함수 인자와 빌림에 대해서는 어떨까요?
여기 작은 러스트 프로그램이 있습니다:

```rust
fn foo(i: &i32) {
let z = 42;
}

fn main() {
let x = 5;
let y = &x;

foo(y);
}
```

우리가 `main()`에 진입할 때, 메모리는 다음과 같습니다:

| Address | Name | Value |
|---------|------|-------|
| 1 | y | 0 |
| 0 | x | 5 |

`x`는 평범하게 `5`로, `y`는 `x`를 참조했습니다. 그래서 그 값은 `x`의 메모리 위치인, `0`이 됩니다.

우리가 `y`를 인자로 넘겨주며 `foo()`를 호출할 때는 어떨까요?

| Address | Name | Value |
|---------|------|-------|
| 3 | z | 42 |
| 2 | i | 0 |
| 1 | y | 0 |
| 0 | x | 5 |

스택 프레임은 지역변수뿐만 아니라, 인자에게도 마찬가지로 적용됩니다. 따라서 이 경우에 우리는 인자인 `i`와 지역변수 `z` 모두 할당해야 합니다. `i`는 인자로써 `y`의 복사본입니다. `y`의 값은 `0`이니, `i` 또한 같습니다.

이것이 어떤 값을 빌리는 게 어떤 메모리도 해제하지 않는 이유 중 하나가 됩니다: 참조 값은 단지 메모리 위치에 대한 포인터입니다. 만약 내부 메모리를 제거한다면, 잘 동작하지 않을 것입니다.

# 복잡한 예시 (A complex example)

그럼, 복잡한 프로그램으로 차근차근 해나가보죠.

```rust
fn foo(x: &i32) {
let y = 10;
let z = &y;

baz(z);
bar(x, z);
}

fn bar(a: &i32, b: &i32) {
let c = 5;
let d = Box::new(5);
let e = &d;

baz(e);
}

fn baz(f: &i32) {
let g = 100;
}

fn main() {
let h = 3;
let i = Box::new(20);
let j = &h;

foo(j);
}
```

우선, `main()`을 호출합니다:

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

`j`, `i`, `h`에 메모리를 할당하고, `i`는 힙에 할당하고 그에 따른 포인터 값을 가지게 되었습니다.

다음으로, `main()`의 끝인 `foo()`를 호출합니다:

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

`x`, `y`, `z`를 할당합니다. 인자 `x`는 호출할 때 넘겨준 `j`와 같은 값을 갖습니다. 포인터 값 `0` 주소로, `j`도 `h`를 가리키게 되었습니다.

다음으로, `foo()`는 `z`를 넘기면서 `baz()`를 호출합니다:

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 7 | g | 100 |
| 6 | f | 4 |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

`f`와 `g`를 할당합니다. `baz()`는 순식간에 작업이 끝나면 스택 프레임에서 제거됩니다:

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

다음으로, `foo()`는 `x`와 `z`를 인자로 `bar()`를 호출합니다.

| Address | Name | Value |
|----------------------|------|----------------------|
| 230 | | 20 |
| (230) - 1 | | 5 |
| ... | ... | ... |
| 10 | e | 9 |
| 9 | d | (230) - 1 |
| 8 | c | 5 |
| 7 | b | 4 |
| 6 | a | 0 |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

힙의 맨 끝에서 또 하나의 값을 할당해야하므로, 230에서 1을 뺀 값의 주소가 됩니다. `1,073,741,823`라고 쓰는 것보단 쉽죠.

`bar()`의 끝에, `baz()`를 호출합니다:

| Address | Name | Value |
|----------------------|------|----------------------|
| 230 | | 20 |
| (230) - 1 | | 5 |
| ... | ... | ... |
| 12 | g | 100 |
| 11 | f | 9 |
| 10 | e | 9 |
| 9 | d | (230) - 1 |
| 8 | c | 5 |
| 7 | b | 4 |
| 6 | a | 0 |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

이것으로, 가장 깊은 지점에 도달했습니다! 여기까지 따라오신 것만으로 대단합니다!

`baz()`가 완료되면, `f` 와 `g`를 제거합니다:

| Address | Name | Value |
|----------------------|------|----------------------|
| 230 | | 20 |
| (230) - 1 | | 5 |
| ... | ... | ... |
| 10 | e | 9 |
| 9 | d | (230) - 1 |
| 8 | c | 5 |
| 7 | b | 4 |
| 6 | a | 0 |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

다음으로, `bar()`함수를 반환합니다. 이 경우 `Box`의 `d`는 힙에 있기 때문에, 따라서 (230) - 1에 할당된 부분도 같이 해제됩니다.

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 5 | z | 4 |
| 4 | y | 10 |
| 3 | x | 0 |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

후에 `foo()`함수도 반환됩니다.

| Address | Name | Value |
|-----------------|------|----------------|
| 230 | | 20 |
| ... | ... | ... |
| 2 | j | 0 |
| 1 | i | 230 |
| 0 | h | 3 |

그리고 마침내, 다른 함수들이 모두 해제된 `main()`만이 남습니다. `i`가 `Drop`된다면, 힙의 나머지 부분도 정리될 것입니다.

# 다른 언어에서는 무엇을 할까요?(What do other languages do?)

쓰레기 수집기(GC)를 가진 대부분의 언어들은 힙 할당을 기본으로 하고 있습니다. 이것은 러스트로 치자면 모든 값들이 `Box`할당되어 있다는 것을 의미합니다. 그런 결정을 한 이유는 여러가지가 있지만, 이 글의 범위에서 벗어납니다.
그러한 언어들은 쓰레기 수집기가 해제할 메모리를 관리해주기 때문에 항상 최적화될 여지가 남아있다는 단점이 있습니다. 어쨌든 다른 언어들은 스택 할당과 `Drop`을 통한 메모리 관리보다는, 쓰레기 수집기와 힙을 통해 관리를 하는 편입니다.

# 어떨때 사용해야 할까요? (Which to use?)

스택이 빠른데다가 관리하기 편하기까지 하다면, 힙은 왜 필요할까요? 가장 큰 이유는 스택 할당은 저장소 조작에 있어서 오직 LIFO(Last In First Out) 구조를 따라야 하기 떄문입니다. 힙 할당은 비교적 좀 더 일반적으로 복잡도 비용이 드는 대신 저장소에 있어서 임의적 접근이 가능합니다.

아마도 (시스템 프로그래밍에서) 스택 할당을 선택해야만 할 것입니다. 러스트 또한 스택 할당이 기본적입니다. 스택의 LIFO 모델은 근본적으로 좀 더 단순하며 예측이 쉽습니다. 그렇기 때문에 두 가지 시사점이 있습니다:런타임 효율성, 문법적 영향

## 런타임 효율성 (Runtime Efficiency)

스택에서의 메모리 관리는 간단한 일입니다: 그저 값을 쌓아서 올라가거나 내려오며, 그렇기 때문에 "스택 포인터"라고 불립니다.
힙에서의 메모리 관리는 간단하지 않습니다: 힙에 할당된 메모리는 임의의 순간에 해제될 수 있으며, 힙에 할당된 메모리 조각들은 어떤 크기도 될 수 있고, 메모리를 관리하기 위해서는 좀 더 신경써야만 합니다.

이 주제에 대해 더 자세히 알아보고 싶다면, [this paper][wilson]가 좋은 시작이 될 것입니다.


[wilson]: http://www.cs.northwcitesteern.edu/~pdinda/icsclass/doc/dsa.pdfx.ist.psu.edu/viewdoc/summary?doi=10.1.1.143.4688

## 문법적 영향 (Semantic impact)

스택 할당은 러스트 언어 그 자체 뿐만 아니라, 개발자들의 마인드 또한 바꾸어놓았습니다. LIFO 방식은 러스트 언어가 자동으로 메모리를 관리할 수 있게 해주었습니다. 여기서 자세히 논하진 않겠지만, 심지어 몇몇 특별하게 할당된 힙의 해제까지도 스택 기반 LIFO 방식으로 이루어질 수 있습니다. non-LIFO 방식은 유연하다지만, 컴파일러가 메모리가 해제될 지점을 제대로 유추하지 못하도록 하는 결과를 낳습니다. 결국 안정적인 해제를 달성하기 위해선 언어 그 자체를 넘어 바깥에 있는 규약들에 의지해야 할지도 모릅니다.(참조 카운팅인 `Rc`, `Arc`가 예시가 될 수 있습니다.)

그러한 일들이 한계에 다다르면, 힙 할당의 댓가는 상당한 런타임 비용 지불(e.g. 쓰레기 수집기) 또는 프로그래머 자신의 엄청난 노고(러스트 컴파일러가 제공하지 않는 명시적 메모리 관리)가 될 것입니다.