스마트 포인터(Smart Pointer)

스마트 포인터(Rc)

Rust는 메모리 안전성을 보장하기 위해 소유권 시스템을 사용하며, 여기에 스마트 포인터라이프타임이 핵심 역할을 합니다. 스마트 포인터는 데이터를 가리키는 기능이외에 소유권, 참조 카운팅 등 추가 기능을 포함하며, Box<T>, Rc<T>, RefCell<T>에 대해 설명하겠습니다.


1. 스마트 포인터(Smart Pointer)란?

  • 일반 포인터처럼 데이터를 가리키지만, 추가적인 기능(소유권, 참조 카운팅 등)을 포함합니다.
  • 표준 스마트 포인터의 종류에는 아래 세 가지가 있습니다.
    1. Box<T>: 일반 포인터는 스택에 저장되는데, Box는 힙에 데이터를 저장합니다.
    2. Rc<T>: 참조 카운팅(reference counting)을 통한 공유
    3. RefCell<T>: 런타임 시점 가변성 검사

2. 스마트 포인터의 종류

가. Box<T> – 힙에 저장

fn main() {
    let b = Box::new(5);
    println!("b = {}", b);
}
  • let b = Box::new(5);
    5는 i32 타입이므로 스택에 저장되는 것인데,
    Box::new를 통해 힙에 저장하고, 변수 b에 할당하므로
    변수 b는 Box<i32> 타입이 되며, 힙에 있는 5를 가리킵니다.
  • println!(“b = {}”, b);
    Box는 Deref(역참조) trait을 구현하고 있어서,
    자동으로 Box 안의 값을 꺼내서 5를 출력해 줍니다.

변수 b는 Box 타입이 되며, 힙에 있는 5를 가리킵니다.

  • 박스는 여러분이 데이터를 스택이 아니라 힙에 저장할 수 있도록 해줍니다.
  • 구조체 간 재귀 타입 정의 시 유용합니다.
  • 아래와 같이 재귀 타입의 enum 정의시, 다시 말해 List가 List를 포함하고 잇으므로, 컴파일 타임에서는 List 타입의 크기를 알 수 없으므로 아래를 실행하면
enum List {
    Cons(i32, List),
    Nil,
}

use List::{Cons, Nil};

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

error[E0072]: recursive type ‘List’ has infinite size
(에러[E0072]: 재귀 타입 List가 무한한 크기를 가지고 있습니다)
란 에러 메시지가 나옵니다.

재귀 타입 열거형 라이프타임 오류

따라서, 아래와 같이 List를 Box로 감싸야 합니다.

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

Box를 사용해서 완성한 코드는 아래와 같습니다.

#[derive(Debug)]
enum List {
    Cons(i32, Box<List>),
    Nil,
}

use List::{Cons, Nil};

fn main() {
    let list = Cons(1,
        Box::new(Cons(2,
            Box::new(Cons(3,
                Box::new(Nil))))));
    println!("list = {:?}", list);
}
  • List 열거형을 출력하기 위해서 #[derive(Debug)]를 추가해야 하며, println!에서는 {:?}로 출력 포맷을 지정해야 합니다.
  • Box::new로 List를 구현합니다.

나. Rc<T> – 여러 소유자 공유

어떤 값이 계속 사용되는지 아니면 그렇지 않은지를 알기 위해 해당 값에 대한 참조자의 갯수를 계속 추적하는 것입니다. 만일 값에 대한 참조자가 0개라면, 그 값은 어떠한 참조자도 무효화하지 않고 메모리에서 정리될 수 있습니다.

우리 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고, 어떤 부분이 그 데이터를 마지막에 이용하게 될지 컴파일 타임에는 알 수 없는 경우 Rc 타입을 사용합니다. 만일 어떤 부분이 마지막으로 사용할지 컴파일 타임에 알 수 있다면 보통의 소유권 규칙이 적용됩니다.

use std::rc::Rc;

