스택과 힙 메모리
스택(stack)과 힙(heap)은 런타임에 메모리를 할당하고 관리하는 데 쓰이는 두 가지 주요 영역이다. Rust는 이 둘을 엄격하게 구분하며, Ownership과 Borrowing 규칙을 통해 어느 시점에 어떤 메모리를 사용하고 정리할지를 컴파일 타임에 결정할 수 있도록 돕는다. Rust의 메모리 모델을 이해하려면 우선 스택과 힙이 어떻게 동작하며 두 영역 사이에서 어떻게 Ownership이 관리되는지 살펴보는 것이 중요하다.
스택은 매우 빠른 할당과 해제가 특징인 메모리 영역이다. 함수가 호출될 때마다 함수와 관련된 데이터(변수, 매개변수, 내부 상태 등)가 스택 프레임(stack frame) 형태로 쌓인다. 함수가 반환되면 해당 스택 프레임은 한꺼번에 제거된다. 스택은 마지막에 삽입된 데이터가 가장 먼저 제거되는 LIFO(Last-In, First-Out) 구조로 동작하며, 정적으로 크기가 결정되는(또는 추론 가능한) 데이터들을 저장하기에 적합하다. 예를 들어, i32 같은 기본 정수형이나 f64 같은 부동소수점 수는 스택에 직접 저장된다. 이처럼 간단하고 크기가 고정된 타입은 스택에 위치하기 때문에 할당과 해제가 매우 빠르며, 추가적인 메모리 오버헤드가 거의 없다.
힙은 런타임에 동적으로 크기가 결정되는 데이터를 저장할 때 주로 사용되는 메모리 영역이다. 프로그램이 실행 중에 어떤 시점에서 필요한 만큼 메모리를 요청할 수 있으며, 그 결과로 얻은 힙 주소에 데이터를 배치한다. 문자열이나 가변 길이 자료구조처럼 크기가 유동적인 타입은 대부분 힙을 사용한다. 힙에 데이터를 저장할 때는 운영체제에 메모리를 요청(allocate)하는 과정이 필요하고, 더 이상 필요하지 않을 때 해제(deallocate)해야 한다. Rust는 Ownership 규칙과 스마트 포인터(Box, Rc, Arc 등), 그리고 빌림 규칙을 통해 이 과정을 자동화해 오류 없이 안전하게 동적 메모리를 다룰 수 있도록 보장한다.
스택에 할당된 데이터와 힙에 할당된 데이터는 Ownership 규칙에 따라 다르게 움직인다. 예를 들어, String 타입은 힙을 사용하므로 새로운 스코프로 이동하면 원본 데이터의 이동(move) 또는 복제(clone)가 일어난다. 반면, 정적 크기를 갖는 슬라이스(&str)는 스택에 참조만을 저장하고 실제 데이터는 별도의 메모리에 있을 수 있다. Rust 컴파일러는 이러한 차이를 바탕으로 변수 이동, 복사, 참조(immutable reference, mutable reference), 라이프타임(lifetime) 등을 컴파일 시점에 철저히 검사한다.
스택과 힙의 동작 방식을 명확히 이해하려면 간단한 예시를 보는 것이 도움이 된다.
fn main() {
let x = 5;
let y = x;
println!("x = {}, y = {}", x, y);
let s1 = String::from("Hello");
let s2 = s1;
// println!("{}", s1); // 이 코드는 컴파일 에러가 난다.
println!("{}", s2);
}x는 i32 타입으로 스택에 5라는 값이 저장된다. y에 x를 대입하면 두 값이 스택 레벨에서 복사(copy)되므로 x는 여전히 유효하다. 반면, String은 힙에 데이터를 저장한다. s1을 s2에 대입하면 s1의 힙 데이터가 s2로 이동(move)된다. Rust는 힙에 있는 실제 데이터의 소유권을 s2가 가져갔다고 판단하고, s1은 더 이상 유효하지 않다고 간주한다. 그렇기 때문에 s1을 이후에 사용하려 하면 컴파일 에러가 발생한다.
힙 메모리는 대체로 스택보다 할당과 해제 비용이 높다. Rust에서 Ownership과 Borrowing 규칙은 힙으로부터 불필요하게 메모리를 획득하거나, 이미 해제된 메모리를 다시 사용하려는 시도를 방지한다. 스택에서는 모든 할당과 해제가 자동으로 발생하고, 스코프를 벗어날 때 바로 제거된다. 힙에서는 Box나 벡터(Vec), 문자열(String) 같은 여러 구조체들이 내부적으로 참조를 소유하고 있어, 스코프가 끝날 때 러스트가 자동으로 drop 함수를 호출하여 해제를 수행한다. 이러한 메커니즘으로 Rust는 가비지 컬렉션 없이도 메모리를 안전하고 자동으로 관리한다.
스택과 힙을 언제 어떻게 사용할지는 자료 구조의 특징과 런타임 요구사항에 따라 달라진다. 코드 작성 시, 크기가 고정된 타입이나 스택에 올려둘 수 있을 정도로 작은 자료는 스택에 두고, 큰 자료나 동적으로 크기가 결정되는 자료를 힙에 두는 식으로 나뉜다. Rust는 최대한 많은 경우에 스택 상의 할당을 활용하도록 돕지만, 필요하다면 당연히 힙 할당도 가능하다. Ownership 체계와 빌림 규칙이 뒷받침되므로, 스택과 힙 어디에 데이터를 두어도 안전성은 보장된다.
스택은 빠르고 단순한 반면, 힙은 유연하지만 할당과 해제에 비용이 따른다. Rust는 두 메모리 영역을 구분해 사용할 수 있도록 타입 시스템과 Ownership 규칙을 강화하고, 빌림을 통해 불필요한 복사나 중복 해제를 방지한다. 결국 Rust를 익히는 과정은 스택과 힙의 특성을 이해하고, Ownership과 Borrowing을 적절히 결합해 효율적이면서도 안전한 메모리 접근을 구현하는 방법을 익히는 과정이라고 볼 수 있다.
Last updated