← 목록 · 디자인 패턴 · 동시성 · 이터레이터 · 고급 트레이트
Rust의 소유권 시스템이 일반 참조(&T)로는 표현하기 어려운 패턴들을 해결하는 방법. 트리, 그래프, 공유 상태, 런타임 가변성이 필요할 때 어떤 타입을 왜 쓰는지 정리한다.
Box<T>는 값을 힙에 올린다. 스택에는 포인터(8 bytes)만 남는다. 세 가지 상황에서 필수다.
// 컴파일 에러: List의 크기가 무한함
enum List {
Cons(i32, List), // 재귀 → 무한 크기
Nil,
}
// Box로 힙에 올리면 크기가 확정됨
enum List {
Cons(i32, Box), // 스택에는 i32 + 포인터(8B)만
Nil,
}
let list = List::Cons(1,
Box::new(List::Cons(2,
Box::new(List::Cons(3,
Box::new(List::Nil))))));
use std::error::Error;
use std::num::ParseIntError;
// 여러 에러 타입을 하나의 함수에서 반환
fn parse_and_double(s: &str) -> Result> {
let n: i32 = s.trim().parse()?; // ParseIntError
if n < 0 { return Err("음수 입력".into()); } // &str → Box
Ok(n * 2)
}
// 실제 앱에서는 anyhow::Result가 더 편리
fn print_str(s: &str) { println!("{}", s); }
let boxed = Box::new(String::from("hello"));
// Box → &String (Deref) → &str (Deref) 자동 변환
print_str(&boxed); // 명시적 변환 필요 없음
여러 곳에서 같은 값을 읽기 전용으로 공유하고 싶을 때. 마지막 소유자가 없어질 때 자동 해제.
use std::rc::Rc;
let a = Rc::new(vec![1, 2, 3]);
let b = Rc::clone(&a); // 복사 아님! 카운트 +1
let c = Rc::clone(&a); // 카운트 +1
println!("{}", Rc::strong_count(&a)); // 3
drop(c); // 카운트 -1
println!("{}", Rc::strong_count(&a)); // 2
// a, b 모두 drop되면 해제
use std::sync::Arc;
use std::thread;
let data = Arc::new(vec![1, 2, 3]);
let handles: Vec<_> = (0..4).map(|i| {
let data = Arc::clone(&data);
thread::spawn(move || {
println!("스레드 {}: {:?}", i, data);
})
}).collect();
for h in handles { h.join().unwrap(); }
부모-자식 트리에서 자식이 부모를 Rc로 참조하면 레퍼런스 카운트가 영원히 0이 안 된다. Weak는 카운트를 올리지 않는다.
use std::rc::{Rc, Weak};
use std::cell::RefCell;
#[derive(Debug)]
struct Node {
val: i32,
parent: RefCell>, // 부모: Weak (순환 방지)
children: RefCell>>, // 자식: Rc (강한 소유)
}
let leaf = Rc::new(Node {
val: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
val: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// leaf의 parent를 branch로 설정 (Weak)
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// parent에 접근하려면 upgrade() → Option>
if let Some(p) = leaf.parent.borrow().upgrade() {
println!("부모 val: {}", p.val); // 5
}
// branch drop → 카운트 0 → leaf도 해제 (순환 없음)
&self)를 통해 내부 값을 바꾸는 패턴. Rust의 borrow checker를 런타임으로 미루거나 Copy로 우회한다.
use std::cell::Cell;
struct Config {
debug: Cell,
counter: Cell,
}
let cfg = Config {
debug: Cell::new(false),
counter: Cell::new(0),
};
// &cfg (불변)임에도 내부 수정 가능
cfg.debug.set(true);
cfg.counter.set(cfg.counter.get() + 1);
println!("{}", cfg.debug.get()); // true
use std::cell::RefCell;
let data = RefCell::new(vec![1, 2, 3]);
// borrow() → Ref (불변 참조 래퍼)
let r = data.borrow();
println!("{:?}", *r); // [1, 2, 3]
drop(r); // 반드시 명시적 drop or 스코프 분리
// borrow_mut() → RefMut
data.borrow_mut().push(4);
// 동시에 불변+가변 borrow → 런타임 패닉!
// let _r = data.borrow();
// let _m = data.borrow_mut(); // PANIC
| 타입 | T 제약 | borrow 확인 | 패닉 가능 | 오버헤드 |
|---|---|---|---|---|
Cell<T> | Copy | 없음 | ❌ | 없음 |
RefCell<T> | 없음 | 런타임 | ✅ | 카운터 1개 |
Mutex<T> | Send | 런타임 (OS) | ✅ (poisoning) | OS 락 |
단일 스레드에서 여러 소유자가 공유 데이터를 쓰기까지 해야 할 때. 가장 자주 등장하는 조합이다.
use std::rc::Rc;
use std::cell::RefCell;
type SharedList = Rc>>;
struct EventLog {
entries: SharedList,
}
impl EventLog {
fn new() -> (Self, SharedList) {
let entries = Rc::new(RefCell::new(Vec::new()));
(EventLog { entries: Rc::clone(&entries) }, entries)
}
fn log(&self, msg: &str) {
self.entries.borrow_mut().push(msg.to_string());
}
}
fn main() {
let (logger, view) = EventLog::new();
logger.log("시작");
logger.log("처리 중");
logger.log("완료");
// view도 같은 Vec을 참조
println!("{:?}", view.borrow());
// ["시작", "처리 중", "완료"]
}
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0i32));
let handles: Vec<_> = (0..10).map(|_| {
let c = Arc::clone(&counter);
thread::spawn(move || {
let mut n = c.lock().unwrap();
*n += 1;
// lock guard drop → 자동 unlock
})
}).collect();
for h in handles { h.join().unwrap(); }
println!("결과: {}", counter.lock().unwrap()); // 10
| 타입 | 스레드 | 공유 | 가변 | 오버헤드 |
|---|---|---|---|---|
| Box<T> | 단일 | ❌ | ✅ | 힙 할당만 |
| Rc<T> | 단일 | ✅ | ❌ | ref count (non-atomic) |
| Arc<T> | 다중 | ✅ | ❌ | atomic ref count |
| RefCell<T> | 단일 | ❌ | 런타임 borrow | 카운터 1개 |
| Rc<RefCell<T>> | 단일 | ✅ | 런타임 borrow | rc + refcell |
| Arc<Mutex<T>> | 다중 | ✅ | ✅ | atomic + OS lock |
| Arc<RwLock<T>> | 다중 | ✅ (읽기 병렬) | ✅ | atomic + OS rwlock |
이벤트를 발생시키는 쪽(Subject)과 반응하는 쪽(Observer)이 서로를 Rc로 참조해야 할 때.
use std::rc::Rc;
use std::cell::RefCell;
trait Observer {
fn on_event(&self, event: &str);
}
struct EventBus {
observers: Vec>,
}
impl EventBus {
fn new() -> Self { EventBus { observers: Vec::new() } }
fn subscribe(&mut self, obs: Rc) {
self.observers.push(obs);
}
fn publish(&self, event: &str) {
for obs in &self.observers {
obs.on_event(event);
}
}
}
// 상태를 가진 옵저버 — 내부 가변성 필요
struct Logger {
log: RefCell>,
name: String,
}
impl Logger {
fn new(name: &str) -> Rc {
Rc::new(Logger { log: RefCell::new(Vec::new()), name: name.to_string() })
}
fn dump(&self) {
println!("[{}] 로그: {:?}", self.name, self.log.borrow());
}
}
impl Observer for Logger {
fn on_event(&self, event: &str) {
// &self (불변)이지만 RefCell로 로그 추가
self.log.borrow_mut().push(format!("[{}] {}", self.name, event));
}
}
fn main() {
let mut bus = EventBus::new();
let logger_a = Logger::new("A");
let logger_b = Logger::new("B");
bus.subscribe(Rc::clone(&logger_a));
bus.subscribe(Rc::clone(&logger_b));
bus.publish("사용자 로그인");
bus.publish("데이터 저장");
bus.publish("로그아웃");
logger_a.dump();
// [A] 로그: ["[A] 사용자 로그인", "[A] 데이터 저장", "[A] 로그아웃"]
logger_b.dump();
// [B] 로그: ["[B] 사용자 로그인", "[B] 데이터 저장", "[B] 로그아웃"]
}
Rc → Arc, RefCell → Mutex로 바꾸고 Observer 트레이트에 Send + Sync를 추가하면 된다. 구조는 동일하다.
EventBus가 Logger를 Rc로 갖고, Logger가 EventBus를 다시 Rc로 갖으면 메모리 누수. Logger→bus 방향은 Weak로 만들 것.