Rust 스마트 포인터
소유권과 대여 규칙만으로도 대부분의 상황을 해결할 수 있어요. 하지만 “주인은 딱 하나”라는 틀에 잘 맞지 않는 경우가 실제로 몇 가지 있어요. 실행해 보기 전까지 크기를 알 수 없는 값, 정말로 주인이 여럿 필요한 데이터, 공유 참조자를 통해 바꿔야 하는 값 같은 경우예요. 스마트 포인터(smart pointer)는 바로 이런 경우를 위한 Rust의 해결책이에요.
Box<T>: 값을 힙에 두기
섹션 제목: “Box<T>: 값을 힙에 두기”기본적으로 값은 스택에 저장되고, 컴파일러는 그 크기를 미리 알아야 해요. Box<T>는 값을 힙으로 옮기고, 그 값을 가리키는 크기가 고정된 포인터를 건네줘요. 컴파일할 때 타입의 크기를 알 수 없을 때 유용한데, 가장 흔한 예가 재귀 타입이에요.
fn main() { let boxed = Box::new(5); println!("{}", *boxed); // *로 안의 값을 꺼내요}실행 결과:
5Box가 꼭 필요한 대표적인 예는 자기 자신을 품는 타입이에요. 연결 리스트의 노드처럼요.
enum List { Cons(i32, Box<List>), Nil,}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));
match list { List::Cons(value, _) => println!("첫 번째 값: {value}"), List::Nil => println!("비어 있어요"), }}실행 결과:
첫 번째 값: 1Box가 없다면 Cons(i32, List)를 저장하는 데 무한한 공간이 필요해요. List 안에 List가 있고, 그 안에 또 List가 있고, 끝없이 이어지니까요. 반면 Box<List>는 크기가 고정된 포인터라서, 컴파일러가 필요한 공간을 계산할 수 있어요.
Rc<T>: 여러 명의 주인
섹션 제목: “Rc<T>: 여러 명의 주인”소유권 규칙에 따르면 값의 주인은 딱 하나예요. 대부분은 그게 맞아요. 그런데 가끔은 프로그램의 여러 부분이 정말로 같은 데이터를 함께 가져야 할 때가 있어요. Rc<T>(“reference counted”, 참조 카운트)는 지금 주인이 몇 명인지 세어 두는 방식으로 이걸 가능하게 해 줘요.
use std::rc::Rc;
fn main() { let a = Rc::new(String::from("공유 데이터")); let b = Rc::clone(&a); // 깊은 복사가 아니에요. 주인이 한 명 더 생길 뿐이에요 let c = Rc::clone(&a);
println!("{a} {b} {c}"); println!("주인 수: {}", Rc::strong_count(&a));}실행 결과:
공유 데이터 공유 데이터 공유 데이터주인 수: 3Rc::clone은 안에 있는 데이터를 복사하지 않아요. 카운터를 하나 올리고, 같은 값을 가리키는 참조를 하나 더 돌려줄 뿐이에요. 데이터는 모든 주인이 스코프를 벗어난 뒤에야 정리돼요.
RefCell<T>: 공유 참조자를 통해 값 바꾸기
섹션 제목: “RefCell<T>: 공유 참조자를 통해 값 바꾸기”보통 대여 규칙은 컴파일할 때 검사해요. 읽기 전용 참조자 여러 개, 아니면 가변 참조자 하나. 둘을 동시에 가질 수는 없어요. RefCell<T>는 이 검사를 실행 시간(런타임)으로 옮겨요. 그래서 겉보기에는 mut가 아닌 공유 참조자를 통해서도 값을 바꿀 수 있어요.
use std::cell::RefCell;
fn main() { let value = RefCell::new(5);
*value.borrow_mut() += 1;
println!("{}", value.borrow());}실행 결과:
6RefCell은 Rc와 짝지어 쓰는 경우가 가장 많아요. Rc<RefCell<T>>를 쓰면 주인이 여럿이면서도 값을 바꿀 수 있는 값을 만들 수 있어요. Rc<T>만으로는 할 수 없는 일이에요.
언제 실제로 써야 할까요
섹션 제목: “언제 실제로 써야 할까요”처음부터 스마트 포인터로 시작하지 마세요. 평범한 소유권과 대여로 시작하고, 컴파일러가 그걸로는 안 된다고 분명히 말해 줄 때만 꺼내 쓰면 돼요. 재귀 타입이라면 Box, 정말로 주인이 여럿 필요한 값이라면 Rc, 대여 규칙이 허락하지 않는 공유 참조자를 통한 변경이라면 RefCell이에요.
연습 문제
섹션 제목: “연습 문제”1. 연결 리스트 더하기
섹션 제목: “1. 연결 리스트 더하기”Box 절에 나온 List 열거형을 써요. 1, 2, 3으로 된 리스트를 만들고, 모든 값을 더하는 함수 fn sum(list: &List) -> i32를 작성해 보세요. 힌트: 리스트에 match를 써요. Cons(value, rest)라면 답은 value에 rest의 합을 더한 값이고, Nil이라면 0이에요.
정답 보기
enum List { Cons(i32, Box<List>), Nil,}
fn sum(list: &List) -> i32 { match list { List::Cons(value, rest) => value + sum(rest), List::Nil => 0, }}
fn main() { let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Cons(3, Box::new(List::Nil)))))); println!("합계: {}", sum(&list));}실행 결과:
합계: 6함수가 Nil에 닿을 때까지 리스트의 나머지 부분에 대해 자기 자신을 호출해요. &List가 필요한 자리에 rest(&Box<List>)를 그대로 넘길 수 있다는 점도 눈여겨보세요. Rust가 Box 안쪽을 알아서 들여다봐 줘요.
2. 함께 쓰는 장보기 목록
섹션 제목: “2. 함께 쓰는 장보기 목록”alice와 bob 두 사람이 함께 소유하고, 둘 다 항목을 추가할 수 있는 장보기 목록(Vec<String>) 하나를 만들어 보세요. 각자 항목을 하나씩 추가한 다음, 목록과 주인 수를 출력해요. 힌트: Vec을 Rc<RefCell<...>>로 감싸고, 각 사람에게 Rc::clone을 하나씩 주고, .borrow_mut().push(...)로 추가해요.
정답 보기
use std::cell::RefCell;use std::rc::Rc;
fn main() { let list = Rc::new(RefCell::new(Vec::new())); let alice = Rc::clone(&list); let bob = Rc::clone(&list);
alice.borrow_mut().push(String::from("우유")); bob.borrow_mut().push(String::from("빵"));
println!("{:?}", list.borrow()); println!("주인 수: {}", Rc::strong_count(&list));}실행 결과:
["우유", "빵"]주인 수: 3주인은 list, alice, bob 이렇게 셋이에요. 셋 모두 같은 Vec을 가리키니까, alice를 통해 추가한 항목이 bob과 list에서도 보여요. RefCell이 변경을 맡아 주기 때문에 어떤 변수에도 mut가 필요 없어요.
Box<T>는 값을 힙에 저장하고 크기가 고정된 포인터로 가리켜요. 재귀 타입에 꼭 필요해요.Rc<T>는 참조 카운트로 단일 스레드 코드에서 같은 값의 주인을 여럿 둘 수 있게 해 줘요. 스레드 사이에서는Arc<T>를 써요.RefCell<T>는 대여 규칙을 컴파일 시간이 아니라 실행 시간에 검사해서, 공유 참조자를 통한 변경을 허용해요. 대신 규칙을 어기면 패닉이 일어나요.- 평범한 소유권과 대여로는 해결되지 않을 때만 이것들을 꺼내 쓰세요.
이해도 점검
섹션 제목: “이해도 점검”이해도 점검
1. 연결 리스트 노드 같은 재귀 타입에는 왜 Box<T>가 필요할까요?
2. Rc::clone(&a)는 실제로 무슨 일을 할까요?
3. RefCell<T>는 대여 규칙을 언제 검사할까요?
4. Rc<T>만으로는 안 되고 Rc<RefCell<T>>로는 되는 일은 무엇일까요?
점수: 0 / 4
더 알아보기
섹션 제목: “더 알아보기”Box<T>를 사용하여 힙에 있는 데이터 가리키기 — The Rust Programming Language 한국어판: 이 페이지에 나온 콘스 리스트 예제를 포함해Box를 자세히 다뤄요.Rc<T>, 참조 카운트 스마트 포인터 — The Rust Programming Language 한국어판: 카운트가 오르내리는 걸 지켜보며 공유 소유권을 한 단계씩 설명해요.RefCell<T>와 내부 가변성 패턴 — The Rust Programming Language 한국어판: 대여 검사를 언제, 왜 실행 시간으로 옮기는지 현실적인 예제로 보여 줘요.std::cell— 표준 라이브러리 문서 (영어):Cell과RefCell을 설명하고, 각각 언제 고르면 좋은지 알려 줘요.- Rc — Rust by Example (영어): 주인이 생기고 사라질 때마다 참조 카운트를 출력하는 짧은 예제를 실행해 볼 수 있어요.