이 문서는 2판 번역본입니다.
최신 2021 에디션 문서는 https://doc.rust-kr.org 에서 확인하실 수 있습니다.
Rc<T>
, 참조 카운팅 스마트 포인터
대부분의 경우에서, 소유권은 명확합니다: 여러분은 어떤 변수가 주어진 값을 소유하는지 정확히 압니다. 그러나, 하나의 값이 여러 개의 소유자를 가질 수도 있는 경우가 있습니다. 예를 들면, 그래프 데이터 구조에서, 여러 에지가 동일한 노드를 가리킬 수도 있고, 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지들에 의해 소유됩니다. 노드는 어떠한 에지도 이를 가리키지 않을 때까지는 메모리 정리가 되어서는 안됩니다.
복수 소유권을 가능하게 하기 위해서, 러스트는 Rc<T>
라 불리우는 타입을 가지고
있습니다. 이 이름은 참조 카운팅 (reference counting) 의 약자인데, 이는
어떤 값이 계속 사용되는지 혹은 그렇지 않은지를 알기 위해 해당 값에 대한 참조자의
갯수를 계속 추적하는 것입니다. 만일 값에 대한 참조자가 0개라면, 그 값은 어떠한
참조자도 무효화하지 않고 메모리 정리될 수 있습니다.
Rc<T>
를 거실의 TV로 상상해보세요. 만일 한 사람이 TV를 보러 들어온다면,
TV를 킵니다. 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다. 마지막 사람이
거실을 나선다면, TV는 더 이상 사용되지 않으므로 이를 끕니다. 만일 다른 사람들이
여전히 TV를 보고 있는 중에 누군가가 이를 끈다면, 남은 TV 시청자들로부터 엄청난
소란이 있을 것입니다!
우리 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶고, 어떤 부분이
그 데이터를 마지막에 이용하게 될지 컴파일 타임에는 알 수 없는 경우 Rc<T>
타입을 사용합니다. 만일 어떤 부분이 마지막으로 사용하는지 알 수 있다면,
우리는 그냥 그 해당 부분을 데이터의 소유자로 만들면 되고, 컴파일 타임에 집행되는
보통의 소유권 규칙이 효과를 발생시킬 것입니다.
Rc<T>
가 오직 단일 스레드 시나리오 상에서만 사용 가능하다는 점을 주의하세요.
16장에서 동시성 (concurrency) 을 논의할 때, 다중 스레드 프로그램에서는
어떻게 참조 카운팅을 하는지 다루겠습니다.
Rc<T>
를 사용하여 데이터 공유하기
Listing 15-5의 cons list 예제로 돌아가 봅시다. 우리는 Box<T>
를 이용해서
이것을 정의했던 것을 상기하세요. 이번에는 세 번째 리스트의 소유권을 둘 다 공유하는
두 개의 리스트를 만들 것인데, 이는 개념적으로 Figure 15-3과 유사하게 보일 것입니다:
우리는 5와 10을 담은 리스트 a
를 만들 것입니다. 그런 다음 두 개의 리스트를 더
만들 것입니다: 3으로 시작하는 b
와 4로 시작하는 c
입니다. 그리고 나서 b
와 c
리스트 둘 모두 5와 10을 담고 있는 첫번째 a
리스트로 계속되게 할 것입니다.
바꿔 말하면, 두 리스트 모두 5와 10을 담은 첫 리스트를 공유하게 될 것입니다.
Listing 15-17에서 보시는 것처럼, 우리가 Box<T>
를 가지고 정의한 List
를
이용하여 이 시나리오를 구현하는 시도는 작동하지 않을 것입니다:
Filename: src/main.rs
enum List {
Cons(i32, Box<List>),
Nil,
}
use 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));
}
이 코드를 컴파일하면, 다음과 같은 에러를 얻습니다:
error[E0382]: use of moved value: `a`
--> src/main.rs:13:30
|
12 | let b = Cons(3, Box::new(a));
| - value moved here
13 | let c = Cons(4, Box::new(a));
| ^ value used here after move
|
= note: move occurs because `a` has type `List`, which does not implement
the `Copy` trait
Cons
variant는 이것이 가지고 있는 데이터를 소유하므로, 우리가 b
리스트를 만들때,
a
는 b
안으로 이동되고 b
는 a
를 소유합니다. 그 뒤, c
를 생성할 때 a
를
다시 이용하는 시도를 할 경우, 이는 a
가 이동되었으므로 허용되지 않습니다.
우리는 Cons
가 대신 참조자를 갖도록 정의를 변경할 수도 있지만, 그러면
라이프타임 파라미터를 명시해야 할 것입니다. 라이프타임 파라미터를 명시함으로써,
리스트 내의 모든 요소들이 최소한 전체 리스트만큼 오래 살아있을 것입니다.
예를 들어 빌림 검사기는 라이프타임 파라미터를 명시함으로써,
let a = Cons(10, &Nil);
의 &Nil
과 같은 임시 값에 대한 참조를 사용할 수 있게
해줍니다. 그러나 경우에 따라 적합한 라이프타임 매개변수를 지정하는 것이
어렵거나 비실용적일 수 있습니다.
대신, 우리는 Listing 15-18과 같이 Box<T>
의 자리에 Rc<T>
를 이용하여
List
의 정의를 바꿀 것입니다. 각각의 Cons
variant는 이제 어떤 값과
List
를 가리키는 Rc<T>
를 갖게 될 것입니다. b
를 만들때는 a
의
소유권을 얻는 대신, a
를 가지고 있는 Rc<List>
를 클론할 것인데, 이는
참조자의 갯수를 하나에서 둘로 증가시키고 a
와 b
가 Rc<List>
안에
있는 값을 공유하게 해줍니다. 우리는 또한 c
를 만들때도 a
를 클론할
것인데, 이는 참조자의 갯수를 둘에서 셋으로 늘립니다. 우리가 Rc::clone
을
호출하는 매번마다, 해당 Rc<List>
가 가지고 있는 데이터에 대한 참조 카운트는
증가할 것이고, 그 데이터는 참조자가 0개가 되지 않으면 메모리 정리되지 않을
것입니다:
Filename: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use 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>
의 클론 생성은 참조 카운트를 증가시킵니다
Listing 15-18의 동작 예제를 변경하여 a
내부의 Rc<List>
에 대한
참조자가 생성되고 드롭될 때 참조 카운트의 변화를 볼 수 있도록 해봅시다.
Listing 15-19에서는 main
을 변경하여 리스트 c
를 감싸고 있는 내부 스코프를
갖도록 하겠습니다; 그런 다음 우리는 c
가 스코프 밖으로 벗어났을 때 참조 카운트가
어떻게 변하는지 볼 수 있습니다. 프로그램 내 참조 카운트가 변하는 각 지점에서, 우리는
참조 카운트 값을 출력할텐데, 이는 Rc::strong_count
함수를 호출함으로써 얻을
수 있습니다. 이 함수는 count
보다는 strong_count
라는 이름을 갖고 있는데
이는 Rc<T>
타입이 weak_count
도 갖고 있기 때문입니다; weak_count
가
무엇을 위해 사용되는지는 “참조 순환 (reference cycles) 방지하기”절에서 볼 것입니다.
Filename: src/main.rs
enum List { Cons(i32, Rc<List>), Nil, } use 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)); }
이 코드는 다음을 출력합니다:
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>
타입에 대해 논의할
것입니다.