이 문서는 로드맵 7단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 스택 밖의 메모리·공유 소유권·내부 가변성·순환 참조를
Box/Rc/Arc/RefCell/Weak로 정리합니다. axum의Arc<AppState>도 여기 계열입니다.
- 큰 데이터를 스택 대신 힙에 두고, 스택에는 포인터만.
- 재귀 타입 — 크기를 알 수 없는 타입을 컴파일하려면 간접이 필요.
- 소유권을 옮기되 복사 비용은 포인터만 (큰 struct 이동).
dyn Trait—Box<dyn Shape>(3단계).
fn main() {
let b = Box::new(5);
println!("{b}"); // Deref로 T처럼 사용
}// enum List { Cons(i32, List), Nil } // 크기 무한 → 에러
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))))));
let _ = list;
}Box는 유일한 소유자입니다. 공유가 필요하면 Rc/Arc로 갑니다.
-
Box::new로 큰 배열(예:[0u8; 1024])을 힙에 두기 - 재귀
List를Cons/Nil로 만들어 보기
use std::rc::Rc;
fn main() {
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // 깊은 복사 아님 — 카운트 +1
let c = Rc::clone(&a);
println!("count = {}", Rc::strong_count(&a)); // 3
println!("{a}, {b}, {c}");
} // 스코프 끝에서 카운트 감소, 0이 되면 해제Rc::clone은 관례상 .clone()보다 Rc::clone(&x)로 써서 “비싸지 않은 클론”임을 드러냅니다.
use std::sync::Arc;
use std::thread;
fn main() {
let data = Arc::new(vec![1, 2, 3]);
let mut handles = vec![];
for i in 0..3 {
let data = Arc::clone(&data);
handles.push(thread::spawn(move || {
println!("thread {i}: {:?}", data);
}));
}
for h in handles {
h.join().unwrap();
}
}Rc |
Arc |
|
|---|---|---|
| 스레드 | 단일 | 다중 (atomic refcount) |
| 비용 | 더 저렴 | 약간 더 비쌈 |
| axum AppState | — | Arc<AppState> 표준 |
Rc/Arc가 주는 건 불변 공유입니다. 공유하면서 내용을 바꾸려면 아래 RefCell/Mutex가 필요합니다.
-
Rc로 두 리스트가 같은 꼬리를 공유하는 그래프 흉내 -
Rc를thread::spawn에 넣어 에러를 보고Arc로 고치기
보통 &T로는 수정 불가. RefCell은 불변 참조만 있어도 내부 값을 빌림 체크 후 수정하게 합니다. 위반 시 컴파일 에러가 아니라 panic.
use std::cell::RefCell;
fn main() {
let data = RefCell::new(5);
*data.borrow_mut() += 1;
println!("{}", data.borrow());
// 런타임 panic 예 (주석 해제 금지 권장):
// let r1 = data.borrow();
// let r2 = data.borrow_mut(); // already borrowed
}use std::cell::Cell;
fn main() {
let c = Cell::new(1);
c.set(2);
println!("{}", c.get());
}Cell은 참조를 빌려주지 않고 값 복사로 出入합니다. Copy가 아닌 큰 타입에는 RefCell이 맞습니다.
use std::cell::RefCell;
use std::rc::Rc;
fn main() {
let shared = Rc::new(RefCell::new(vec![1]));
let a = Rc::clone(&shared);
let b = Rc::clone(&shared);
a.borrow_mut().push(2);
b.borrow_mut().push(3);
println!("{:?}", shared.borrow()); // [1, 2, 3]
}단일 스레드 UI·트리·그래프에서 매우 흔합니다.
멀티스레드면 Arc<Mutex<T>> / Arc<RwLock<T>>가 대응 패턴입니다.
-
RefCell에서 불변 borrow 중 mut borrow로 panic 재현 (짧게) -
Rc<RefCell<i32>>로 두 핸들이 같은 카운터를 올리기
Rc끼리 서로를 강한 참조로 가리키면 카운트가 0이 되지 않아 누수가 납니다.
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>, // 부모는 Weak
children: RefCell<Vec<Rc<Node>>>, // 자식은 Rc
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf strong={}, weak={}", Rc::strong_count(&leaf), Rc::weak_count(&leaf));
{
let branch = Rc::new(Node {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
println!(
"branch strong={}, weak={}",
Rc::strong_count(&branch),
Rc::weak_count(&branch)
);
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
} // branch drop → leaf의 parent Weak는 upgrade 시 None
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
println!("leaf strong={}", Rc::strong_count(&leaf));
}규칙 감각:
- 소유 관계의 방향에는
Rc(부모 → 자식). - 역참조(자식 → 부모, 캐시, 옵저버)에는
Weak. Weak는upgrade() -> Option<Rc<T>>로만 접근 — 이미 죽었으면None.
- 부모·자식을 둘 다
Rc로만 연결해strong_count가 안 내려가는 상황 관찰 후Weak로 수정 -
upgrade성공/실패를 분기하는 코드 작성
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct TreeNode {
name: String,
parent: RefCell<Weak<TreeNode>>,
children: RefCell<Vec<Rc<TreeNode>>>,
}
impl TreeNode {
fn new(name: impl Into<String>) -> Rc<Self> {
Rc::new(Self {
name: name.into(),
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
})
}
fn add_child(self: &Rc<Self>, child: &Rc<TreeNode>) {
child.parent.borrow_mut().replace(Rc::downgrade(self));
self.children.borrow_mut().push(Rc::clone(child));
}
fn path(self: &Rc<Self>) -> String {
let mut parts = vec![self.name.clone()];
let mut current = Rc::clone(self);
while let Some(parent) = current.parent.borrow().upgrade() {
parts.push(parent.name.clone());
current = parent;
}
parts.reverse();
parts.join("/")
}
}
fn main() {
let root = TreeNode::new("root");
let home = TreeNode::new("home");
let async_user = TreeNode::new("async");
root.add_child(&home);
home.add_child(&async_user);
println!("{}", async_user.path()); // root/home/async
println!("root children = {}", root.children.borrow().len());
// root만 잡고 있으면 서브트리 유지.
// 순환이 없으므로 스코프 종료 시 정상 drop.
drop(async_user);
drop(home);
println!("root still alive: {}", root.name);
}-
find(name: &str) -> Option<Rc<TreeNode>>DFS 구현 - 같은 구조를
Arc+Mutex로 바꿔 스레드에서 자식 추가 - axum
State(Arc<AppState>)를 떠올리며: 왜Rc가 아니라Arc인지 한 문단 정리
Box<T> 유일한 힙 소유, 재귀 타입, dyn Trait
Rc<T> / Arc<T> 공유 소유 (단일 / 멀티 스레드)
Cell / RefCell 내부 가변성 (Copy set / 런타임 빌림)
Mutex / RwLock 멀티스레드 내부 가변 (RefCell 대응)
Rc<RefCell<T>> 단일 스레드 공유+가변 단골
Weak<T> 순환 참조 끊기, upgrade → Option
8단계 모듈 시스템에서는 mod / pub / use, 파일 배치, 워크스페이스, feature/cfg를 다룹니다. 문법은 단순하지만 프로젝트 규모가 커질수록 체감이 큽니다.