RefCell<T>
와 내부 가변성 패턴
내부 가변성 (interior mutability) 은 어떤 데이터에 대한 불변 참조자가 있을
때라도 데이터를 변경할 수 있게 해주는 러스트의 디자인 패턴입니다; 보통 이러한
동작은 대여 규칙에 의해 허용되지 않습니다. 데이터를 변경하기 위해서, 이 패턴은
데이터 구조 내에서 unsafe
(안전하지 않은) 코드를 사용하여 변경과 대여를 지배하는
러스트의 일반적인 규칙을 우회합니다. 안전하지 않은 코드는 이 규칙들을 지키고 있는지에
대한 검사를 컴파일러에게 맡기는 대신 수동으로 하는 중임을 컴파일러에게 알립니다;
안전하지 않은 코드에 대해서는 19장에서 더 알아보겠습니다.
컴파일러는 대여 규칙을 준수함을 보장할 수 없을지라도, 우리가 이를
런타임에 보장할 수 있는 경우라면 내부 가변성 패턴을 쓰는 타입을
사용할 수 있습니다. 여기에 포함된 unsafe
코드는 안전한 API로
감싸져 있고, 바깥쪽 타입은 여전히 불변입니다.
내부 가변성 패턴을 따르는 RefCell<T>
타입을 살펴보면서 이 개념을
탐구해 봅시다.
RefCell<T>
으로 런타임에 대여 규칙 집행하기
Rc<T>
와는 다르게, RefCell<T>
타입은 가지고 있는 데이터에 대한 단일 소유권을
나타냅니다. 그렇다면, Box<T>
와 같은 타입과 RefCell<T>
의 다른 부분은
무엇일까요? 4장에서 배웠던 대여 규칙을 상기해 봅시다:
- 어떠한 경우이든 간에, 하나의 가변 참조자 혹은 여러 개의 불변 참조자 중 (둘 다가 아니고) 하나만 가질 수 있습니다.
- 참조자는 항상 유효해야 합니다.
참조자와 Box<T>
를 이용할 때, 대여 규칙의 불변성은 컴파일 타임에 집행됩니다.
RefCell<T>
를 이용할 때, 이 불변성은 런타임에 집행됩니다. 참조자를 가지고서
이 규칙을 어기면 컴파일러 에러를 얻게 될 것입니다. RefCell<T>
를 가지고서
여러분이 이 규칙을 어기면, 프로그램은 panic!
을 일으키고 종료될 것입니다.
컴파일 타임의 대여 규칙 검사는 개발 과정에서 에러를 더 일찍 잡을 수 있다는 점, 그리고 이 모든 분석이 사전에 완료되기 때문에 런타임 성능에 영향이 없다는 장점이 있습니다. 이러한 이유로 컴파일 타임의 대여 규칙을 검사하는 것이 대부분의 경우에서 가장 좋은 선택이고, 이것이 러스트의 기본 설정인 이유이기도 합니다.
런타임의 대여 규칙 검사를 하면 컴파일 타임 검사에 의해서는 허용되지 않을 특정 메모리 안정성 시나리오가 허용된다는 장점이 있습니다. 러스트 컴파일러와 같은 정적 분석은 태생적으로 보수적입니다. 어떤 코드 속성은 코드 분석으로는 발견이 불가능합니다: 가장 유명한 예제로 정지 문제 (halting problem) 가 있는데, 이는 이 책의 범위를 벗어나지만 연구하기에 흥미로운 주제입니다.
몇몇 분석이 불가능하기 때문에, 러스트 컴파일러가 어떤 코드의 소유권
규칙 준수를 확신할 수 없다면, 올바른 프로그램을 거부할지도 모릅니다;
이런 식으로 컴파일러는 보수적입니다. 러스트가 올바르지 않은 프로그램을
수용한다면, 사용자들은 러스트가 보장하는 것을 신뢰할 수 없을 것입니다.
하지만, 만일 러스트가 올바른 프로그램을 거부한다면, 프로그래머는 불편하겠지만
어떠한 재앙도 일어나지 않을 수 있습니다. 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>
내부의 값을 변경할 수 있습니다.
불변값 내부의 값을 변경하는 것이 내부 가변성 패턴입니다. 내부 가변성이 유용한 경우를 살펴보고 이것이 어떻게 가능한지 조사해 봅시다.
내부 가변성: 불변값에 대한 가변 대여
대여 규칙의 결과로 불변값을 가지고 있을 때 이걸 가변으로 빌려올 수는 없습니다. 예를 들면, 다음 코드는 컴파일되지 않을 것입니다:
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
|
2 | let x = 5;
| - help: consider changing this to be mutable: `mut x`
3 | let y = &mut x;
| ^^^^^^ cannot borrow as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `borrowing` due to previous error
하지만, 어떤 값이 자신의 메서드 내부에서는 변경되지만 다른 코드에서는
불변으로 보이게 하는 것이 유용한 경우가 있습니다. 그 값의 메서드 바깥쪽
코드에서는 값을 변경할 수 없을 것입니다. RefCell<T>
을 이용하는 것이
내부 가변성의 기능을 얻는 한 가지 방법이지만, RefCell<T>
이 대여
규칙을 완벽하게 피하는 것은 아닙니다: 컴파일러의 대여 검사기는
이러한 내부 가변성을 허용하고, 대신 대여 규칙은 런타임에 검사 됩니다.
만일 이 규칙을 위반하면, 컴파일러 에러 대신 panic!
을 얻을
것입니다.
RefCell<T>
를 이용하여 불변값을 변경할 수 있는 실질적인 예제를
실습해보고 이것이 왜 유용한지를 알아봅시다.
내부 가변성에 대한 용례: 목 객체
테스트 중 종종 프로그래머는 어떤 타입 대신 다른 타입을 사용하게 되는데, 이를 통해 특정 동작을 관측하고 정확하게 구현되었음을 단언하기 위한 것입니다. 이러한 자리 표시형 타입을 테스트 더블 (test double) 이라고 합니다. 영화 제작에서 ‘스턴트 더블 (stunt double)’이라고 부르는, 어떤 사람이 나서서 배우를 대신해 특정한 어려운 장면을 수행하는 것과 같은 의미로 생각하시면 됩니다. 테스트 더블은 테스트를 수행할 때 다른 타입 대신 나서는 것이죠. 목 객체 (mock object) 는 테스트 더블의 특정한 형태로서 테스트 중 어떤 일이 일어났는지 기록하여 정확한 동작이 일어났음을 단언할 수 있도록 해줍니다.
러스트에는 다른 언어들에서의 객체와 동일한 의미의 객체가 없고, 러스트에는 몇몇 다른 언어들처럼 표준 라이브러리에 미리 만들어진 목 객체 기능이 없습니다. 하지만, 당연하게도 목 객체로서 동일한 목적을 제공할 구조체를 만들 수 있습니다.
여기서 테스트하려는 시나리오는 다음과 같습니다: 최댓값을 기준으로 어떤 값을 추적하여 현재 값이 최댓값에 얼마나 근접했는지에 대한 메시지를 전송하는 라이브러리를 만들려고 합니다. 이를테면 이 라이브러리는 한 명의 사용자에게 허용되고 있는 API 호출 수의 허용량을 추적하는 데 사용될 수 있습니다.
우리의 라이브러리는 어떤 값이 최댓값에 얼마나 근접했는지를 추적하고 어떤 메시지를
언제 보내야 할지에 대한 기능만 제공할 것입니다. 이 라이브러리를 사용하는 애플리케이션이
메시지를 전송하는 것에 대한 메커니즘을 제공할 예정입니다: 이 애플리케이션은 메시지를
애플리케이션 내에 집어넣거나, 이메일을 보내거나, 문자 메시지를 보내거나, 혹은 그
밖의 것들을 할 수 있습니다. 라이브러리는 그런 자세한 사항을 알 필요가 없습니다.
필요한 모든 것은 우리가 제공하게 될 Messenger
라는 이름의 트레이트를 구현하는 것입니다.
예제 15-20은 라이브러리 코드를 보여줍니다:
파일명: 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!");
}
}
}
이 코드에서 한 가지 중요한 부분은 Messenger
트레이트가 self
에 대한
불변 참조자와 메시지의 텍스트를 인수로 갖는 send
라는 메서드 하나를 갖고
있다는 것입니다. 이 트레이트는 목 객체가 실제 오브젝트와 동일한 방식으로 사용될
수 있도록 하기 위해 구현해야 하는 인터페이스입니다. 그 외에 중요한
부분은 LimitTracker
상의 set_value
메서드의 동작을 테스트해야
한다는 점입니다. value
매개변수에 어떤 것을 넘길지 바꿀 수는
있지만, set_value
는 단언에 필요한 어떤 것도 반환하지 않습니다.
Messenger
트레이트를 구현한 어떤 것과 max
에 대한 특정 값과 함께
LimitTracker
를 만든다면, value
에 대해 다른 숫자들을 넘겼을 때
메신저가 적합한 메시지를 보냈다고 말할 수 있길 원하는 것이죠.
send
를 호출했을 때 메일이나 텍스트 메시지를 보내는 대신, 보냈다고
언급하는 메시지만 추적할 목 객체가 필요합니다. 목 객체의 새 인스턴스를
생성하고, 이 목 객체를 사용하는 LimitTracker
를 만들고, LimitTracker
의
set_value
메서드를 호출한 다음, 목 객체가 예상한 메시지를 가지고
있는지 검사할 수 있겠습니다. 예제 15-21이 바로 이런 일을 하기 위한
목 객체 구현 시도이지만, 대여 검사기가 이를 허용하지 않을 것입니다:
파일명: 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);
}
}
이 테스트 코드는 보냈다고 알려주는 메시지를 추적하기 위한 String
값의 Vec
인 sent_messages
필드를 갖는 MockMessenger
구조체를
정의합니다. 또한 연관 함수 new
를 정의하여 편리하게 빈 메시지 리스트로
시작하는 새로운 MockMessenger
값을 생성할 수 있도록 합니다.
그런 다음에는 MockMessenger
에 대한 Messenger
트레이트를 구현하여
MockMessenger
를 LimitTracker
에 넘겨줄 수 있도록 하였습니다.
send
메서드의 정의 부분에서는 매개변수로 넘겨진 메시지를 가져와서
MockMessenger
내의 sent_messages
리스트에 저장합니다.
테스트 내에서는 LimitTracker
의 value
에 max
값의 75퍼센트 이상인
어떤 값이 설정되었다 했을 때 무슨 일이 일어나는지 테스트하고 있습니다. 먼저
새로운 MockMessenger
를 만드는데, 이는 빈 메시지 리스트로 시작될 것입니다.
그 다음 새로운 LimitTracker
를 만들고 여기에 새로운 MockMessenger
의
참조자와 max
값 100을 매개변수로 넘깁니다. LimitTracker
의 set_value
메서드를 80 값으로 호출하였는데, 이는 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
|
2 | fn send(&self, msg: &str);
| ----- help: consider changing that to be a mutable reference: `&mut self`
...
58 | self.sent_messages.push(String::from(message));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `self` is a `&` reference, so the data it refers to cannot be borrowed as mutable
For more information about this error, try `rustc --explain E0596`.
error: could not compile `limit-tracker` due to previous error
warning: build failed, waiting for other jobs to finish...
메시지를 추적하기 위해서 MockMessenger
를 수정할 수가 없는데, 그 이유는
send
메서드가 self
의 불변 참조자를 가져오기 때문입니다. 또한 에러
메시지가 제안하는 &mut self
를 대신 사용하라는 것도 받아들일 수 없는데, 그렇게
되면 send
의 시그니처가 Messenger
트레이트의 정의에 있는 시그니처와 맞지
않게 될 것이기 때문입니다. (편하게 한번 시도해 보고 어떤 에러가 나오는지 보세요.)
지금이 내부 가변성의 도움을 받을 수 있는 상황입니다! sent_messages
가
RefCell<T>
내에 저장되게 하면, send
메서드는 sent_message
를
수정하여 우리에게 보이는 메시지를 저장할 수 있게 될 것입니다. 예제 15-22는
이것이 어떤 형태인지를 보여줍니다:
파일명: 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() {
// --생략--
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);
}
}
sent_message
필드는 이제 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>
가 어떻게 동작하는지 보았으니, 어떻게 동작하는지 파봅시다!
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>
는
불변 참조자가 활성화된 개수를 증가시킵니다. Ref<T>
값이 스코프 밖으로 벗어날
때는 불변 대여의 개수가 하나 감소합니다. 컴파일 타임에서의 대여 규칙과 똑같이,
RefCell<T>
는 어떤 시점에서든 여러 개의 불변 대여 혹은 하나의 가변 대여를
가질 수 있도록 만들어 줍니다.
만일 이 규칙들을 위반한다면, RefCell<T>
의 구현체는 참조자에 대해 그렇게
했을 때처럼 컴파일 에러를 내는 것이 아니라, 런타임에 panic!
을 일으킬
것입니다. 예제 15-23은 예제 15-22의 send
구현을 수정한
것입니다. 고의로 같은 스코프에서 두 개의 가변 대여를 만드는 시도를 하여
RefCell<T>
가 이렇게 하는 것을 런타임에 방지한다는 것을 보여주고
있습니다.
파일명: 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);
}
}
borrow_mut
로부터 반환되는 RefMut<T>
스마트 포인터를 위한 one_borrow
변수가 만들어졌습니다. 그런 다음 또 다른 가변 대여를 같은 방식으로 two_borrow
변수에
만들어 넣었습니다. 이는 같은 스코프에 두 개의 가변 참조자를 만드는 것이고, 허용되지
않습니다. 라이브러리를 위한 테스트를 실행하면 예제 15-23의 코드는 어떠한
에러 없이 컴파일되겠지만, 테스트는 실패할 것입니다:
$ cargo test
Compiling limit-tracker v0.1.0 (file:///projects/limit-tracker)
Finished test [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 'already borrowed: BorrowMutError', src/lib.rs:60:53
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에서 Rc<T>
를 사용하여 여러 개의 리스트가
어떤 리스트의 소유권을 공유하도록 해준 콘스 리스트 예제를 상기해 보세요.
Rc<T>
가 오직 불변의 값만을 가질 수 있기 때문에, 일단 이것들을 만들면
리스트 안의 값들을 변경하는 것은 불가능했습니다. RefCell<T>
를 추가하여
이 리스트 안의 값을 변경하는 능력을 얻어봅시다. 예제 15-24는 Cons
정의
내에 RefCell<T>
를 사용하여 모든 리스트 내에 저장된 값이 변경될 수
있음을 보여줍니다:
파일명: 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); }
먼저 Rc<RefCell<i32>>
의 인스턴스 값을 생성하고 value
라는 이름의 변수
안에 저장하여 나중에 이를 직접 접근할 수 있게 했습니다. 그다음 value
를
가지고 있는 Cons
배리언트로 List
를 생성하여 a
에 넣었습니다. value
는
클론되어 value
가 가진 내부의 값 5
값에 대한 소유권이 a
로 이동되거나
a
가 value
로부터 빌려오는 것이 아니라 a
와 value
모두가 이 값에 대한
소유권을 갖도록 할 필요가 있습니다.
리스트 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 [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>
의 스레드 안전 버전이고, 이는 16장에서
다루겠습니다.