`Rc<T>`, 참조 카운팅 스마트 포인터

Rc<T>, 참조 카운팅 스마트 포인터

대부분의 경우 소유권은 분명해요. 어떤 변수가 주어진 값을 소유하는지 정확히 알 수 있죠. 하지만 하나의 값에 여러 소유자가 있을 수 있는 경우도 있어요. 예를 들어 그래프 자료 구조에서 여러 간선(edge)이 같은 노드(node)를 가리킬 수 있는데, 그 노드는 개념적으로 그 노드를 가리키는 모든 간선이 공동으로 소유합니다. 아무 간선도 가리키지 않아 소유자가 없게 되기 전까지는 노드를 정리해서는 안 되겠죠.

출처: The Rust Book

여러 소유자 활성화하기 (Enabling Multiple Ownership)

여러 소유권은 Rust의 Rc<T> 타입(참조 카운팅의 약자)을 명시적으로 사용해야 활성화할 수 있어요. Rc<T> 타입은 값에 대한 참조 수를 추적해서 그 값이 여전히 사용 중인지 판단합니다. 값에 대한 참조가 0개라면, 어떤 참조도 무효화되지 않는 채 값이 정리될 수 있어요.

Rc<T>를 거실의 TV라고 상상해 보세요. 한 사람이 TV를 보려고 들어오면 TV를 켭니다. 다른 사람들도 방에 들어와 TV를 볼 수 있어요. 마지막 사람이 방을 나가면, 더 이상 사용되지 않으므로 TV를 끕니다. 다른 사람들이 아직 보고 있는데 누군가 TV를 끄면, 남아 있는 시청자들이 들고 일어나겠죠!

Rc<T> 타입은 프로그램의 여러 부분이 읽기 위해 어떤 데이터를 힙에 할당하고 싶은데, 어떤 부분이 마지막으로 사용을 끝낼지 컴파일 타임에 알 수 없을 때 사용해요. 어떤 부분이 마지막에 끝날지 안다면 그 부분을 데이터의 소유자로 만들면 되고, 컴파일 타임에 강제되는 일반 소유권 규칙이 적용되니까요.

참고로 Rc<T>는 단일 스레드 시나리오에서만 사용하세요. 16장에서 동시성을 다룰 때, 멀티스레드 프로그램에서 참조 카운팅을 하는 방법을 배울 거예요.

데이터 공유하기 (Sharing Data)

Listing 15-5의 cons list 예시로 돌아가 볼게요. 그때는 Box<T>로 정의했었죠. 이번에는 세 번째 리스트의 소유권을 공유하는 리스트 두 개를 만들어 볼게요. 개념적으로 이는 Figure 15-3과 비슷해요.

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

510을 담는 리스트 a를 만들 거예요. 그런 다음 두 리스트를 더 만드는데, 3으로 시작하는 b4로 시작하는 c요. bc 리스트 모두 그다음으로 510을 담는 첫 번째 a 리스트로 이어집니다. 다시 말해 두 리스트 모두 510을 담는 첫 번째 리스트를 공유해요.

이 시나리오를 Box<T>로 정의한 List로 구현하려 하면 Listing 15-17처럼 동작하지 않아요.

Filename: 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));
}

Listing 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
   |
note: if `List` implemented `Clone`, you could clone the value
  --> src/main.rs:1:1
   |
 1 | enum List {
   | ^^^^^^^^^ consider implementing `Clone` for this type
...
10 |     let b = Cons(3, Box::new(a));
   |                              - you could clone this value

For more information about this error, try `rustc --explain E0382`.
error: could not compile `cons-list` (bin "cons-list") due to 1 previous error

Cons 변형(variant)은 담고 있는 데이터를 소유하기 때문에, b 리스트를 만들 때 ab로 이동하고 ba를 소유하게 돼요. 그런 다음 c를 만들 때 a를 다시 사용하려고 하면, a는 이미 이동되었으므로 허용되지 않습니다.

Cons의 정의를 참조를 담도록 바꿀 수도 있지만, 그러면 라이프타임 파라미터를 지정해야 해요. 라이프타임 파라미터를 지정하면 리스트의 모든 요소가 전체 리스트만큼 최소한은 살아 있도록 지정하는 셈이에요. Listing 15-17의 요소와 리스트에는 해당하는 말이지만, 모든 시나리오에서 그런 건 아닙니다.

대신 Listing 15-18처럼 List 정의를 Box<T> 대신 Rc<T>를 사용하도록 바꿀게요. 이제 각 Cons 변형은 값과 List를 가리키는 Rc<T> 하나를 담아요. b를 만들 때 a의 소유권을 가져오는 대신, a가 담고 있는 Rc<List>를 클론해서 참조 수를 1에서 2로 늘리고 ab가 그 Rc<List> 안의 데이터 소유권을 공유하게 합니다. c를 만들 때도 a를 클론해 참조 수를 2에서 3으로 늘리고, Rc::clone을 호출할 때마다 Rc<List> 안의 데이터에 대한 참조 수가 증가하며, 참조가 0개가 되기 전까지는 데이터가 정리되지 않아요.

Filename: 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));
}

