목차
- Rc 타입과 Weak 타입
- Rc 타입 사용하기
- Weak 타입 사용하기
- 순환 참조의 문제
Rc 타입과 Weak 타입
소유권 규칙에 따라 Rust에서 어떤 값은 여러 소유자를 가질 수 없습니다. Reference Counted를 의미하는 Rc는 힙 메모리에 할당된 타입 T 값의 소유권을 공유할 수 있게 해주는 타입입니다. 즉, 스마트 포인터 Rc를 사용하면 타입 T의 값에 대한 여러 개의 소유자를 만들 수 있습니다.
기본적으로, Rc 타입은 Clone Trait을 구현하고 있고 clone
을 호출해서 T 값에 대한 새로운 포인터를 생성하며, 모든 Rc 포인터가 해제되면 메모리에 할당된 T 값이 drop되는 구조입니다. Rust에서 공유된 참조자는 수정할 수 없는데, Rc 타입 또한 예외가 아니며 일반적인 방법으로는 mutable한 참조를 얻을 수 없습니다. 만약, mutable 한 참조가 필요하면 Cell 타입이나 RefCell 타입을 함께 사용해야 합니다.
Rc 타입은 원자적이지 않은 참조 카운팅으로 오버헤드가 낮은 장점이 있지만 단일 스레드에서만 사용 가능합니다. 따라서, Rc 타입은 스레드 간 전송을 위한 Trait인 Send
타입을 구현하지 않습니다. 한편, 원자적 참조 카운팅 타입인 Arc(Atomic Reference Counted)가 있습니다. Arc 타입은 다중 스레드 간 사용이 가능하지만 Rc 타입보다 오버헤드가 큰 단점이 있습니다.
std::rc 모듈에는 Rc 타입과 더불어 약한 참조인 Weak 타입이 존재합니다. Rc 타입을 downgrade
해서 Weak<T>
타입을 얻을 수 있고, Weak 타입을 upgrade
해서 Option<Rc<T>>
타입을 얻을 수 있습니다. Weak 타입에서 Rc 타입을 얻고자 upgrade
를 사용할 때, T 값이 이미 drop 되었다면 None을 리턴합니다. 이 말은 Rc 타입과 다르게 Weak 타입은 메모리에 할당된 타입 T의 값이 살아있도록 유지시키지 않는다는 것을 의미합니다.
즉, Rc 타입의 강한 참조 카운트가 0이 되면 T 값이 drop 되는데, 약한 참조 카운트를 의미하는 Weak 타입은 영향을 미치지 않습니다.
앞서 설명했듯, Rc 타입이 갖고 있는 강한 참조 카운트가 0이 되지 않으면 T 값은 drop 되지 않습니다. 이는 T 값이 결코 drop 될 수 없는 순환 참조를 야기할 수도 있습니다. T 값의 drop에 영향을 미치지 않는 Weak 타입은 이런 문제의 상황에서 유용하게 사용될 수 있습니다. 자세한 내용은 아래에서 코드와 함께 살펴보겠습니다.
Rc 타입 사용하기
fn main() {
let rc = Rc::new(10);
println!("[ 1 ]");
println!("value of rc => {}", rc);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
}
Rc 타입은 new 함수를 통해 생성할 수 있습니다. 위 코드 3번째 라인에서 볼 수 있듯이 Rc 타입은 Deref Trait을 구현하고 있어 자동으로 역참조 됩니다. Rc가 생성될 때 strong_count
값은 1로 초기화됩니다.
아래 코드는 clone
을 통해 타입 T 값에 대한 여러 개의 참조자를 만드는 것을 보여줍니다.
fn main() {
let rc = Rc::new(10);
println!("[ 1 ]");
println!("value of rc => {}", rc);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
{
let rc2 = rc.clone();
println!("[ 2 ]");
println!("value of rc2 => {}", rc2);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
let rc3 = Rc::clone(&rc);
println!("[ 3 ]");
println!("value of rc3 => {}", rc3);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
}
println!("[ 4 ]");
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
}
Rc 타입은 Clone Trait을 구현하고 있고 rc.clone()
과 Rc::clone()
두 가지 방식으로 호출할 수 있습니다. clone을 호출하면 타입 T 값에 대한 새로운 참조자가 생성되며, strong_count
는 1 증가합니다. rc2와 rc3가 코드 블록을 벗어남에 따라 strong_count
는 총 2가 감소해 마지막으로 출력되는 strong_count
의 값은 1이 됩니다. 생성된 모든 Rc 타입이 drop 되면 strong_count
가 0이 되면서 힙에 할당된 타입 T 값 역시 drop 됩니다.
Weak 타입 사용하기
Weak 타입에도 생성 메서드 new가 존재하지만, 인자로 어떠한 타입 T 값도 받지 않습니다. 즉, 타입 T에 대한 어떠한 값도 메모리에 할당되지 않습니다. 따라서, new로 새롭게 생성한 Weak 타입의 upgrade 메서드는 항상 None을 리턴합니다.
fn main() {
let weak: Weak<i32> = Weak::new();
assert!(weak.upgrade().is_none());
}
아래 코드에서 Rc 타입을 Weak 타입으로 downgrade 하고, Weak 타입을 Rc 타입으로 upgrade 하는 과정을 볼 수 있습니다. 예제를 실행해 strong_count
와 weak_count
값의 변화를 확인해보세요.
fn main() {
let rc = Rc::new(10);
println!("[ 1 ]");
println!("value of rc => {}", rc);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
let weak = Rc::downgrade(&rc);
println!("[ 2 ]");
println!("value of weak => {}", unsafe { &*weak.as_ptr() });
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
if let Some(rc2) = Weak::upgrade(&weak) {
println!("[ 3 ]");
println!("value of rc2 => {}", rc2);
println!("strong count => {}", Rc::strong_count(&rc));
println!("weak count => {}", Rc::weak_count(&rc));
} else {
println!("강한 참조가 남아 있지 않습니다.");
}
}
9번째 줄에서 값을 출력하는 방식이 Rc와 다른 것을 확인할 수 있습니다. Weak 타입은 Rc 타입과 다르게 Deref Trait을 구현하고 있지 않기 때문에 자동으로 역참조가 일어나지 않습니다. 또한, as_ptr() 메서드를 통해 T 값에 접근할 수 있지만, 아직 타입 T 값이 메모리에서 drop 되지 않았다는 것을 알 수 없기 때문에 unsafe 키워드를 사용해야 합니다. 그래서 개발자는 Weak 참조자가 가리키는 값이 아직 유효하다는 것을 보장할 수 있을 때 사용해야 합니다.
Weak 타입은 Rc 타입으로 upgrade 할 수 있는데, downgrade와 달리 upgrade는 Option<Rc> 을 반환합니다. 이는, 앞서 설명했듯이 Weak 타입이 메모리에 할당된 타입 T 값의 유효성을 보장하지 않기 때문입니다. 만약 strong_count
가 0이 되어 타입 T 값이 drop 된 상태라면, upgrade 메서드는 None을 반환할 것입니다.
순환 참조의 문제
Rc 타입 간에는 순환 참조 문제가 발생할 수 있는데, 이를 해결하기 위해서 Weak 타입을 사용할 수 있습니다. 트리 데이터 구조 예제 코드를 통해 순환 참조가 일어날 수 있는 상황에서 Weak 타입을 활용하는 방법을 알아보겠습니다.
#[derive(Debug)]
struct Node {
value: i32,
children: RefCell<Vec<Rc<Node>>>,
}
먼저, 트리의 노드 구조체를 만듭니다. 이 노드는 하나의 값과 자식 노드들의 참조자들을 가지고 있습니다. 여기서 자식 노드는 Rc 타입으로 소유권을 공유하고 직접 접근할 수 있습니다. 또한 자식 노드가 수정될 수 있도록 RefCell<T>
타입으로 감쌌습니다.
이제, 이 노드 구조체를 이용해 leaf와 leaf를 자식 노드로 가지는 branch를 만들어 보겠습니다.
fn main() {
let leaf = Rc::new(
Node {
value: 3,
children: RefCell::new(vec![]),
}
);
let branch = Rc::new(
Node {
value: 5,
children: RefCell::new(vec![Rc::clone(&leaf)]),
}
);
}
leaf는 자식이 없는 Node이고, branch는 leaf를 자식으로 갖는 Node입니다. 우리는 이제 branch.children
을 통해 branch에서 leaf로 접근할 수 있습니다. 하지만, leaf가 부모 노드에 대한 참조자를 알지 못하기 때문에 leaf에서 branch로는 접근이 불가능한 상황입니다. 이를 위해 자식 노드가 부모 노드로 접근할 수 있도록 하는 parent 참조자를 추가해야 합니다.
우리가 parent 타입을 추가함으로써 leaf가 부모인 branch를 참조하고, branch가 자식인 leaf를 참조한다는 것을 쉽게 생각해볼 수 있습니다. 이때 parent의 타입을 children과 같이 Rc 타입으로 만든다면, strong_count
값이 0이 될 수 없는 순환 참조 문제를 야기할 수 있습니다. 따라서, 우리는 순환 참조 문제를 피하기 위해 parent 타입을 Weak 타입으로 만들 것입니다.
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Vec<Rc<Node>>>,
}
이제, 노드는 부모 노드를 소유하지는 않지만 참조할 수 있게 되었습니다.
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
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!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
16번째 줄에서 leaf 노드의 parent에 branch 노드의 Weak 참조자를 넣어 주는 것을 볼 수 있습니다.
아래는 위 코드를 출력한 결과입니다.
결과를 보면 Weak 참조자가 (Weak)
로 출력되는 것을 알 수 있고, 무한 출력이 없다는 것은 순환 참조를 야기하지 않고 있다는 것을 의미합니다.
References
'Dev > Language' 카테고리의 다른 글
impl Trait과 Box<dyn Trait> - Rust 프로그래밍 (1) | 2022.12.19 |
---|---|
Cell 타입과 RefCell 타입 - Rust 프로그래밍 (1) | 2022.11.26 |
소유권(Ownership)과 원시 타입 - Rust 프로그래밍 (0) | 2022.08.29 |
트레잇(Trait)과 트레잇 바운드(Trait Bound) - Rust 프로그래밍 (1) | 2022.08.25 |
반복문과 루프 레이블 - Rust 프로그래밍 (0) | 2022.08.14 |