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처럼 생겼습니다:

Two lists that share ownership of a third list

그림 15-3: 세 번째 리스트 a의 소유권을 공유하는 두 리스트 bc

먼저 5와 10을 담은 리스트 a를 만들겠습니다. 그런 다음 두 개의 리스트를 더 만들 것입니다: 3으로 시작하는 b와 4로 시작하는 c를 말이죠. 그리고서 bc 리스트 둘 모두 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));
}

예제 15-17: Box<T>를 이용한 두 리스트가 세 번째 리스트에 대한 소유권을 공유하는 시도는 허용되지 않음을 보이는 예

이 코드를 컴파일하면 다음과 같은 에러를 얻습니다:

$ 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 리스트를 만들 때 ab 안으로 이동되어 b의 소유가 됩니다. 그다음 c를 생성할 때 a를 다시 사용하려 할 경우는 허용되지 않는데, 이미 a가 이동되었기 때문입니다.

Cons의 정의를 변경하여 참조자를 대신 들고 있도록 할 수도 있지만, 그러면 라이프타임 매개변수를 명시해야 할 것입니다. 라이프타임 매개변수를 명시함으로써, 리스트 내의 모든 요소가 최소한 전체 리스트만큼 오래 살아있도록 지정할 것입니다. 이는 예제 15-17의 요소와 리스트에 대한 경우지, 모든 시나리오에 맞는 것은 아닙니다.

그 대신 예제 15-18과 같이 Box<T>의 자리에 Rc<T>를 이용하는 형태로 List의 정의를 바꾸겠습니다. 각각의 Cons 배리언트는 이제 어떤 값과 List를 가리키는 Rc<T>를 갖게 될 것입니다. b를 만들 때는 a의 소유권을 얻는 대신, a를 가지고 있는 Rc<List>를 클론할 것인데, 이는 참조자의 개수를 하나에서 둘로 증가시키고 abRc<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));
}

예제 15-18: Rc<T>를 이용하는 List 정의

Rc<T>는 프렐루드에 포함되어 있지 않으므로 이를 스코프로 가져오려면 use 구문을 추가해야 합니다. main 안에서 5와 10을 가지고 있는 리스트가 만들어지고 이것이 a의 새로운 Rc<List>에 저장됩니다. 그다음 bc를 만들 때는 Rc::clone 함수를 호출하고 aRc<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));
}

예제 15-19: Printing the reference count

프로그램 내 참조 카운트가 변하는 각 지점에서 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

aRc<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> 타입에 대해 논의하겠습니다.