Box\<T\>로 힙에 데이터 올리기

Box<T>로 힙에 데이터 올리기

러스트에서 값은 기본적으로 스택에 쌓여요. 그런데 재귀 타입처럼 크기를 컴파일 타임에 알 수 없는 경우나, 큰 데이터를 스택 대신 힙에 두고 싶을 때가 있어요. 가장 단순한 스마트 포인터인 Box<T>는 이를 해결해 줘요. Box<T>는 스택에는 힙 데이터를 가리키는 포인터만 남기고, 실제 값은 힙에 저장해요.

출처: The Rust Programming Language - Using Box<T> to Point to Data on the Heap

힙에 데이터 저장하기

Box::new()에 값을 넣으면 힙에 값이 올라가요. 접근 방식은 스택에 있을 때와 크게 다르지 않아요.

fn main() {
    let b = Box::new(5);
    println!("b = {b}");
}

이 프로그램은 b = 5를 출력해요. b가 스코프를 벗어나면, 박스(스택)와 그것이 가리키는 힙 데이터가 함께 해제돼요.

재귀 타입을 가능하게 만들기

Box<T>가 특히 유용한 순간은 재귀 타입(recursive type)을 정의할 때예요. 재귀 타입은 자기 자신을 값으로 다시 담는 타입인데, 러스트는 타입의 크기를 컴파일 타임에 알아야 해서 이런 정의가 그대로는 통하지 않아요.

대표적인 예로 cons list가 있어요. 한 요소와 나머지 리스트를 담는 enum으로 정의해 볼게요.

enum List {
    Cons(i32, List),
    Nil,
}

이 정의는 컴파일되지 않아요. Cons가 다시 List를 직접 들고 있으니 크기가 무한히 커져서, 컴파일러가 "has infinite size" 에러를 냅니다. 여기에 Box<T>를 끼워 넣으면 문제가 풀려요. 힙에 대한 포인터 하나의 크기는 항상 정해져 있기 때문이에요.

enum List {
    Cons(i32, Box<List>),
    Nil,
}

use crate::List::{Cons, Nil};

fn main() {
    let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}

이제 Cons variant는 i32 크기와 박스 포인터 크기만 가지면 되고, Nil variant는 값을 담지 않으니 더 작아요. Box<T> 덕분에 러스트가 List 값을 저장하는 데 필요한 크기를 확정할 수 있게 됐어요.

Box<T>Deref 트레이트를 구현해서 참조처럼 취급할 수 있고, 스코프를 벗어나면 Drop 트레이트 구현에 따라 가리키던 힙 데이터까지 정리돼요. 이 두 트레이트는 뒤에서 나올 다른 스마트 포인터들의 동작을 이해하는 기초가 돼요.

더 알아보기