RefCell<T>와 내부 가변성 패턴
내부 가변성(interior mutability) 은 러스트의 설계 패턴으로, 어떤 데이터에 대해
불변 참조만 있어도 그 데이터 내부를 변경할 수 있게 해 줍니다. 보통 이런 동작은
대여 규칙에 의해 금지됩니다. 이 패턴은 러스트가 원래의 대여·가변성 규칙을 “조금
비틀기” 위해 자료구조 안쪽에서 unsafe 코드를 사용합니다. unsafe 코드는
“이 규칙 검사는 컴파일러가 아니라 우리가 직접 책임지고 확인하겠다”는 뜻입니다.
unsafe 코드는 20장에서 더 자세히 다룹니다.
내부 가변성 패턴을 사용하는 타입은, 컴파일러가 보장해 주지는 못하더라도 런타임에는
대여 규칙이 지켜질 것이라는 확신이 있을 때만 사용해야 합니다. 이런 unsafe 코드는
안전한 API 안에 감춰져 있으므로, 바깥에서 보이는 타입 자체는 여전히 불변입니다.
이제 내부 가변성 패턴을 따르는 RefCell<T> 타입을 통해 이 개념을 살펴보겠습니다.
런타임에 대여 규칙 강제하기
Rc<T> 와 달리 RefCell<T> 는 자신이 담고 있는 데이터에 대해 단일 소유권만을
가집니다. 그렇다면 RefCell<T> 는 Box<T> 같은 타입과 무엇이 다를까요? 4장에서
배운 대여 규칙을 떠올려 보세요.
- 어떤 시점이든, 가변 참조는 하나만 있거나, 불변 참조는 여러 개 있을 수 있다 (둘은 동시에 안 된다).
- 참조는 항상 유효해야 한다.
일반 참조나 Box<T> 에서는 이 규칙이 컴파일 시점 에 강제됩니다. 반면 RefCell<T>
에서는 이 규칙이 런타임 에 강제됩니다. 일반 참조에서 이 규칙을 어기면 컴파일 오류가
나고, RefCell<T> 에서는 런타임에 패닉이 일어나며 프로그램이 종료됩니다.
대여 규칙을 컴파일 시점에 검사하는 장점은, 오류를 개발 초기에 훨씬 빨리 잡을 수 있고, 분석이 모두 미리 끝나므로 런타임 성능에 영향이 없다는 점입니다. 이런 이유로, 대여 규칙은 대부분의 경우 컴파일 시점에 검사하는 편이 최선이고, 그래서 이것이 러스트의 기본 방식입니다.
반대로 런타임에 검사할 때의 장점은, 컴파일 시점 검사에서는 거부되었을 어떤 메모리 안전한 시나리오들이 허용된다는 점입니다. 러스트 컴파일러 같은 정적 분석기는 본질적으로 보수적입니다. 어떤 코드의 성질은 분석만으로는 판별할 수 없기 때문입니다. 가장 유명한 예가 정지 문제(Halting Problem)인데, 이 책의 범위를 벗어나지만 흥미로운 주제입니다.
어떤 분석은 원천적으로 불가능하기 때문에, 러스트 컴파일러는 코드가 소유권 규칙을
지키는지 확신할 수 없으면 사실은 올바른 프로그램조차 거부할 수 있습니다. 이런 의미에서
컴파일러는 보수적입니다. 하지만 러스트가 잘못된 프로그램을 받아들인다면, 사용자는
러스트가 보장하는 안전성을 신뢰할 수 없게 됩니다. 반대로 러스트가 올바른 프로그램을
거부하더라도 프로그래머가 조금 불편할 뿐, 치명적인 문제는 일어나지 않습니다.
RefCell<T> 는 여러분이 “이 코드는 대여 규칙을 지킨다”는 사실을 확신하지만,
컴파일러는 그 사실을 이해하고 보장하지 못할 때 유용합니다.
Rc<T> 와 마찬가지로 RefCell<T> 역시 단일 스레드 상황에서만 사용할 수 있고,
멀티스레드 문맥에서 사용하려 하면 컴파일 시점 오류가 납니다. 16장에서는 멀티스레드
프로그램에서 RefCell<T> 와 비슷한 기능을 얻는 방법을 다룹니다.
여기서 Box<T>, Rc<T>, RefCell<T> 를 선택하는 기준을 다시 정리해 보면 이렇습니다.
Rc<T>는 같은 데이터의 여러 소유자를 가능하게 하고,Box<T>와RefCell<T>는 단일 소유자만 가집니다.Box<T>는 컴파일 시점에 검사되는 불변/가변 대여를 허용하고,Rc<T>는 컴파일 시점에 검사되는 불변 대여만 허용하며,RefCell<T>는 런타임에 검사되는 불변/가변 대여를 모두 허용합니다.RefCell<T>는 런타임에 검사되는 가변 대여를 허용하기 때문에,RefCell<T>가 불변이어도 그 안의 값을 바꿀 수 있습니다.
불변 값의 내부를 바꾸는 것이 바로 내부 가변성 패턴입니다. 이제 이것이 왜 유용한지, 어떤 상황에서 사용할 수 있는지 살펴봅시다.
내부 가변성 사용하기
대여 규칙의 결과로, 불변 값을 가지고 있다면 그것을 가변으로 빌릴 수 없습니다. 예를 들어 다음 코드는 컴파일되지 않습니다.
fn main() {
let x = 5;
let y = &mut x;
}
이 코드를 컴파일하려 하면 다음과 같은 오류를 얻게 됩니다.
$ cargo run
Compiling borrowing v0.1.0 (file:///projects/borrowing)
error[E0596]: cannot borrow `x` as mutable, as it is not declared as mutable
--> src/main.rs:3:13
|
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
|
help: consider changing this to be mutable
|
2 | let mut x = 5;
| +++
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` (bin "borrowing") due to 1 previous error
그런데 어떤 값이 외부 코드에는 불변처럼 보이면서, 자기 메서드 안에서는 스스로를
변경할 수 있으면 유용한 상황이 있습니다. 값의 메서드 바깥쪽 코드에서는 그 값을
변경할 수 없어야 합니다. RefCell<T> 는 이런 내부 가변성을 얻는 한 가지 방법이지만,
대여 규칙 전체를 없애는 것은 아닙니다. 컴파일러의 대여 검사기가 이 내부 가변성
패턴을 허용하는 대신, 규칙 검사는 런타임에 이루어집니다. 규칙을 어기면 컴파일 오류가
아니라 panic! 을 얻게 됩니다.
이제 RefCell<T> 를 사용해 불변 값을 변경할 수 있는 실용적인 예를 하나 따라가며,
왜 이것이 유용한지 살펴보겠습니다.
모의 객체(mock objects)로 테스트하기
테스트를 할 때 프로그래머는 어떤 타입 대신 다른 타입을 사용하는 경우가 있습니다. 그 목적은 특정 동작을 관찰하고 그것이 제대로 구현되었는지 단언하는 데 있습니다. 이런 대체 타입을 테스트 더블(test double) 이라고 부릅니다. 영화에서 배우 대신 위험한 장면을 촬영하는 스턴트 더블과 비슷하게 생각하면 됩니다. 테스트 더블은 테스트 동안 다른 타입을 대신합니다. 그중에서도 모의 객체(mock object) 는 테스트 중 어떤 일이 일어났는지를 기록해 두고, 예상한 동작이 실제로 일어났는지를 단언할 수 있게 해 주는 구체적인 종류의 테스트 더블입니다.
러스트는 다른 언어들이 말하는 의미 그대로의 객체를 갖고 있지는 않고, 어떤 언어들처럼 표준 라이브러리에 모의 객체 기능이 내장되어 있지도 않습니다. 하지만 모의 객체와 같은 목적을 수행하는 구조체를 직접 만드는 것은 충분히 가능합니다.
우리가 테스트할 시나리오는 이렇습니다. 어떤 값이 최대값에 얼마나 가까운지를 추적하고, 현재 값이 최대에 얼마나 가까운지에 따라 메시지를 보내는 라이브러리를 하나 만들겠습니다. 예를 들어 사용자가 몇 번의 API 호출을 할 수 있는지 quota 를 추적하는 데 사용할 수도 있습니다.
우리 라이브러리는 “값이 최대에 얼마나 가까운지” 와, “어떤 시점에 어떤 메시지를 보내야
하는지” 만 제공합니다. 메시지를 실제로 보내는 수단은 라이브러리를 사용하는 애플리케이션이
제공해야 합니다. 예를 들어 애플리케이션은 메시지를 화면에 보여 줄 수도 있고, 이메일로
보낼 수도 있고, 문자로 보낼 수도 있습니다. 라이브러리는 그런 세부를 알 필요가 없습니다.
그저 Messenger 라는 이름의 트레이트를 구현한 무언가만 필요합니다. 목록 15-20이
그 라이브러리 코드를 보여 줍니다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
이 코드에서 중요한 부분 하나는, Messenger 트레이트가 self 에 대한 불변 참조와
메시지 텍스트를 받는 send 메서드 하나를 가진다는 점입니다. 바로 이 트레이트가
모의 객체가 구현해야 하는 인터페이스가 되며, 덕분에 모의 객체를 실제 객체와 같은
방식으로 사용할 수 있습니다. 다른 중요한 부분은, 우리가 LimitTracker 의
set_value 메서드 동작을 테스트하고 싶다는 점입니다. value 인수로 무엇을 넘길지는
바꿀 수 있지만, set_value 는 우리가 직접 단언할 만한 값을 아무것도 반환하지 않습니다.
우리가 원하는 것은, Messenger 트레이트를 구현한 어떤 것과 특정 max 값을 사용해
LimitTracker 를 만들었을 때, value 에 따라 적절한 메시지가 전송되었는지를 확인할
수 있는 것입니다.
따라서 우리는, 이메일이나 문자를 실제로 보내는 대신 send 호출 시 “보내라고
지시받은 메시지들”만 기록하는 모의 객체가 필요합니다. 새 모의 객체 인스턴스를 만들고,
그 모의 객체를 쓰는 LimitTracker 를 만든 뒤, LimitTracker 에 대해 set_value
를 호출하고, 마지막으로 모의 객체에 우리가 기대하는 메시지가 기록되었는지 확인할 수
있어야 합니다. 목록 15-21은 이를 구현하려는 시도를 보여 주지만, 대여 검사기가
허용하지 않습니다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockMessenger {
sent_messages: Vec<String>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.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_messages.len(), 1);
}
}
MockMessenger 구현 시도이 테스트 코드는 sent_messages 라는 필드 안에 String 벡터를 가진
MockMessenger 구조체를 정의합니다. 이 벡터는 “전송하라고 지시받은 메시지들”을
기록합니다. 또한 빈 메시지 목록으로 시작하는 새 MockMessenger 값을 쉽게 만들기
위해 new 연관 함수도 정의합니다. 그런 뒤 MockMessenger 에 Messenger
트레이트를 구현하여, LimitTracker 에 MockMessenger 를 넘길 수 있게 합니다.
send 메서드 정의에서는 인수로 받은 메시지를 sent_messages 목록에 저장합니다.
테스트에서는 LimitTracker 가 value 를 max 의 75퍼센트를 넘는 값으로 설정했을
때 어떤 일이 일어나는지 테스트합니다. 먼저 빈 메시지 목록을 가진 새
MockMessenger 를 만들고, 그것에 대한 참조와 max 값 100을 가진
LimitTracker 를 생성합니다. 그런 다음 set_value 를 80으로 호출합니다. 80은
100의 75퍼센트를 넘기 때문에, 이제 MockMessenger 가 추적 중인 메시지 목록에는
하나의 메시지가 있어야 한다고 단언합니다.
하지만 이 테스트에는 문제가 있습니다.
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
error[E0596]: cannot borrow `self.sent_messages` as mutable, as it is behind a `&` reference
--> src/lib.rs:58:13
|
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
|
help: consider changing this to be a mutable reference in the `impl` method and the `trait` definition
|
2 ~ fn send(&mut self, msg: &str);
3 | }
...
56 | impl Messenger for MockMessenger {
57 ~ fn send(&mut self, message: &str) {
|
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` (lib test) due to 1 previous error
send 메서드가 self 에 대한 불변 참조를 받기 때문에, MockMessenger 안의
메시지 목록을 수정할 수 없습니다. 또한 오류 메시지가 제안하듯 &mut self 로
바꾸는 것도 원하지 않습니다. 테스트만을 위해 Messenger 트레이트 자체를 바꾸고
싶지는 않기 때문입니다. 따라서 현재 설계를 유지하면서도 테스트 코드가 제대로
동작하게 만들 다른 방법이 필요합니다.
여기서 바로 내부 가변성이 도움이 됩니다! sent_messages 를 RefCell<T> 안에
저장하면, send 메서드가 sent_messages 를 수정해 본 메시지를 기록할 수 있게
됩니다. 목록 15-22가 그 모습입니다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages.borrow_mut().push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
// --snip--
let mock_messenger = MockMessenger::new();
let mut limit_tracker = LimitTracker::new(&mock_messenger, 100);
limit_tracker.set_value(80);
assert_eq!(mock_messenger.sent_messages.borrow().len(), 1);
}
}
RefCell<T> 사용하기이제 sent_messages 필드 타입은 Vec<String> 이 아니라 RefCell<Vec<String>>
입니다. new 함수 안에서는 빈 벡터 주위에 새 RefCell<Vec<String>> 인스턴스를
만듭니다.
send 메서드를 구현할 때, 첫 번째 매개변수는 여전히 trait 정의와 같은 불변 self
참조입니다. 우리는 self.sent_messages 안의 RefCell<Vec<String>> 에 대해
borrow_mut 를 호출해 내부 벡터에 대한 가변 참조를 얻습니다. 그런 다음 그 가변
참조에 대해 push 를 호출해 테스트 도중 전송된 메시지를 기록할 수 있습니다.
마지막으로 단언 부분도 바꿔야 합니다. 안쪽 벡터에 항목이 몇 개 있는지 확인하려면,
RefCell<Vec<String>> 에 대해 borrow 를 호출해 벡터에 대한 불변 참조를 얻습니다.
이제 RefCell<T> 를 어떻게 쓰는지 보았으니, 내부적으로 어떻게 동작하는지도
살펴보겠습니다.
런타임에 대여 추적하기
일반 불변 참조와 가변 참조를 만들 때는 각각 & 와 &mut 문법을 사용합니다.
RefCell<T> 에 대해서는 borrow 와 borrow_mut 메서드를 사용하는데, 이것들은
RefCell<T> 가 제공하는 안전한 API 일부입니다. borrow 는 Ref<T> 라는 스마트
포인터를 반환하고, borrow_mut 는 RefMut<T> 라는 스마트 포인터를 반환합니다.
두 타입 모두 Deref 를 구현하므로, 일반 참조처럼 취급할 수 있습니다.
RefCell<T> 는 현재 활성화된 Ref<T> 와 RefMut<T> 스마트 포인터 수를 추적합니다.
borrow 를 호출할 때마다 RefCell<T> 는 활성 불변 대여 수를 1 늘립니다.
Ref<T> 값이 스코프를 벗어나면 그 수는 다시 1 줄어듭니다. 컴파일 시 대여 규칙과
마찬가지로, RefCell<T> 도 어떤 시점에서든 여러 개의 불변 대여 또는 하나의 가변
대여만 허용합니다.
이 규칙을 어기려 하면, 일반 참조처럼 컴파일 오류가 나는 대신 RefCell<T> 구현이
런타임에 패닉을 일으킵니다. 목록 15-23은 목록 15-22의 send 구현을 수정해,
같은 스코프 안에서 두 개의 가변 대여를 일부러 만들려는 예입니다. 이를 통해
RefCell<T> 가 런타임에 이것을 막는다는 점을 보여 줍니다.
pub trait Messenger {
fn send(&self, msg: &str);
}
pub struct LimitTracker<'a, T: Messenger> {
messenger: &'a T,
value: usize,
max: usize,
}
impl<'a, T> LimitTracker<'a, T>
where
T: Messenger,
{
pub fn new(messenger: &'a T, max: usize) -> LimitTracker<'a, 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.75 {
self.messenger
.send("Warning: You've used up over 75% of your quota!");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
let mut one_borrow = self.sent_messages.borrow_mut();
let mut two_borrow = self.sent_messages.borrow_mut();
one_borrow.push(String::from(message));
two_borrow.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_messages.borrow().len(), 1);
}
}
RefCell<T> 가 패닉함을 보기우리는 borrow_mut 가 반환한 RefMut<T> 스마트 포인터를 one_borrow 변수에
저장합니다. 그런 다음 같은 방식으로 two_borrow 라는 두 번째 가변 대여를 만들려고
합니다. 이것은 같은 스코프 안에 두 개의 가변 참조를 만드는 것이므로 허용되지 않습니다.
하지만 이 코드는 컴파일은 되며, 테스트를 실행했을 때 실패하게 됩니다.
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.91s
Running unittests src/lib.rs (target/debug/deps/limit_tracker-e599811fa246dbde)
running 1 test
test tests::it_sends_an_over_75_percent_warning_message ... FAILED
failures:
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'tests::it_sends_an_over_75_percent_warning_message' panicked at src/lib.rs:60:53:
RefCell already borrowed
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::it_sends_an_over_75_percent_warning_message
test result: FAILED. 0 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
코드가 already borrowed: BorrowMutError 메시지와 함께 패닉했다는 점에 주목하세요.
이것이 바로 RefCell<T> 가 런타임에 대여 규칙 위반을 처리하는 방식입니다.
이처럼 컴파일 시가 아니라 런타임에 대여 오류를 잡도록 선택하면, 개발 과정의 더 늦은
시점에서야 실수를 발견하게 될 가능성이 있습니다. 심지어 프로덕션에 배포하고 나서야
드러날 수도 있습니다. 또한 런타임에 대여 상태를 추적해야 하므로 약간의 성능 비용도
생깁니다. 하지만 RefCell<T> 를 사용하면, 오직 불변 값만 허용되는 문맥 안에서도
자기 자신을 바꾸어 “어떤 메시지를 보았는지” 를 기록하는 모의 객체를 만들 수 있습니다.
이런 트레이드오프에도 불구하고, 일반 참조보다 더 많은 기능이 필요하다면 RefCell<T>
를 쓸 가치가 있습니다.
Rc<T> 와 RefCell<T> 로 여러 소유자가 있는 가변 데이터 만들기
RefCell<T> 를 흔히 쓰는 방식 하나는 Rc<T> 와 결합하는 것입니다. Rc<T> 는
데이터에 여러 소유자가 있도록 해 주지만, 그 데이터에 대해서는 오직 불변 접근만
허용한다는 점을 기억하세요. 그런데 RefCell<T> 를 담고 있는 Rc<T> 를 사용하면,
여러 소유자를 가지면서도 값을 변경할 수 있는 구조를 만들 수 있습니다!
예를 들어 목록 15-18의 cons list 예제를 떠올려 봅시다. 그때는 Rc<T> 로 여러
리스트가 하나의 다른 리스트를 함께 소유하게 만들었습니다. 하지만 Rc<T> 는 불변
값만 담으므로, 한 번 만든 뒤 리스트 값을 바꿀 수는 없습니다. 여기에 RefCell<T>
를 더해 리스트 안의 값을 변경할 수 있게 만들어 보겠습니다. 목록 15-24는 Cons
정의 안에 RefCell<T> 를 사용하면 모든 리스트 안의 저장된 값을 바꿀 수 있음을
보여 줍니다.
#[derive(Debug)]
enum List {
Cons(Rc<RefCell<i32>>, Rc<List>),
Nil,
}
use crate::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(3)), Rc::clone(&a));
let c = Cons(Rc::new(RefCell::new(4)), Rc::clone(&a));
*value.borrow_mut() += 10;
println!("a after = {a:?}");
println!("b after = {b:?}");
println!("c after = {c:?}");
}
List 를 만들기 위해 Rc<RefCell<i32>> 사용하기우리는 Rc<RefCell<i32>> 인스턴스를 하나 만들고, 나중에 직접 접근할 수 있도록
value 라는 변수에 저장합니다. 그런 다음 a 라는 List 를 만들고, 그 안의
Cons variant 가 value 를 담도록 합니다. 이때 a 와 value 둘 다 내부의
5 값에 대한 소유권을 가져야 하므로, 소유권을 value 에서 a 로 옮기거나
a 가 value 를 빌리게 하는 대신 value 를 clone 합니다.
이후 리스트 a 를 Rc<T> 로 감싸 두었기 때문에, b 와 c 를 만들 때 둘 다
a 를 참조할 수 있습니다. 이는 목록 15-18에서 했던 것과 같습니다.
리스트 a, b, c 를 만든 뒤에는 value 안의 값에 10을 더하고 싶습니다.
이를 위해 value 에 대해 borrow_mut 를 호출합니다. 이 과정에서는 5장의
“-> 연산자는 어디에 있나요?” 절에서
다뤘던 자동 역참조 기능이 사용되어, Rc<T> 가 안쪽 RefCell<T> 값으로 자동
역참조됩니다. borrow_mut 는 RefMut<T> 스마트 포인터를 반환하고, 우리는 그
위에 역참조 연산자를 써서 내부 값을 바꿉니다.
그 뒤 a, b, c 를 출력하면, 모두 원래 5 가 아니라 수정된 값 15 를
가지고 있음을 볼 수 있습니다.
$ cargo run
Compiling cons-list v0.1.0 (file:///projects/cons-list)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.63s
Running `target/debug/cons-list`
a after = Cons(RefCell { value: 15 }, Nil)
b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
c after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
아주 멋진 기법입니다! RefCell<T> 를 사용했기 때문에, 바깥에서 보면 불변인 List
값이지만 필요할 때는 내부 가변성에 접근해 데이터를 바꿀 수 있게 됩니다. 런타임에
대여 규칙을 검사해 주므로 데이터 경쟁도 막을 수 있고, 약간의 속도를 희생하는 대신
이런 유연성을 얻을 가치가 있는 경우도 있습니다. 다만 RefCell<T> 는 멀티스레드
코드에서는 동작하지 않습니다! RefCell<T> 의 스레드 안전한 버전은 Mutex<T> 이고,
이는 16장에서 다룹니다.