Sized: 컴파일 타임에 크기를 아는 타입
Sized 트레이트는 러스트의 타입 시스템에서 매우 근본적인 역할을 하는 마커 트레이트입니다. 평소에는 잘 드러나지 않지만, 컴파일러가 메모리를 어떻게 다룰지 결정하는 데 핵심적인 정보를 제공합니다.
Sized 트레이트는 **“이 타입의 크기는 컴파일 타임에 알려져 있다”**는 것을 의미합니다. 러스트의 거의 모든 타입은 Sized를 구현합니다.
왜 타입의 크기를 알아야 하는가?
러스트 컴파일러는 함수가 호출될 때 스택(Stack)에 변수를 위한 공간을 할당해야 합니다. 스택에 메모리를 할당하려면, 각 변수가 얼마나 많은 공간을 차지할지 컴파일 시점에 정확히 알아야 합니다. 만약 크기를 모른다면, 얼마나 많은 메모리를 예약해야 할지 알 수 없기 때문입니다.
fn my_function() {
let x: i32 = 5; // i32는 4바이트 크기 (Sized)
let y: bool = true; // bool은 1바이트 크기 (Sized)
// 컴파일러는 my_function 스택 프레임에 총 5바이트 이상이 필요함을 안다.
}Sized가 아닌 타입: ?Sized와 DST
Sized의 반대는 ‘크기를 알 수 없는 타입’이며, 이를 DST(Dynamically Sized Type) 라고 부릅니다. 제네릭 문맥에서 Sized가 아니어도 되는 타입을 나타낼 때는 ?Sized 문법을 사용합니다.
대표적인 DST에는 세 가지가 있습니다.
-
str:String이 소유한 힙 데이터의 실제 내용물입니다."hello"는 5바이트,"안녕하세요"는 15바이트로 길이가 가변적입니다. 우리는str을 직접 사용할 수 없고, 항상 포인터와 길이를 함께 담고 있는 ‘뚱뚱한 포인터(fat pointer)‘인&str을 통해 사용합니다. -
[T]: 슬라이스(slice) 타입입니다.Vec<T>가 소유한 데이터의 연속된 부분을 가리킵니다.[1, 2]와[1, 2, 3]은 다른 크기를 가집니다. 우리는 항상&[T]나&mut [T]를 통해 슬라이스를 다룹니다. -
dyn Trait: 트레이트 객체(trait object)입니다. 특정 트레이트를 구현하는 어떤 타입이든 될 수 있으므로, 컴파일 시점에는 실제 타입의 크기를 알 수 없습니다.dyn Trait역시 데이터 포인터와 가상 테이블(vtable) 포인터를 담은 뚱뚱한 포인터인&dyn Trait나Box<dyn Trait>뒤에 숨겨서 사용해야 합니다.
이러한 DST들은 스택에 직접 저장될 수 없으며, 항상 Box<T>, &T, Rc<T> 등 포인터 타입 뒤에 위치해야 합니다. 포인터 자체는 크기가 고정되어 있기 때문입니다.
// 아래 코드는 모두 컴파일 에러를 발생시킵니다.
// let s: str = "hello"; // error[E0277]: the size for values of type `str` cannot be known at compilation time
// let a: [i32] = [1, 2, 3];
// let t: dyn Clone;Sized 트레이트의 기본 제약
러스트에서 제네릭 함수나 구조체를 정의할 때, 타입 매개변수 T에는 기본적으로 Sized 제약이 암묵적으로 추가됩니다.
// 아래 두 선언은 사실상 동일합니다.
struct MyStruct<T> { data: T }
struct MyStruct<T: Sized> { data: T }이것은 제네릭 타입 T가 기본적으로는 스택에 저장될 수 있는, 크기가 알려진 타입이라고 가정한다는 의미입니다. 만약 DST를 다루는 제네릭 코드를 작성하고 싶다면, ?Sized를 사용하여 이 기본 제약을 해제해야 합니다.
// T가 `Sized`가 아니어도 되도록 `?Sized` 제약을 추가합니다.
// 이렇게 하면 MyWrapper<str>와 같은 타입을 다룰 수 있게 됩니다.
// (물론 MyWrapper<str> 자체는 직접 생성할 수 없고 &MyWrapper<str> 등으로 다뤄야 함)
struct MyWrapper<T: ?Sized> {
data: T,
}
fn main() {
let text: &MyWrapper<str> = &MyWrapper { data: *"hello" };
println!("{}", &text.data);
}결론
Sized 트레이트는 러스트의 메모리 관리 모델의 근간을 이루는 조용한 영웅입니다. 모든 타입의 크기가 컴파일 시점에 알려져야 한다는 기본 원칙을 통해, 컴파일러는 효율적인 스택 할당을 수행할 수 있습니다. str, [T], dyn Trait과 같은 DST의 존재와 이들을 포인터 뒤에 숨겨서 다뤄야 하는 규칙을 이해하는 것은 Sized의 역할을 이해하는 것과 직결되며, 이는 러스트의 소유권 및 메모리 모델을 더 깊이 파악하는 데 필수적입니다.