Rc<T>
, 참조 카운트 스마트 포인터
대부분의 경우에서 소유권은 명확합니다: 즉 어떤 변수가 주어진 값을 소유하는지 정확히 압니다. 그러나 하나의 값이 여러 개의 소유자를 가질 수 있는 경우도 있습니다. 예를 들어, 그래프 데이터 구조에서 여러 에지가 동일한 노드를 가리킬 수도 있고, 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지에 의해 소유됩니다. 노드는 어떠한 에지도 이를 가리키지 않아 소유자가 하나도 없는 상태가 아니라면 메모리 정리가 되어서는 안 됩니다.
명시적으로 복수 소유권을 가능하게 하려면 러스트의 Rc<T>
타입을 이용해야
하는데, 이는 참조 카운팅 (reference counting) 의 약자입니다. Rc<T>
타입은 어떤 값의 참조자 개수를 계속 추적하여 해당 값이 계속 사용 중인지를
판단합니다. 만일 어떤 값에 대한 참조자가 0개라면 이 값의 메모리 정리를
하더라도 유효하지 않은 참조자가 발생하지 않을 수 있습니다.
Rc<T>
를 거실의 TV라고 상상해 봅시다. 한 사람이 TV를 보러 들어올 때
TV를 켭니다. 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다. 마지막 사람이
거실을 나선다면, TV는 더 이상 사용되고 있지 않으므로 끕니다. 만일 누군가
계속 TV를 보고 있는 중에 어떤 이가 꺼버리면, 남아있던 TV 시청자들로부터 엄청난
소란이 있겠죠!
Rc<T>
타입은 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고
싶은데 컴파일 타임에는 어떤 부분이 그 데이터를 마지막에 이용하게 될지
알 수 없는 경우 사용됩니다. 만일 어떤 부분이 마지막으로 사용하는지
알았다면, 그냥 그 해당 부분을 데이터의 소유자로 만들면 되고, 보통의
소유권 규칙이 컴파일 타임에 수행되어 효력을 발생시킬 겁니다.
Rc<T>
는 오직 싱글스레드 시나리오용이라는 점을 주의하세요. 16장에서
동시성 (cuncurrency) 에 대한 논의를 할 때, 멀티스레드 프로그램에서
참조 카운팅을 하는 방법을 다루겠습니다.
Rc<T>
를 사용하여 데이터 공유하기
예제 15-5의 콘스 리스트 예제로 돌아가 봅시다. Box<T>
를 이용해서 이를
정의했던 것을 상기합시다. 이번에는 두 개의 리스트를 만들고 이 둘이 모두 세 번째
리스트의 소유권을 공유하도록 하겠습니다. 개념적으로는 그림 15-3처럼 생겼습니다:
먼저 5와 10을 담은 리스트 a
를 만들겠습니다. 그런 다음 두 개의 리스트를 더
만들 것입니다: 3으로 시작하는 b
와 4로 시작하는 c
를 말이죠. 그리고서 b
와
c
리스트 둘 모두 5와 10을 가지고 있는 첫 번째 a
리스트로 계속되도록 하겠습니다.
바꿔 말하면, 두 리스트 모두 5와 10을 담고 있는 첫 리스트를 공유하게 될 것입니다.
예제 15-17과 같이 Box<T>
를 가지고 정의한 List
를 이용하여 이
시나리오의 구현을 시도하면 작동하지 않을 것입니다:
파일명: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
let b = Cons(3, Box::new(a));
let c = Cons(4, Box::new(a));
}
이 코드를 컴파일하면 다음과 같은 에러를 얻습니다:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
error[E0382]: use of moved value: `a`
--> src/main.rs:11:30
|
9 | let a = Cons(5, Box::new(Cons(10, Box::new(Nil))));
| - move occurs because `a` has type `List`, which does not implement the `Copy` trait
10 | let b = Cons(3, Box::new(a));
| - value moved here
11 | let c = Cons(4, Box::new(a));
| ^ value used here after move
For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` due to previous error
Cons
배리언트는 자신이 들고 있는 데이터를 소유하므로, b
리스트를 만들 때
a
는 b
안으로 이동되어 b
의 소유가 됩니다. 그다음 c
를 생성할 때 a
를
다시 사용하려 할 경우는 허용되지 않는데, 이미 a
가 이동되었기 때문입니다.
Cons
의 정의를 변경하여 참조자를 대신 들고 있도록 할 수도 있지만, 그러면
라이프타임 매개변수를 명시해야 할 것입니다. 라이프타임 매개변수를 명시함으로써,
리스트 내의 모든 요소가 최소한 전체 리스트만큼 오래 살아있도록 지정할
것입니다. 이는 예제 15-17의 요소와 리스트에 대한 경우지, 모든 시나리오에
맞는 것은 아닙니다.
그 대신 예제 15-18과 같이 Box<T>
의 자리에 Rc<T>
를 이용하는
형태로 List
의 정의를 바꾸겠습니다. 각각의 Cons
배리언트는 이제 어떤
값과 List
를 가리키는 Rc<T>
를 갖게 될 것입니다. b
를 만들 때는 a
의
소유권을 얻는 대신, a
를 가지고 있는 Rc<List>
를 클론할 것인데, 이는
참조자의 개수를 하나에서 둘로 증가시키고 a
와 b
가 Rc<List>
안에
있는 데이터의 소유권을 공유하도록 해줍니다. 또한 c
를 만들 때도 a
를
클론할 것인데, 이로써 참조자의 개수가 둘에서 셋으로 늘어납니다. Rc::clone
가
호출될 때마다 그 Rc<List>
가 가지고 있는 데이터에 대한 참조 카운트는
증가할 것이고, 그 데이터는 참조자가 0개가 되지 않으면 메모리가 정리되지
않을 것입니다.
파일명: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); let b = Cons(3, Rc::clone(&a)); let c = Cons(4, Rc::clone(&a)); }
Rc<T>
는 프렐루드에 포함되어 있지 않으므로 이를 스코프로 가져오려면 use
구문을 추가해야 합니다. main
안에서 5와 10을 가지고 있는 리스트가 만들어지고
이것이 a
의 새로운 Rc<List>
에 저장됩니다. 그다음 b
와 c
를 만들 때는
Rc::clone
함수를 호출하고 a
의 Rc<List>
에 대한 참조자를 인수로서
넘깁니다.
Rc::clone(&a)
대신 a.clone()
을 호출할 수도 있지만, 위의 경우
러스트의 관례는 Rc::clone
를 이용하는 것입니다. Rc::clone
의 구현체는
대부분의 타입들에 대한 clone
구현체들이 그러하듯 모든 데이터에 대한 깊은
복사 (deep copy) 를 하지 않습니다. Rc::clone
의 호출은 오직 참조 카운트만
증가시키는데, 이는 시간이 얼마 걸리지 않습니다. 데이터의 깊은 복사는 많은
시간이 걸릴 수 있습니다. 참조 카운팅을 위해 Rc::clone
을 사용함으로써
깊은 복사 종류의 클론과 참조 카운트를 증가시키는 종류의 클론을 시각적으로
구별할 수 있습니다. 코드에서 성능 문제를 찾는 중이라면 깊은 복사 클론만
고려할 필요가 있고 Rc::clone
호출은 무시할 수
있습니다.
Rc<T>
를 클론하는 것은 참조 카운트를 증가시킵니다
예제 15-18의 작동하는 예제를 변경하여 a
내부의 Rc<List>
에 대한
참조자가 생성되고 버려질 때 참조 카운트가 변하는 것을 볼 수 있도록 해봅시다.
예제 15-19에서는 main
을 변경하여 안쪽의 스코프가 리스트 c
를 감싸도록 하겠습니다;
그러면 c
가 스코프 밖으로 벗어났을 때 참조 카운트가 어떻게 바뀌는지 볼 수 있습니다.
파일명: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use crate::List::{Cons, Nil}; use std::rc::Rc; fn main() { let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil))))); println!("count after creating a = {}", Rc::strong_count(&a)); let b = Cons(3, Rc::clone(&a)); println!("count after creating b = {}", Rc::strong_count(&a)); { let c = Cons(4, Rc::clone(&a)); println!("count after creating c = {}", Rc::strong_count(&a)); } println!("count after c goes out of scope = {}", Rc::strong_count(&a)); }
프로그램 내 참조 카운트가 변하는 각 지점에서 Rc::strong_count
함수를 호출하여 얻은 참조 카운트 값을 출력합니다. 이 함수가
count
가 아니고 strong_count
라는 이름이 된 이유는 Rc<T>
타입이 weak_count
도 갖고 있기 때문입니다; weak_count
가 뭘 위해서 사용되는지는
‘순환 참조 방지하기: Rc<T>
를 Weak<T>
로 바꾸기’절에서
알아보겠습니다.
이 코드는 다음을 출력합니다:
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished dev [unoptimized + debuginfo] target(s) in 0.45s
Running `target/debug/cons-list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
a
의 Rc<List>
는 초기 참조 카운트 1을 갖고 있음을 볼 수 있습니다;
그 후 clone
을 호출할 때마다 카운트가 1씩 증가합니다. c
가 스코프 밖으로
벗어날 때는 카운트가 1 감소합니다. Rc::clone
를 호출하여 참조 카운트를
증가시켜야 했던 것과 달리 참조 카운트를 감소시키기 위해 어떤 함수를
호출할 필요는 없습니다: Rc<T>
값이 스코프 밖으로 벗어나면 Drop
트레이트의 구현체가 자동으로 참조 카운트를 감소시킵니다.
main
의 끝부분에서 b
와 그다음 a
가 스코프 밖을 벗어나서, 카운트가
0이 되고, 그 시점에서 Rc<List>
가 완전히 메모리 정리되는 것은 이
예제에서 볼 수 없습니다. Rc<T>
를 이용하면 단일 값이 복수 소유자를 갖도록
할 수 있고, 그 개수는 소유자 중 누구라도 계속 존재하는 한 해당 값이 계속
유효하도록 보장해 줍니다.
Rc<T>
는 불변 참조자를 통하여 읽기 전용으로 프로그램의 여러 부분에서
데이터를 공유하도록 해줍니다. 만일 Rc<T>
가 여러 개의 가변 참조자도
만들도록 해준다면, 4장에서 논의했던 대여 규칙 중 하나를 위반할지도 모릅니다:
동일한 위치에서 여러 개의 가변 대여는 데이터 경합 및 데이터 불일치를
야기할 수 있습니다. 하지만 데이터의 변형을 가능하게 하는 것은 매우 유용하죠!
다음 절에서는 이러한 불변성 제약과 함께 동작하도록 하기 위한 내부 가변성
(interior mutability) 패턴 및 Rc<T>
와 같이 결합하여 사용할 수 있는
RefCell<T>
타입에 대해 논의하겠습니다.