Listing 15-18: Rc<T>를 사용하는 List 정의

Rc<T>는 prelude에 없기 때문에 스코프로 가져오는 use 문을 추가해야 해요. main에서 510을 담는 리스트를 만들고 a의 새로운 Rc<List>에 저장해요. 그런 다음 bc를 만들 때 Rc::clone 함수를 호출해 aRc<List>에 대한 참조를 인자로 전달합니다.

Rc::clone(&a) 대신 a.clone()을 호출할 수도 있었지만, Rust의 관례는 이 경우 Rc::clone을 쓰는 거예요. Rc::clone의 구현은 대부분의 타입에서 clone 구현이 하는 것처럼 모든 데이터를 깊은 복사해 오지 않아요. Rc::clone 호출은 참조 수만 증가시킬 뿐이라 시간이 거의 걸리지 않습니다. 데이터의 깊은 복사는 시간이 많이 걸릴 수 있죠. 참조 카운팅에 Rc::clone을 쓰면, 깊은 복사 종류의 클론과 참조 수를 증가시키는 종류의 클론을 시각적으로 구분할 수 있어요. 코드에서 성능 문제를 찾을 때 깊은 복사 클론만 고려하면 되고 Rc::clone 호출은 무시해도 됩니다.

참조 수를 늘리는 클론 (Cloning to Increase the Reference Count)

Listing 15-18의 동작하는 예시를 바꿔서, aRc<List>에 대한 참조를 만들고 드롭하면서 참조 수가 어떻게 변하는지 볼게요.

Listing 15-19에서는 c 리스트 주위에 내부 스코프를 두도록 main을 바꿉니다. 그러면 c가 스코프를 벗어날 때 참조 수가 어떻게 변하는지 볼 수 있어요.

Filename: src/main.rs

enum List {
    Cons(i32, Rc<List>),
    Nil,
}

use crate::List::{Cons, Nil};
use std::rc::Rc;

// --snip--

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

Listing 15-19: 참조 수 출력하기

참조 수가 변하는 프로그램의 각 지점에서, Rc::strong_count 함수를 호출해 얻은 참조 수를 출력해요. 이 함수 이름이 count가 아니라 strong_count인 이유는 Rc<T> 타입에 weak_count도 있기 때문이에요. weak_count가 무엇에 쓰이는지는 "Weak<T>로 참조 순환 막기"에서 볼게요.

이 코드는 다음을 출력합니다.

$ cargo run
   Compiling cons-list v0.1.0 (file:///projects/cons-list)
    Finished `dev` profile [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을 호출해야 하는 것처럼 참조 수를 줄이는 함수를 호출할 필요는 없어요. Drop 트레이트의 구현이 Rc<T> 값이 스코프를 벗어날 때 참조 수를 자동으로 줄여 주기 때문이에요.

이 예시에서 볼 수 없는 것은, main 끝에서 ba가 스코프를 벗어날 때 수가 0이 되고 Rc<List>가 완전히 정리된다는 점이에요. Rc<T>를 사용하면 하나의 값에 여러 소유자가 생길 수 있고, 그 수 덕분에 소유자 중 하나라도 존재하는 한 값이 유효하게 유지됩니다.

불변 참조를 통해 Rc<T>는 프로그램의 여러 부분이 읽기 전용으로 데이터를 공유하게 해 줘요. 만약 Rc<T>가 여러 가변 참조도 허용한다면, 4장에서 다룬 빌림 규칙 중 하나를 어길 수 있어요. 같은 위치에 대한 여러 가변 빌림은 데이터 레이스와 불일치를 일으킬 수 있으니까요. 하지만 데이터를 변경할 수 있다는 것은 매우 유용해요! 다음 절에서 불변성 제한을 우회하기 위해 Rc<T>와 함께 사용할 수 있는 내부 가변성 패턴과 RefCell<T> 타입을 다룰게요.

더 알아보기 (Learn more)

  • Rc<T> 문서Rc::strong_count, Rc::weak_count, Rc::downgrade 등 전체 API를 확인해 보세요.
  • 15장의 다음 절에서 RefCell<T>와 결합해 내부 가변성을 얻는 방법을 살펴보세요.