`RefCell<T>`와 내부 가변성 패턴
RefCell<T>와 내부 가변성 패턴
내부 가변성(interior mutability)은 Rust의 설계 패턴으로, 데이터에 대한 불변 참조가 있음에도 그 데이터를 변경할 수 있게 해 줘요. 보통 이 동작은 빌림 규칙에 의해 금지되죠. 이 패턴은 데이터를 변경하기 위해 자료 구조 내부의 unsafe 코드를 사용해, 변경과 빌림을 다루는 Rust의 일반적인 규칙을 우회합니다. unsafe 코드는 규칙을 컴파일러가 대신 확인해 주는 것에 의존하는 대신 우리가 수동으로 확인하고 있음을 컴파일러에게 알려주는 거예요. unsafe 코드는 20장에서 더 자세히 다룰게요.
출처: The Rust Book
런타임에 빌림 규칙 강제하기 (Enforcing Borrowing Rules at Runtime)
Rc<T>와 달리 RefCell<T> 타입은 담고 있는 데이터에 대한 단일 소유권을 나타내요. 그렇다면 RefCell<T>가 Box<T> 같은 타입과 무엇이 다른 걸까요? 4장에서 배운 빌림 규칙을 다시 떠올려 볼게요.
- 어떤 순간에도 가변 참조 하나 또는 임의 개수의 불변 참조를 가질 수 있어요(둘 다는 불가능).
- 참조는 항상 유효해야 해요.
참조와 Box<T>에서는 빌림 규칙의 불변 조건이 컴파일 타임에 강제돼요. RefCell<T>에서는 이 조건이 런타임에 강제됩니다. 참조를 쓸 때 이 규칙을 어기면 컴파일러 에러가 나요. RefCell<T>를 쓸 때 이 규칙을 어기면 프로그램이 패닉(panic)하고 종료됩니다.
빌림 규칙을 컴파일 타임에 확인하는 장점은 에러가 개발 과정에서 더 일찍 잡히고, 모든 분석이 미리 완료되므로 런타임 성능에 영향이 없다는 거예요. 이런 이유로 빌림 규칙을 컴파일 타임에 확인하는 것이 대부분의 경우 최선의 선택이며, 그래서 이게 Rust의 기본 동작이에요.
대신 런타임에 빌림 규칙을 확인하는 장점은, 컴파일 타임 검사에서는 금지되었을 특정 메모리 안전 시나리오가 허용된다는 거예요. Rust 컴파일러 같은 정적 분석은 본질적으로 보수적입니다. 코드를 분석해도 탐지할 수 없는 코드 속성이 있어요. 가장 유명한 예시는 정지 문제(Halting Problem)인데, 이 책의 범위를 벗어나지만 연구해 볼 만한 흥미로운 주제입니다.
어떤 분석은 불가능하기 때문에, Rust 컴파일러가 코드가 소유권 규칙을 준수한다고 확신할 수 없으면 올바른 프로그램을 거부할 수도 있어요. 이런 점에서 보수적입니다. Rust가 잘못된 프로그램을 받아들이면 사용자들은 Rust가 보장하는 것들을 믿을 수 없게 되거든요. 반면 Rust가 올바른 프로그램을 거부하면 프로그래머가 불편할 뿐이고, 치명적인 일은 생기지 않아요. RefCell<T> 타입은 코드가 빌림 규칙을 따른다고 확신하지만 컴파일러가 그것을 이해하고 보장하지 못할 때 유용해요.
Rc<T>와 비슷하게 RefCell<T>도 단일 스레드 시나리오에서만 사용할 수 있고, 멀티스레드 맥락에서 사용하려 하면 컴파일 타임 에러가 나요. 멀티스레드 프로그램에서 RefCell<T>의 기능을 얻는 방법은 16장에서 다룰게요.
Box<T>, Rc<T>, RefCell<T> 중 무엇을 고를지에 대한 요점을 정리하면 이래요.
Rc<T>는 같은 데이터에 여러 소유자를 허용하고,Box<T>와RefCell<T>는 단일 소유자를 가져요.Box<T>는 컴파일 타임에 확인되는 불변 또는 가변 빌림을 허용하고,Rc<T>는 컴파일 타임에 확인되는 불변 빌림만 허용하며,RefCell<T>는 런타임에 확인되는 불변 또는 가변 빌림을 허용해요.RefCell<T>가 런타임에 확인되는 가변 빌림을 허용하므로,RefCell<T>자체가 불변이어도RefCell<T>안의 값을 변경할 수 있어요.
불변 값 안의 값을 변경하는 것이 바로 내부 가변성 패턴이에요. 내부 가변성이 유용한 상황을 살펴보고, 어떻게 가능한지 알아볼게요.
내부 가변성 사용하기 (Using Interior Mutability)
빌림 규칙의 결과로, 불변 값을 가지면 그것을 가변으로 빌릴 수 없어요. 예를 들어 다음 코드는 컴파일되지 않습니다.
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>를 사용하는 것은 내부 가변성을 얻는 한 가지 방법인데, RefCell<T>는 빌림 규칙을 완전히 우회하지는 않아요. 컴파일러의 빌림 검사기가 이 내부 가변성을 허용하고, 빌림 규칙은 대신 런타임에 확인됩니다. 규칙을 어기면 컴파일러 에러 대신 panic!이 나요.
RefCell<T>로 불변 값을 변경하는 실용적인 예시를 함께 작업하며 왜 유용한지 알아볼게요.
목(mock) 객체로 테스트하기 (Testing with Mock Objects)
때로는 테스트 중에 프로그래머가 특정 동작을 관찰하고 제대로 구현되었는지 단언하기 위해 어떤 타입 대신 다른 타입을 사용해요. 이 대체 타입을 테스트 더블(test double)이라고 해요. 영화 촬영에서 스턴트맨이 특히 까다로운 장면을 위해 배우를 대신해 들어서는 것처럼 생각하면 됩니다. 테스트 더블은 테스트를 실행할 때 다른 타입을 대신해요. 목 객체(mock object)는 테스트 중에 일어나는 일을 기록해서 올바른 동작이 일어났는지 단언할 수 있게 하는 특정 종류의 테스트 더블이에요.
Rust는 다른 언어들이 가진 것과 같은 의미의 객체를 가지지 않고, 일부 다른 언어가 그런 것처럼 표준 라이브러리에 목 객체 기능이 내장되어 있지도 않아요. 하지만 목 객체와 같은 목적을 제공하는 구조체를 분명히 만들 수 있습니다.
우리가 테스트할 시나리오는 이래요. 값을 최댓값과 비교해 추적하고, 현재 값이 최댓값에 얼마나 가까운지에 따라 메시지를 보내는 라이브러리를 만들 거예요. 이 라이브러리는 예를 들어 사용자가 호출할 수 있는 API 호출 횟수 쿼터를 추적하는 데 쓸 수 있어요.
우리 라이브러리는 값이 최댓값에 얼마나 가까운지 추적하고, 어떤 시점에 어떤 메시지를 보내야 하는지만 제공할 거예요. 라이브러리를 사용하는 애플리케이션은 메시지를 보내는 메커니즘을 제공해야 합니다. 애플리케이션은 메시지를 사용자에게 직접 보여주거나, 이메일을 보내거나, 문자 메시지를 보내거나, 다른 일을 할 수 있어요. 라이브러리는 그 세부사항을 알 필요가 없어요. 우리가 제공할 Messenger라는 트레이트를 구현하는 무언가만 있으면 됩니다. Listing 15-20은 라이브러리 코드를 보여줘요.
Filename: src/lib.rs
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!");
}
}
}
Listing 15-20: 값이 최댓값에 얼마나 가까운지 추적하고 값이 특정 수준에 도달하면 경고하는 라이브러리
이 코드에서 중요한 부분 하나는, Messenger 트레이트가 self에 대한 불변 참조와 메시지 텍스트를 받는 send라는 메서드를 가진다는 거예요. 이 트레이트가 우리 목 객체가 구현해야 하는 인터페이스로, 목이 실제 객체처럼 사용될 수 있게 해 줍니다. 또 다른 중요한 부분은 LimitTracker의 set_value 메서드 동작을 테스트하고 싶다는 거예요. value 파라미터에 무엇을 넣을지는 바꿀 수 있지만, set_value는 단언할 수 있는 것을 반환하지 않아요. Messenger 트레이트를 구현하는 무언가와 특정 max 값을 가진 LimitTracker를 만들면, value에 다른 숫자를 전달할 때 메신저에게 적절한 메시지를 보내라고 지시되는지를 말할 수 있게 하고 싶은 거예요.
send를 호출할 때 이메일이나 문자 메시지를 보내는 대신 보내라고 지시된 메시지만 기록하는 목 객체가 필요해요. 목 객체의 새 인스턴스를 만들고, 목 객체를 사용하는 LimitTracker를 만든 다음, LimitTracker의 set_value 메서드를 호출하고, 마지막으로 목 객체가 우리가 기대하는 메시지를 가지고 있는지 확인할 수 있어요. Listing 15-21은 바로 그런 목 객체를 구현하려는 시도인데, 빌림 검사기가 허락하지 않아요.
Filename: src/lib.rs
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);
}
}
Listing 15-21: 빌림 검사기가 허용하지 않는 MockMessenger 구현 시도
이 테스트 코드는 보내라고 지시된 메시지를 추적하기 위해 String 값의 Vec인 sent_messages 필드를 가진 MockMessenger 구조체를 정의해요. 빈 메시지 목록으로 시작하는 새 MockMessenger 값을 쉽게 만들 수 있도록 연관 함수 new도 정의하죠. 그런 다음 MockMessenger에 Messenger 트레이트를 구현해서 MockMessenger를 LimitTracker에 넘길 수 있게 해요. send 메서드 정의에서 파라미터로 전달된 메시지를 받아 MockMessenger의 sent_messages 목록에 저장합니다.
테스트에서는 LimitTracker가 max 값의 75%보다 큰 값으로 value를 설정하라고 지시받았을 때 무슨 일이 일어나는지 테스트해요. 먼저 빈 메시지 목록으로 시작하는 새 MockMessenger를 만듭니다. 그런 다음 새 LimitTracker를 만들고 새 MockMessenger에 대한 참조와 max 값 100을 넘겨줘요. LimitTracker의 set_value 메서드를 값 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를 수정해 메시지를 기록할 수 없어요. 또 에러 텍스트의 제안대로 impl 메서드와 트레이트 정의 양쪽에서 &mut self를 사용할 수도 없어요. 테스트를 위해 Messenger 트레이트를 바꾸고 싶지는 않으니까요. 대신 기존 설계에 맞게 테스트 코드가 올바르게 동작하게 만드는 방법을 찾아야 해요.
이런 상황에서 내부 가변성이 도움을 줄 수 있어요! sent_messages를 RefCell<T> 안에 저장하면 send 메서드가 sent_messages를 수정해 본 메시지를 저장할 수 있게 됩니다. Listing 15-22가 그것을 보여줘요.
Filename: src/lib.rs
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);
}
}
Listing 15-22: 바깥 값이 불변으로 간주되는 동안 내부 값을 변경하기 위해 RefCell<T> 사용하기
이제 sent_messages 필드는 Vec<String> 대신 RefCell<Vec<String>> 타입이에요. new 함수에서는 빈 벡터를 감싼 새 RefCell<Vec<String>> 인스턴스를 만듭니다.
send 메서드 구현에서 첫 번째 파라미터는 여전히 self에 대한 불변 빌림으로, 트레이트 정의와 일치해요. self.sent_messages의 RefCell<Vec<String>>에 borrow_mut을 호출해 RefCell<Vec<String>> 안의 값, 즉 벡터에 대한 가변 참조를 얻습니다. 그런 다음 벡터에 대한 가변 참조에 push를 호출해 테스트 중에 보낸 메시지를 추적할 수 있어요.
마지막으로 바꿔야 할 것은 단언이에요. 내부 벡터에 항목이 몇 개 있는지 보려면 RefCell<Vec<String>>에 borrow를 호출해 벡터에 대한 불변 참조를 얻으면 됩니다.
이제 RefCell<T>를 어떻게 쓰는지 봤으니, 어떻게 동작하는지 더 깊이 파고들어 볼게요!
런타임에 빌림 추적하기 (Tracking Borrows at Runtime)
불변 참조와 가변 참조를 만들 때 우리는 각각 &와 &mut 문법을 사용해요. RefCell<T>에서는 RefCell<T>가 가진 안전한 API에 속하는 borrow와 borrow_mut 메서드를 사용합니다. borrow 메서드는 Ref<T> 스마트 포인터 타입을 반환하고, borrow_mut은 RefMut<T> 스마트 포인터 타입을 반환해요. 두 타입 모두 Deref를 구현하므로 일반 참조처럼 다룰 수 있습니다.
RefCell<T>는 현재 몇 개의 Ref<T>와 RefMut<T> 스마트 포인터가 활성화되어 있는지 추적해요. borrow를 호출할 때마다 RefCell<T>는 활성화된 불변 빌림의 수를 증가시킵니다. Ref<T> 값이 스코프를 벗어나면 불변 빌림의 수가 1 줄어들죠. 컴파일 타임 빌림 규칙과 똑같이, RefCell<T>는 어느 순간에도 많은 불변 빌림 또는 하나의 가변 빌림을 허용해요.
이 규칙을 어기려고 하면 참조에서처럼 컴파일러 에러를 받는 대신, RefCell<T>의 구현이 런타임에 패닉을 일으킵니다. Listing 15-23은 Listing 15-22의 send 구현을 수정한 모습이에요. 같은 스코프에 가변 빌림 두 개를 활성화하려고 의도적으로 시도해서, RefCell<T>가 런타임에 이를 막는다는 것을 보여주려는 거예요.
Filename: src/lib.rs
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);
}
}
Listing 15-23: 같은 스코프에 가변 참조 두 개를 만들어 RefCell<T>가 패닉을 일으키는지 확인하기
borrow_mut이 반환한 RefMut<T> 스마트 포인터를 one_borrow 변수에 넣어요. 그런 다음 같은 방식으로 two_borrow 변수에 또 하나의 가변 빌림을 만듭니다. 이렇게 해서 같은 스코프에 가변 참조 두 개가 생기는데, 이는 허용되지 않아요. 라이브러리의 테스트를 실행하면 Listing 15-23의 코드는 에러 없이 컴파일되지만 테스트는 실패합니다.
$ 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>를 사용하면 일반 참조가 제공하는 것보다 더 많은 기능을 얻을 수 있어요.
가변 데이터의 여러 소유자 허용하기 (Allowing Multiple Owners of Mutable Data)
RefCell<T>를 사용하는 흔한 방법은 Rc<T>와 함께 쓰는 거예요. Rc<T>는 어떤 데이터의 여러 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 제공한다는 걸 기억하세요. 만약 RefCell<T>를 담는 Rc<T>가 있다면, 여러 소유자를 가질 수 있고 변경할 수도 있는 값을 얻을 수 있어요!
예를 들어 Listing 15-18에서 Rc<T>로 여러 리스트가 다른 리스트의 소유권을 공유하게 했던 cons list 예시를 다시 떠올려 보세요. Rc<T>는 불변 값만 담기 때문에, 리스트를 만든 뒤에는 리스트 안의 어떤 값도 변경할 수 없어요. 리스트 안의 값을 변경하는 기능을 위해 RefCell<T>를 추가해 볼게요. Listing 15-24는 Cons 정의에 RefCell<T>를 사용하면 모든 리스트에 저장된 값을 수정할 수 있다는 것을 보여줘요.
Filename: src/main.rs
#[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:?}");
}
Listing 15-24: 변경할 수 있는 List를 만들기 위해 Rc<RefCell<i32>> 사용하기
Rc<RefCell<i32>>의 인스턴스인 value를 만들어 value라는 변수에 저장해 두면, 나중에 직접 접근할 수 있어요. 그런 다음 value를 담는 Cons 변형으로 a에 List를 만듭니다. a와 value 둘 다 내부의 5 값에 대한 소유권을 가지도록 value를 클론해야 해요. 소유권을 value에서 a로 옮기거나 value에서 빌리면 안 되니까요.
a 리스트를 Rc<T>로 감싸서, b와 c 리스트를 만들 때 둘 다 a를 참조할 수 있게 해요. 이는 Listing 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>는 RefCell<T>의 스레드 안전 버전이고, Mutex<T>는 16장에서 다룰게요.
더 알아보기 (Learn more)
RefCell<T>문서 —borrow,borrow_mut,Ref<T>,RefMut<T>의 전체 API를 확인해 보세요.- 16장에서
Mutex<T>와 스레드 안전한 데이터 공유 방법을 살펴보세요.