fn main() {
    let a = Rc::new(10);
    let b = Rc::clone(&a);
    println!("a = {}, b = {}", a, b);
}
  • use std::rc::Rc;
    Rc는 프렐루드(prelude, 프렐루드는 Rust가 모든 Rust 프로그램에 자동으로 가져오는 항목들의 목록입니다. )에 포함되어 있지 않으므로 use 구문을 추가했습니다. Box는 use를 사용하지 않는 것과 대비됩니다.
  • let a = Rc::new(10);
    10을 Rc에 저장한 후 a에 할당합니다.
    따라서, a는 Rc<i32> 타입이 되고, 다시 말하면 a는 10을 가리키는 참조 카운팅 스마트 포인터입니다.
    이 경우 참조 카운트는 1입니다.
  • let b = Rc::clone(&a);
    a.clone()을 호출할 수도 있지만, 러스트의 관례는 Rc::clone(&a)를 사용합니다. Rc::clone은 clone 이 하는 것처럼 깊은 복사 (deep copy) 를 만들지 않고, 오직 참조 카운트만 증가 시켜 큰 시간이 들지 않습니다.
    이제 참조 카운트는 2가 됩니다.
  • println!(“a = {}, b = {}”, a, b);
    Rc는 Deref(역참조) trait을 구현해서 자동으로 &T처럼 동작하므로 내부 값이 출력됩니다.
    출력 값은 a = 10, b = 10입니다.
스마트 포인터(Rc)
스마트 포인터(Rc)
  • 단일 스레드 환경에서만 사용가능합니다.
  • 참조수는 Rc::strong_count(&a)로 확인 가능합니다.

다. RefCell<T> – 런타임 내부 가변성

내부 가변성 (interior mutability) 은 어떤 데이터에 대한 불변 참조자가 있을 때라도 여러분이 데이터를 변형할 수 있게 해주는 러스트의 디자인 패턴입니다. 보통 이러한 동작은 빌림 규칙에 의해 허용되지 않습니다.

불변 및 가변 참조자를 만들때, 우리는 각각 & 및 &mut 문법을 사용합니다. RefCell을 이용할때는 borrow와 borrow_mut 메소드를 사용하는데, 이들은 RefCell이 소유한 안전한 API 중 일부입니다. borrow 메소드는 스마트 포인터 타입인 Ref<T>를 반환하고, borrow_mut는 스마트 포인터 타입 RefMut<T>를 반환합니다. 두 타입 모두 Deref를 구현하였으므로 우리는 이들을 보통의 참조자처럼 다룰 수 있습니다.

만일 런타임 시 빌림 규칙을 위반한다면, RefCell의 구현체는 panic!을 일으킵니다.

use std::cell::RefCell;

fn main() {
    let data = RefCell::new(1);
    *data.borrow_mut() += 1;
    println!("data = {:?}", data.borrow());
}
  • let data = RefCell::new(1);
    1이라는 값을 감싼 RefCell<i32>를 생성한 후 data에 할당하므로, data는 RefCell<i32> 타입이며, 내부에 1을 가지고 있습니다.
    data는 mut가 없으므로 불변 참조입니다.
  • *data.borrow_mut() += 1;
    .borrow_mut()은 RefCell<i32>인 data의 내부 값을 가변 참조(mutably borrow) 하겠다는 의미입니다.

    * 연산자는 실제 값을 꺼내는 역할을 합니다.
    따라서, 이 코드는 data의 내부값을 꺼내서 1을 더한다는 의미입니다.

    이 함수는 런타임에 현재 가변 참조가 가능한지 검사해서
    이미 다른 가변/불변 참조가 있으면 panic!이 발생합니다.
  • println!(“data = {:?}”, data.borrow());
    .borrow()는 내부 값을 불변 참조(immutably borrow)하겠다는 뜻입니다.
    {:?}는 Debug 포맷 출력입니다.
    이 시점에서 내부 값은 2입니다.
    따라서, 출력 결과는
    data = 2 입니다.

Box와 Rc는 컴파일 타임 불변성, RefCell은 런타임 가변성


3. Box<T>, Rc<T>, RefCell<T> 비교

  • Rc<T>는 동일한 데이터에 대해 복수개의 소유자를 가능하게 하지만, Box<T>와 RefCell<T>는 단일 소유자만 갖습니다.
  • Box<T>는 컴파일 타임에 검사된 불변 혹은 가변 빌림을 허용하고, Rc<T>는 오직 컴파일 타임에 검사된 불변 빌림만 허용하지만, RefCell<T>는 런타임에 검사된 불변 혹은 가변 빌림을 허용합니다.
  • RefCell<T>가 런타임에 검사된 가변 빌림을 허용하기 때문에, RefCell<T>가 불변일 때라도 RefCell<T> 내부의 값을 변경할 수 있습니다.

4. 요약 정리

항목설명
Box<T>힙에 값 저장, 단일 소유자
Rc<T>다중 소유자 참조 카운팅, 단일 스레드만 지원
RefCell<T>내부 가변성 제공, 런타임시 체크

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다