이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
Rust의 소유권 규칙은 한 순간에 하나의 값에 대한 소유권은 하나의 변수만 갖도록 한다.
그런데 우리가 그래프와 같은 자료구조를 구현한다고 하자.
그래프의 노드는 그것과 연결된 녀석들에 대한 참조를 가지고 있어야 한다.
그렇다고 불변 참조를 하기엔 노드의 변경도 필요한 연산이다.
그리고 노드는 다른 노드가 자신을 참조하는 동안 절대 해제되어서는 안된다.
다행히도 Rust에는 다중 소유권을 지원하기 위한 자료형이 존재한다.
우리는 그것을 참조 카운터Reference Counter라고 하며
Rc<T>
라는 자료형을 통해 사용한다.
Rc<T>
참조 카운터 Rc<T>
는 스마트 포인터의 일종이다.
이것은 값에 대한 참조의 개수를 추적하여 그것이 존재하지 않게 되었을 때 값을 해제한다.
Rc<T>
변수는 여러 곳에서 참조할 수 있게 heap 메모리에 저장되며
소유권이 적용되는 범위는 컴파일 시간에 알 수 없다.
아직 다루지 않은 내용이지만 Rc<T>
는 멀티 쓰레드 환경에서는 사용할 수 없음을 유의하자.
Rc<T>
자료형은 다음과 같이 사용할 수 있다.
peter@hp-laptop:~/rust-practice/chapter15$ cargo new rc_list
Created binary (application) `rc_list` package
peter@hp-laptop:~/rust-practice/chapter15$ cd rc_list/
peter@hp-laptop:~/rust-practice/chapter15/rc_list$ vi src/main.rs
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)); }
b
와 c
가 Rc::clone
메서드를 통해 a
를 참조하였다.
Rc::clone
은 일반 복제와는 달리 참조 카운트만 증가시키고 깊은 복사를 하지 않는다.
이 상태에서 a
가 먼저 범위를 벗어나게 되더라도
b
또는 c
가 살아있는 한 a
가 가지고 있던 값은 해제되지 않는다.
b
와 c
가 해제되더라도 그 사이에 다른 녀석이 이 값을 참조했다면
마찬가지로 이것은 해제되지 않는다.
마지막 하나의 참조가 사라지고 나서야 해제된다.
이번에는 위 예제를 수정하여 참조 카운터 값의 변화를 확인해보자.
peter@hp-laptop:~/rust-practice/chapter15/rc_list$ vi src/main.rs
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)); }
peter@hp-laptop:~/rust-practice/chapter15/rc_list$ cargo run
Compiling rc_list v0.1.0 (/home/peter/rust-practice/chapter15/rc_list)
# snip warnings
Finished dev [unoptimized + debuginfo] target(s) in 0.26s
Running `target/debug/rc_list`
count after creating a = 1
count after creating b = 2
count after creating c = 3
count after c goes out of scope = 2
peter@hp-laptop:~/rust-practice/chapter15/rc_list$
최초로 생성되었을 땐 참조 카운터 값이 1이었지만
참조하는 변수가 증가할수록 참조 카운터 값도 증가하고
그것이 범위를 벗어나면 참조 카운터 값도 감소하는 것을 확인할 수 있다.
그리고 이 값이 0이 되었을 때 비로소 이 녀석이 해제된다.
그런데 이렇게 다중 소유권을 지원하면 대여 규칙을 위반하는 문제가 생기지 않을까?
우리는 내부 가변성 패턴을 통해 이 문제를 해결할 수 있다.
기본적으로 불변 참조로 대여한 값은 말 그대로 불변이다.
그런데 불변 참조를 사용하면서도 값을 수정할 수 있도록 할 수 있다.
물론 일반적인 경우는 아니고, 이것은 Rust의 unsafe 코드 중 하나다.
unsafe 코드는 컴파일러는 보장하지 못하지만 실행 시간에 보장되는 경우에 사용할 수 있는 코드다.
내부 가변성 패턴은 불변 API로 감싸 외부에서는 불변 속성을 유지한다.
내부 가변성 패턴을 따르는 스마트 포인터 RefCell<T>
에 대해 알아보자.
RefCell<T>
RefCell<T>
는 다중 소유권을 지원하는 Rc<T>
와 달리 단일 소유권만을 지원한다.
단일 소유권이라는 점에서 Box<T>
와 유사하다고 여겨질 수 있지만
대여 규칙을 컴파일 시간에 검사하는 Box<T>
와 달리
RefCell<T>
는 그것을 실행 시간에 검사한다.
Box<T>
를 사용했는데 대여 규칙을 위반하면 컴파일 자체가 안되지만
RefCell<T>
을 사용했을 경우 실행 오류로 패닉을 내뱉으며 종료한다.
물론 성능 측면에서는 Box<T>
와 같이 컴파일 시간에 검사하는 것이 유리하다.
그래서 대부분의 검사가 컴파일 시간에 실행되는 것이기도 하고 말이다.
그러나 실행 시간에 검사할 경우 컴파일러가 보장하지 못하는 기능을 수행할 수 있다.
실행 시간에는 안전성이 보장되지만 컴파일 시간에는 보장되지 않는 경우
컴파일 시간에 수행하는 검사에 대해서는 Rust 컴파일러는 그것을 허용하지 않으려고 하는데
이러한 정적 분석은 일부 기능 상의 제약을 야기한다.
RefCell<T>
은 사용자가 실행 시간에 올바르게 작동한다고 확신하지만
컴파일러가 이를 보장해주지 못하는 경우에 유용하다.
아직 다루지 않은 내용이지만 RefCell<T>
도 멀티 쓰레드 환경에서는 사용할 수 없음을 유의하자.
우리가 알아본 세 가지 스마트 포인터 중 한 가지를 사용하다면
다음과 같은 사항을 고려하여 결정하도록 하자.
Rc<T>
는 동일 자료에 대해 다중 소유권을 가질 수 있다; Box<T>
와 RefCell<T>
는 하나의 소유자만 존재한다.Rc<T>
enables multiple owners of the same data; Box<T>
and RefCell<T>
have single owners.Box<T>
는 컴파일 시간에 검사한 불변 또는 가변 대여를 허용한다; Rc<T>
는 컴파일 시간에 검사한 불변 대여만 허용한다; RefCell<T>
는 실행 시간에 검사한 불변 또는 가변 대여를 허용한다.Box<T>
allows immutable or mutable borrows checked at compile time; Rc<T>
allows only immutable borrows checked at compile time; RefCell<T>
allows immutable or mutable borrows checked at runtime.RefCell<T>
는 실행 시간에 가변 대여를 검사하므로 RefCell<T>
이 불변인 경우에도 RefCell<T>
내부의 값을 변경할 수 있다.RefCell<T>
allows mutable borrows checked at runtime, you can mutate the value inside the RefCell<T>
even when the RefCell<T>
is immutable.자, 그러면 이제 내부 가변성을 필요로 하는 예제를 살펴보자.
테스트를 수행할 때 시스템 밖의 무언가와 상호작용 하는 부분을 테스트해야 할 때가 있다.
이 경우 우리는 의도한 동작이 이루어지는지 확인하기 위해
비슷한 역할을 하는 녀석을 단순화하여 만들어 사용하곤 한다.
우리는 이 녀석을 모조 객체라고 부른다.
어떤 언어는 표준 라이브러리에서 모조 객체를 지원하지만 Rust는 구조체로 직접 만들어야 한다.
그리고 이 모조 객체를 구현할 때 RefCell<T>
을 사용한다.
테스트 대상과 모조 객체를 직접 작성해보자.
먼저 테스트 대상이 되는 라이브러리 코드를 작성하겠다.
peter@hp-laptop:~/rust-practice/chapter15/rc_list$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new mock_object --lib
Created library `mock_object` package
peter@hp-laptop:~/rust-practice/chapter15$ cd mock_object/
peter@hp-laptop:~/rust-practice/chapter15/mock_object$ vi src/lib.rs
src/lib.rs
pub trait Messenger { fn send(&self, msg: &str); } pub struct LimitTracker<'a, T: 'a + Messenger> { messenger: &'a T, value: usize, max: usize, } impl<'a, T> LimitTracker<'a, T> where T: Messenger { pub fn new(messenger: &T, max: usize) -> LimitTracker<T> { LimitTracker { messenger, value: 0, max, } } pub fn set_value(&mut self, value: usize) { self.value = value; let percentage_of_max = self.value as f64 / self.max as f64; if percentage_of_max >= 1.0 { self.messenger.send("Error: You are over your quota!"); } else if percentage_of_max >= 0.9 { self.messenger.send("Urgent warning: You've used up over 90% of your quota!"); } else if percentage_of_max >= 0.7 { self.messenger.send("Warning: You've used up over 75% of your quota!"); } } }
이것은 할당량이 채워지면 사용자에게 메신저로 알림을 보내는 라이브러리다.
75% 이상, 90% 이상, 100% 달성 시 각각 적절한 알림을 보낸다.
이 때, 알림을 보내는 방식은 중요하지 않다.
그저 Messenger
트레이트를 구현한 녀석이 send
메서드를 호출하면 된다.
Messenger
트레이트의 send
메서드는 self
의 불변 참조를 사용한다.
우리는 이 라이브러리의 set_value
메서드를 테스트하고자 한다.
이를 위해서는 LimitTracker
생성 시 사용할 messenger
가 필요하다.
따라서 우리는 임의로 Messenger
를 구현한 모조 객체를 만들어야 한다.
일단 RefCell<T>
없이 테스트를 작성해보고 왜 그것이 필요한지 알아본 후
그것을 사용하여 수정하도록 하겠다.
peter@hp-laptop:~/rust-practice/chapter15/mock_object$ vi src/lib.rs
src/lib.rs
# snip #[cfg(test)] mod tests { use super::*; struct MockMessenger { sent_message: Vec<String>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_message: vec![], } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_message.push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_message.len(), 1); } }
그런데 이렇게 작성하면 send
메서드 구현에서
불변참조 self
의 필드에 값을 추가하고자 한다고 오류가 발생한다.
그렇다고 send
의 self
를 가변참조로 변경하게 되면
set_value
에서 LimitTracker
를 참조할 때
원래의 LimitTracker
와 그것의 참조 모두 Messenger
의 가변참조를 가지고 있어
복수개의 가변참조로 인해 컴파일을 거부한다.
따라서 우리는 불변참조이면서도 그 값을 변경할 수 있는, 내부 가변성을 가진 녀석이 필요하다.
자, 그런 의미에서 내부 가변성을 가진 RefCell<T>
을 이용하여 코드를 수정해보자.
peter@hp-laptop:~/rust-practice/chapter15/mock_object$ vi src/lib.rs
src/lib.rs
# snip #[cfg(test)] mod tests { use super::*; use std::cell::RefCell; struct MockMessenger { sent_message: RefCell<Vec<String>>, } impl MockMessenger { fn new() -> MockMessenger { MockMessenger { sent_message: RefCell::new(vec![]), } } } impl Messenger for MockMessenger { fn send(&self, message: &str) { self.sent_message.borrow_mut().push(String::from(message)); } } #[test] fn it_sends_an_over_75_percent_warning_message() { let mock_messenger = MockMessenger::new(); let mut limit_tracker = LimitTracker::new(&mock_messenger, 100); limit_tracker.set_value(80); assert_eq!(mock_messenger.sent_message.borrow().len(), 1); } }
그저 기존의 자료를 RefCell<T>
로 묶고
가변참조가 필요할 땐 borrow_mut
메서드를,
불변참조가 필요할 땐 borrow
메서드를 사용하였다.
실행 시간에 borrow_mut
를 통해 가변참조를 하는 구간이 겹치지만 않는다면
이것은 참조와 대여에 대한 오류를 발생시키지 않는다.
borrow
는 스마트 포인터 Ref<T>
를 반환하고
borrow_mut
는 스마트 포인터 RefMut<T>
를 반환한다.
그리고 RefCell<T>
은 그들의 개수를 추적하여 대여 규칙을 만족하는지 검사한다.
Rc<T>
와 RefCell<T>
의 조합Rc<T>
안에 RefCell<T>
을 넣는다면 우리는
다중 소유권과 내부 가변성을 가진 자료를 사용할 수 있다.
실제로 이것이 RefCell<T>
을 사용하는 보편적인 방법 중 하나다.
이것을 사용하는 예제로, 앞서 작성해본 리스트에 RefCell<T>
을 적용해보자.
peter@hp-laptop:~/rust-practice/chapter15/mock_object$ cd ..
peter@hp-laptop:~/rust-practice/chapter15$ cargo new rc_refcell_list
Created binary (application) `rc_refcell_list` package
peter@hp-laptop:~/rust-practice/chapter15$ cp rc_list/src/main.rs rc_refcell_list/src/main.rs
peter@hp-laptop:~/rust-practice/chapter15$ cd rc_refcell_list/
peter@hp-laptop:~/rust-practice/chapter15/rc_refcell_list$ vi src/main.rs
src/main.rs
#[derive(Debug)] enum List { Cons(Rc<RefCell<i32>>, Rc<List>), Nil, } use List::{Cons, Nil}; use std::cell::RefCell; use std::rc::Rc; fn main() { let value = Rc::new(RefCell::new(5)); let a = Rc::new(Cons(Rc::clone(&value), Rc::new(Nil))); let b = Cons(Rc::new(RefCell::new(6)), Rc::clone(&a)); let c = Cons(Rc::new(RefCell::new(10)), Rc::clone(&a)); *value.borrow_mut() += 10; println!("a after = {:?}", a); println!("b after = {:?}", b); println!("c after = {:?}", c); }
peter@hp-laptop:~/rust-practice/chapter15/rc_refcell_list$ cargo run
Compiling rc_refcell_list v0.1.0 (/home/peter/rust-practice/chapter15/rc_refcell_list)
Finished dev [unoptimized + debuginfo] target(s) in 0.27s
Running `target/debug/rc_refcell_list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 6 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 10 }, Cons(RefCell { value: 15 }, Nil))
peter@hp-laptop:~/rust-practice/chapter15/rc_refcell_list$
자, 이제 우리는 다중 소유권을 가지면서도 값을 변경할 수 있는 자료를 다룰 수 있게 되었다.
이 포스트의 내용은 공식문서의 15장 4절
Rc<T>
, the Reference Counted Smart Pointer & 15장 5절RefCell<T>
and the Interior Mutability Pattern에 해당합니다.