Skip to content

Latest commit

 

History

History
310 lines (227 loc) · 8.48 KB

07.md

File metadata and controls

310 lines (227 loc) · 8.48 KB

Rust 문법 로드맵 — 7단계: 스마트 포인터 & 메모리 모델 (상세 교재)

이 문서는 로드맵 7단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 스택 밖의 메모리·공유 소유권·내부 가변성·순환 참조를 Box / Rc / Arc / RefCell / Weak로 정리합니다. axum의 Arc<AppState>도 여기 계열입니다.

선수: 2단계 소유권, 3단계 Deref/Drop, 6단계 move 클로저


7.1 Box<T> — 힙 할당

왜 쓰는가

  1. 큰 데이터를 스택 대신 힙에 두고, 스택에는 포인터만.
  2. 재귀 타입 — 크기를 알 수 없는 타입을 컴파일하려면 간접이 필요.
  3. 소유권을 옮기되 복사 비용은 포인터만 (큰 struct 이동).
  4. 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로 만들어 보기

7.2 Rc<T> / Arc<T> — 참조 카운팅

Rc<T> — 단일 스레드 공유 소유권

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)로 써서 “비싸지 않은 클론”임을 드러냅니다.

Arc<T> — 스레드 안전 버전

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로 고치기

7.3 RefCell<T> / Cell<T> — 내부 가변성

빌림 규칙을 런타임에 검사

보통 &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
}

Cell<T> — Copy 타입에 단순 set/get

use std::cell::Cell;

fn main() {
    let c = Cell::new(1);
    c.set(2);
    println!("{}", c.get());
}

Cell은 참조를 빌려주지 않고 값 복사로 出入합니다. Copy가 아닌 큰 타입에는 RefCell이 맞습니다.

Rc<RefCell<T>> — 공유 + 내부 가변

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>>로 두 핸들이 같은 카운터를 올리기

7.4 순환 참조와 Weak<T>

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 성공/실패를 분기하는 코드 작성

7.5 종합 실습 — 트리 + Weak로 순환 해결

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를 다룹니다. 문법은 단순하지만 프로젝트 규모가 커질수록 체감이 큽니다.