← 목록  ·  디자인 패턴  ·  동시성  ·  이터레이터  ·  고급 트레이트

스마트 포인터 & 내부 가변성

Rust의 소유권 시스템이 일반 참조(&T)로는 표현하기 어려운 패턴들을 해결하는 방법. 트리, 그래프, 공유 상태, 런타임 가변성이 필요할 때 어떤 타입을 왜 쓰는지 정리한다.


1. Box<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))))));

Box<dyn Error> — 에러 타입 지우기

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가 더 편리

Deref Coercion

fn print_str(s: &str) { println!("{}", s); }

let boxed = Box::new(String::from("hello"));
// Box → &String (Deref) → &str (Deref) 자동 변환
print_str(&boxed); // 명시적 변환 필요 없음
스택 ptr → 0x7f... 실제 값 T

2. Rc<T> vs Arc<T> 공유 소유권

여러 곳에서 같은 값을 읽기 전용으로 공유하고 싶을 때. 마지막 소유자가 없어질 때 자동 해제.

Rc<T> — 단일 스레드

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되면 해제

Arc<T> — 멀티 스레드

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(); }

순환 참조 — Weak<T>로 끊기

부모-자식 트리에서 자식이 부모를 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도 해제 (순환 없음)
branch (Rc) strong_count: 1 leaf (Rc) strong_count: 2 Rc (strong) Weak (카운트 미증가)

3. Cell<T> vs RefCell<T> 내부 가변성

Interior Mutability — 불변 참조(&self)를 통해 내부 값을 바꾸는 패턴. Rust의 borrow checker를 런타임으로 미루거나 Copy로 우회한다.

Cell<T> — 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

RefCell<T> — 런타임 borrow

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 락

4. Rc<RefCell<T>> 패턴 공유 + 가변

단일 스레드에서 여러 소유자가 공유 데이터를 쓰기까지 해야 할 때. 가장 자주 등장하는 조합이다.

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());
    // ["시작", "처리 중", "완료"]
}

멀티스레드: Arc<Mutex<T>>

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

5. 어떤 걸 쓸지 결정 가이드

값을 힙에 두어야? 소유자가 하나? Box<T> 아니오 멀티 스레드? 아니오 Rc<T> Arc<T> 가변? Rc<RefCell<T>> 가변? Arc<Mutex<T>> 단일 소유자 + 불변 참조로 내부 수정? Copy → Cell<T> / 나머지 → RefCell<T>
타입스레드공유가변오버헤드
Box<T>단일힙 할당만
Rc<T>단일ref count (non-atomic)
Arc<T>다중atomic ref count
RefCell<T>단일런타임 borrow카운터 1개
Rc<RefCell<T>>단일런타임 borrowrc + refcell
Arc<Mutex<T>>다중atomic + OS lock
Arc<RwLock<T>>다중✅ (읽기 병렬)atomic + OS rwlock

6. 실전 예시 — 옵저버 패턴 with Rc<RefCell>

이벤트를 발생시키는 쪽(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] 로그아웃"]
}
멀티스레드 버전RcArc, RefCellMutex로 바꾸고 Observer 트레이트에 Send + Sync를 추가하면 된다. 구조는 동일하다.
옵저버 + 순환 참조 주의EventBusLoggerRc로 갖고, LoggerEventBus를 다시 Rc로 갖으면 메모리 누수. Loggerbus 방향은 Weak로 만들 것.