소개
Box
이 랩에서는 컴파일 시간에 타입의 크기를 알 수 없는 경우, 복사를 피하기 위해 대량의 데이터 소유권을 이전하는 경우, 또는 특정 트레이트를 구현하는 값을 소유하는 경우 등, Box
Box
이 랩에서는 컴파일 시간에 타입의 크기를 알 수 없는 경우, 복사를 피하기 위해 대량의 데이터 소유권을 이전하는 경우, 또는 특정 트레이트를 구현하는 값을 소유하는 경우 등, Box
<T>를 사용하여 힙의 데이터를 가리키는 방법가장 간단한 스마트 포인터는 box이며, 타입은 Box<T>로 표기합니다. Box 를 사용하면 스택 대신 힙에 데이터를 저장할 수 있습니다. 스택에 남는 것은 힙 데이터에 대한 포인터입니다. 스택과 힙의 차이점에 대해서는 4 장을 참조하십시오.
Box 는 스택 대신 힙에 데이터를 저장하는 것 외에는 성능 오버헤드가 없습니다. 하지만 특별한 기능도 많지 않습니다. 다음 상황에서 가장 자주 사용하게 됩니다.
"Box 를 사용하여 재귀적 타입 활성화"에서 첫 번째 상황을 시연할 것입니다. 두 번째 경우, 대량의 데이터 소유권을 이전하는 데 시간이 오래 걸릴 수 있는데, 이는 데이터가 스택에서 복사되기 때문입니다. 이 상황에서 성능을 향상시키기 위해, 힙에 있는 box 에 대량의 데이터를 저장할 수 있습니다. 그러면 작은 양의 포인터 데이터만 스택에서 복사되고, 참조하는 데이터는 힙의 한 위치에 유지됩니다. 세 번째 경우는 *트레이트 객체 (trait object)*라고 하며, "다양한 타입의 값을 허용하는 트레이트 객체 사용"에서 해당 주제를 다룹니다. 따라서 여기서 배우는 내용은 해당 섹션에서도 다시 적용할 수 있습니다!
<T>를 사용하여 힙에 데이터 저장하기Box<T>의 힙 저장 사용 사례를 논의하기 전에, 구문과 Box<T> 내에 저장된 값과 상호 작용하는 방법을 살펴보겠습니다.
Listing 15-1 은 box 를 사용하여 힙에 i32 값을 저장하는 방법을 보여줍니다.
Filename: src/main.rs
fn main() {
let b = Box::new(5);
println!("b = {b}");
}
Listing 15-1: box 를 사용하여 힙에 i32 값을 저장하기
변수 b를 힙에 할당된 값 5를 가리키는 Box의 값으로 정의합니다. 이 프로그램은 b = 5를 출력합니다. 이 경우, 이 데이터가 스택에 있는 경우와 유사하게 box 내의 데이터에 접근할 수 있습니다. 모든 소유된 값과 마찬가지로, box 가 main의 끝에서 b가 하는 것처럼 범위를 벗어나면 할당 해제됩니다. 할당 해제는 box (스택에 저장됨) 와 box 가 가리키는 데이터 (힙에 저장됨) 모두에 대해 발생합니다.
단일 값을 힙에 넣는 것은 그다지 유용하지 않으므로, 이러한 방식으로 box 를 단독으로 사용하는 경우는 드뭅니다. 단일 i32와 같은 값을 스택에 두는 것이 대부분의 상황에서 더 적절합니다. 이제 box 가 없었다면 정의할 수 없었을 타입을 box 를 사용하여 정의할 수 있는 경우를 살펴보겠습니다.
*재귀적 타입 (recursive type)*의 값은 자체의 일부로 동일한 타입의 다른 값을 가질 수 있습니다. 재귀적 타입은 컴파일 시간에 Rust 가 타입이 차지하는 공간의 크기를 알아야 하기 때문에 문제를 야기합니다. 그러나 재귀적 타입 값의 중첩은 이론적으로 무한히 계속될 수 있으므로 Rust 는 값에 필요한 공간의 크기를 알 수 없습니다. Box 는 알려진 크기를 가지므로, 재귀적 타입 정의에 box 를 삽입하여 재귀적 타입을 활성화할 수 있습니다.
재귀적 타입의 예로, cons list를 살펴보겠습니다. 이는 함수형 프로그래밍 언어에서 일반적으로 발견되는 데이터 타입입니다. 우리가 정의할 cons list 타입은 재귀를 제외하고는 간단합니다. 따라서 우리가 사용할 예제의 개념은 재귀적 타입을 포함하는 더 복잡한 상황에 직면할 때마다 유용할 것입니다.
Cons list는 Lisp 프로그래밍 언어와 그 방언에서 유래된 데이터 구조로, 중첩된 쌍으로 구성되며, 연결 리스트의 Lisp 버전입니다. 이름은 Lisp 의 cons 함수 ( construct function의 약자) 에서 유래되었으며, 이 함수는 두 인수를 사용하여 새로운 쌍을 구성합니다. 값과 다른 쌍으로 구성된 쌍에 cons를 호출함으로써 재귀적 쌍으로 구성된 cons list 를 구성할 수 있습니다.
예를 들어, 1, 2, 3 목록을 포함하는 cons list 의 의사 코드 표현은 다음과 같습니다 (각 쌍은 괄호 안에 있음):
(1, (2, (3, Nil)))
cons list 의 각 항목은 두 개의 요소를 포함합니다: 현재 항목의 값과 다음 항목입니다. 목록의 마지막 항목은 다음 항목 없이 Nil이라는 값만 포함합니다. cons list 는 cons 함수를 재귀적으로 호출하여 생성됩니다. 재귀의 기본 사례를 나타내는 표준 이름은 Nil입니다. 이는 6 장에서 다룬 "null" 또는 "nil" 개념과는 다르며, 이는 유효하지 않거나 부재하는 값입니다.
Cons list 는 Rust 에서 일반적으로 사용되는 데이터 구조는 아닙니다. Rust 에서 항목 목록이 있는 경우 대부분의 경우 Vec<T>를 사용하는 것이 더 나은 선택입니다. 다른, 더 복잡한 재귀적 데이터 타입은 다양한 상황에서 유용하지만, 이 장에서 cons list 로 시작함으로써 box 를 사용하여 재귀적 데이터 타입을 방해 없이 정의할 수 있는 방법을 탐구할 수 있습니다.
Listing 15-2 는 cons list 에 대한 enum 정의를 포함합니다. 이 코드는 아직 컴파일되지 않습니다. List 타입이 알려진 크기를 갖지 않기 때문이며, 이를 시연할 것입니다.
Filename: src/main.rs
enum List {
Cons(i32, List),
Nil,
}
Listing 15-2: i32 값을 갖는 cons list 데이터 구조를 나타내는 enum 을 정의하려는 첫 번째 시도
참고: 이 예제를 위해
i32값만 저장하는 cons list 를 구현하고 있습니다. 10 장에서 논의한 것처럼 제네릭을 사용하여 모든 타입의 값을 저장할 수 있는 cons list 타입을 정의할 수도 있습니다.
List 타입을 사용하여 목록 1, 2, 3을 저장하는 것은 Listing 15-3 의 코드와 같습니다.
Filename: src/main.rs
--snip--
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
Listing 15-3: List enum 을 사용하여 목록 1, 2, 3을 저장하기
첫 번째 Cons 값은 1과 다른 List 값을 저장합니다. 이 List 값은 2와 다른 List 값을 저장하는 또 다른 Cons 값입니다. 이 List 값은 3과 List 값을 저장하는 또 다른 Cons 값이며, 마지막으로 목록의 끝을 알리는 비재귀적 변형인 Nil입니다.
Listing 15-3 의 코드를 컴파일하려고 하면 Listing 15-4 에 표시된 오류가 발생합니다.
error[E0072]: recursive type `List` has infinite size
--> src/main.rs:1:1
|
1 | enum List {
| ^^^^^^^^^ recursive type has infinite size
2 | Cons(i32, List),
| ---- recursive without indirection
|
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
|
2 | Cons(i32, Box<List>),
| ++++ +
Listing 15-4: 재귀적 enum 을 정의하려고 할 때 발생하는 오류
오류는 이 타입이 "무한한 크기"를 갖는다고 표시합니다. 그 이유는 List를 재귀적인 변형으로 정의했기 때문입니다: 자체의 다른 값을 직접 저장합니다. 결과적으로 Rust 는 List 값을 저장하는 데 필요한 공간의 크기를 파악할 수 없습니다. 이 오류가 발생하는 이유를 자세히 살펴보겠습니다. 먼저 Rust 가 비재귀적 타입의 값을 저장하는 데 필요한 공간의 크기를 어떻게 결정하는지 살펴보겠습니다.
6 장에서 enum 정의에 대해 논의했을 때 Listing 6-2 에서 정의한 Message enum 을 기억하십시오:
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
Message 값에 할당할 공간의 크기를 결정하기 위해 Rust 는 각 변형을 거쳐 어떤 변형이 가장 많은 공간을 필요로 하는지 확인합니다. Rust 는 Message::Quit이 어떤 공간도 필요로 하지 않고, Message::Move는 두 개의 i32 값을 저장할 공간이 필요하며, 기타 등등을 확인합니다. 하나의 변형만 사용되므로, Message 값이 필요로 하는 최대 공간은 가장 큰 변형을 저장하는 데 필요한 공간입니다.
Listing 15-2 의 List enum 과 같은 재귀적 타입이 필요로 하는 공간의 크기를 Rust 가 결정하려고 할 때 발생하는 상황과 대조해 보십시오. 컴파일러는 Cons 변형을 먼저 살펴봅니다. 이 변형은 i32 타입의 값과 List 타입의 값을 저장합니다. 따라서 Cons는 i32의 크기 더하기 List의 크기와 같은 양의 공간이 필요합니다. List 타입이 얼마나 많은 메모리를 필요로 하는지 파악하기 위해 컴파일러는 Cons 변형부터 시작하여 변형을 살펴봅니다. Cons 변형은 i32 타입의 값과 List 타입의 값을 저장하며, 이 과정은 그림 15-1 에 표시된 것처럼 무한히 계속됩니다.
그림 15-1: 무한한 Cons 변형으로 구성된 무한한 List
BoxRust 는 재귀적으로 정의된 타입에 할당할 공간의 크기를 파악할 수 없기 때문에 컴파일러는 다음과 같은 유용한 제안과 함께 오류를 발생시킵니다:
help: insert some indirection (e.g., a `Box`, `Rc`, or `&`) to make `List`
representable
|
2 | Cons(i32, Box<List>),
| ++++ +
이 제안에서 indirection은 값을 직접 저장하는 대신, 해당 값에 대한 포인터를 저장하여 간접적으로 값을 저장하도록 데이터 구조를 변경해야 함을 의미합니다.
Box<T>는 포인터이므로 Rust 는 Box<T>가 얼마나 많은 공간을 필요로 하는지 항상 알고 있습니다: 포인터의 크기는 가리키는 데이터의 양에 따라 변경되지 않습니다. 즉, 다른 List 값을 직접 저장하는 대신 Cons 변형 내부에 Box<T>를 넣을 수 있습니다. Box<T>는 Cons 변형 내부에 있는 대신 힙에 있는 다음 List 값을 가리킬 것입니다. 개념적으로, 우리는 여전히 다른 리스트를 포함하는 리스트로 생성된 리스트를 가지고 있지만, 이 구현은 이제 항목을 서로 안에 넣는 것이 아니라 서로 옆에 배치하는 것과 더 유사합니다.
Listing 15-2 의 List enum 정의와 Listing 15-3 의 List 사용법을 Listing 15-5 의 코드로 변경할 수 있으며, 이 코드는 컴파일됩니다.
Filename: src/main.rs
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)
))
))
);
}
Listing 15-5: 알려진 크기를 갖기 위해 Box<T>를 사용하는 List의 정의
Cons 변형은 i32의 크기 더하기 박스의 포인터 데이터를 저장할 공간이 필요합니다. Nil 변형은 값을 저장하지 않으므로 Cons 변형보다 적은 공간이 필요합니다. 이제 모든 List 값은 i32의 크기 더하기 박스의 포인터 데이터의 크기를 차지한다는 것을 알 수 있습니다. 박스를 사용함으로써 무한한 재귀적 체인을 끊었으므로 컴파일러는 List 값을 저장하는 데 필요한 크기를 파악할 수 있습니다. 그림 15-2 는 Cons 변형이 어떻게 보이는지 보여줍니다.
그림 15-2: Cons가 Box를 포함하기 때문에 무한한 크기가 아닌 List
Box 는 indirection 과 힙 할당만 제공합니다; 다른 스마트 포인터 타입에서 볼 수 있는 다른 특별한 기능은 없습니다. 또한 이러한 특별한 기능으로 인해 발생하는 성능 오버헤드도 없으므로, indirection 이 우리가 필요로 하는 유일한 기능인 cons list 와 같은 경우에 유용할 수 있습니다. 17 장에서 박스에 대한 더 많은 사용 사례를 살펴보겠습니다.
Box<T> 타입은 Deref 트레이트를 구현하므로 스마트 포인터입니다. 이 트레이트를 통해 Box<T> 값을 참조처럼 처리할 수 있습니다. Box<T> 값이 범위를 벗어나면 Drop 트레이트 구현으로 인해 박스가 가리키는 힙 데이터도 정리됩니다. 이 두 트레이트는 이 장의 나머지 부분에서 논의할 다른 스마트 포인터 타입에서 제공되는 기능에 훨씬 더 중요합니다. 이 두 트레이트를 자세히 살펴보겠습니다.
축하합니다! 힙의 데이터를 가리키기 위해 Box