본문 바로가기

Rust

[Rust] 스마트 포인터 (1) - Box<T>, Rc<T>

안녕하세요.

오늘은 스마트 포인터에 대해 알아보고,

그 중 Box<T>, Rc<T>에 대해 살펴보도록 하겠습니다.


📌 스마트 포인터

스마트 포인터는 일반적인 포인터와 달리, 메모리 해제 및 관리 기능을 제공하는 포인터 입니다.

  • 소유권과 수명을 따르며,
  • Drop 트레잇을 구현하여 자동으로 메모리 정리가 됩니다

 

Rust에서 자주 사용하는 스마트 포인터 중 Box<T>와 Rc<T>에 대해 알아보겠습니다.


📌 Box<T>

✅ 기본 개념

힙 할당 스마트 포인터

fn main() {
    let x = Box::new(10); // 10을 힙에 저장
    println!("x = {}", x); // 10
}
 

Deref와 Drop 트레잇 | Notion

스마트 포인터를 더 편리하게 사용하고, 메모리 관리를 자동화하기 위해 Deref와 Drop 이라는 두 트레잇을 제공함

mixolydian-tadpole-478.notion.site


✅ When?

Box<T>를 언제 사용하는지 알아봅시다.

 

Case1) 크기가 큰 데이터를 힙에 저장하여, 스택 메모리를 절약할 때

Rust는 기본적으로 모든 데이터가 스택에 저장됩니다.

Box<T>를 사용하면 큰 데이터를 힙에 저장하고, 스택에는 작은 크기의 포인터만 저장할 수 있습니다.

  • 32비트 또는 64비트

 

struct BigData {
    data: [u8; 1024], // 1KB 크기의 배열
}

fn main() {
    let _big_data = Box::new(BigData { data: [0; 1024] }); // 힙에 저장
}
  • 스택에는 Box<T> 포인터만 저장되고,
  • 실제 데이터는 힙에 저장됩니다.

 

Case2) 컴파일 타임에 크기를 알 수 없는 타입을 저장할 때

Rust는 모든 변수의 크기를 컴파일 타임에 알아야 합니다.

런타임에 크기가 결정되는 경우도 있는데, Box<T>를 사용해 해결할 수 있습니다.

#[derive(Debug)]

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

fn main() {
    let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
    println!("{:?}", list) // Cons(1, Cons(2, Nil))
}

📌 Rc<T>

✅ 기본 개념

참조 카운팅 스마트 포인터

  • 하나의 데이터를 여러 개의 소유자가 공유할 수 있도록 해줍니다.
  • 데이터가 더 이상 사용되지 않을 때 자동으로 메모리를 해제합니다.

✅ When?

여러 개의 소유자가 동일한 값을 공유해야 할 때 사용합니다.

 

아래 코드는 컴파일 에러가 납니다.

struct Node {
    value: i32,
    next: Option<Box<Node>>, // Box<Node>는 한 개의 소유자만 가능!
}

fn main() {
    let node1 = Node {
        value: 10,
        next: None,
    };

    let node2 = Node {
        value: 20,
        next: Some(Box::new(node1)), // ❌ 여기서 node1의 소유권이 이동됨
    };

    println!("{}", node1.value); // ❌ 에러 발생! (node1의 소유권이 이동되었기 때문)
}
  • node1은 node2에게 소유권이 넘어가서 더 이상 사용할 수 없습니다.
  • 여러 개의 노드가 같은 데이터를 공유하고 싶을 때 → Rc<T>를 사용할 수 있습니다.

 

use std::rc::Rc;

struct Node {
    value: i32,
    next: Option<Rc<Node>>, // 여러 개의 소유자가 가능하도록 Rc<T> 사용
}

fn main() {
    let node1 = Rc::new(Node {
        value: 10,
        next: None,
    });

    let node2 = Node {
        value: 20,
        next: Some(Rc::clone(&node1)), // Rc<T>의 참조 카운트를 증가시킴
    };

    let node3 = Node {
        value: 30,
        next: Some(Rc::clone(&node1)), // 또 다른 노드도 node1을 공유 가능
    };

    println!("node1을 가리키는 참조 개수: {}", Rc::strong_count(&node1)); // 출력: 3

    println!("{}", node1.value); // 10
    println!("{}", node2.next.as_ref().unwrap().value); // 10
    println!("{}", node3.next.as_ref().unwrap().value); // 10
}
  • Rc::new(value) → Rc 인스턴스를 생성합니다.
  • Rc::clone(&rc_value) → 실제 데이터를 복사하는 것이 아니라, 참조 카운트를 증가시킵니다.
  • Rc::string_count(&rc_value) → 현재 Rc가 몇 개의 소유자를 가지고 있는지 확인합니다.
  • Rc::weak_count(&rc_value) → Weak (약한 참조) 개수를 확인할 때 사용합니다.

✅ 한계

1) 가변성을 허용하지 않는다

  • 내부 값을 변경하고 싶다면 RefCell<T>와 함께 사용해야 합니다.

 

2) 멀티스레드 환경에서 사용이 불가능하다

  • 멀티스레드 환경에서는 Arc<T>를 사용해야 합니다.