소개
RefCell
이 랩에서는 Rust 의 내부 가변성 개념과 RefCell<T> 타입을 사용하여 이를 구현하는 방법을 살펴봅니다.
RefCell
이 랩에서는 Rust 의 내부 가변성 개념과 RefCell<T> 타입을 사용하여 이를 구현하는 방법을 살펴봅니다.
<T>과 내부 가변성 패턴*내부 가변성 (Interior mutability)*은 Rust 에서 해당 데이터에 대한 불변 참조가 있는 경우에도 데이터를 변경할 수 있도록 하는 디자인 패턴입니다. 일반적으로 이 작업은 차용 규칙에 의해 허용되지 않습니다. 데이터를 변경하기 위해 이 패턴은 데이터 구조 내에서 unsafe 코드를 사용하여 변경 및 차용을 관리하는 Rust 의 일반적인 규칙을 우회합니다. Unsafe 코드는 컴파일러에게 컴파일러가 규칙을 확인하도록 하는 대신 수동으로 규칙을 확인하고 있음을 나타냅니다. Unsafe 코드는 19 장에서 더 자세히 논의할 것입니다.
컴파일러가 보장할 수 없더라도 런타임에 차용 규칙이 준수될 수 있음을 확신할 수 있는 경우에만 내부 가변성 패턴을 사용하는 타입을 사용할 수 있습니다. 관련된 unsafe 코드는 안전한 API 로 래핑되며 외부 타입은 여전히 불변입니다.
내부 가변성 패턴을 따르는 RefCell<T> 타입을 살펴봄으로써 이 개념을 살펴보겠습니다.
<T>을 사용하여 런타임에 차용 규칙 적용하기Rc<T>와 달리, RefCell<T> 타입은 보유하고 있는 데이터에 대한 단일 소유권을 나타냅니다. 그렇다면 RefCell<T>이 Box<T>와 같은 타입과 다른 점은 무엇일까요? 4 장에서 배운 차용 규칙을 기억해 보세요.
참조와 Box<T>를 사용하면 차용 규칙의 불변성이 컴파일 시간에 적용됩니다. RefCell<T>을 사용하면 이러한 불변성이 런타임에 적용됩니다. 참조를 사용하는 경우 이러한 규칙을 위반하면 컴파일러 오류가 발생합니다. RefCell<T>을 사용하는 경우 이러한 규칙을 위반하면 프로그램이 패닉 상태가 되어 종료됩니다.
컴파일 시간에 차용 규칙을 확인하는 것의 장점은 개발 프로세스 초기에 오류를 발견할 수 있고, 모든 분석이 사전에 완료되므로 런타임 성능에 영향이 없다는 것입니다. 이러한 이유로 컴파일 시간에 차용 규칙을 확인하는 것이 대부분의 경우 최선의 선택이며, 이것이 Rust 의 기본값입니다.
대신 런타임에 차용 규칙을 확인하는 것의 장점은 컴파일 시간 검사에서 허용되지 않았을 특정 메모리 안전 시나리오를 허용한다는 것입니다. Rust 컴파일러와 같은 정적 분석은 본질적으로 보수적입니다. 코드의 일부 속성은 코드를 분석하여 감지할 수 없습니다. 가장 유명한 예는 이 책의 범위를 벗어나지만 연구할 가치가 있는 흥미로운 주제인 중지 문제 (Halting Problem) 입니다.
일부 분석이 불가능하기 때문에 Rust 컴파일러가 코드가 소유권 규칙을 준수하는지 확신할 수 없는 경우 올바른 프로그램을 거부할 수 있습니다. 이러한 방식으로 보수적입니다. Rust 가 잘못된 프로그램을 허용하면 사용자는 Rust 가 제공하는 보증을 신뢰할 수 없게 됩니다. 그러나 Rust 가 올바른 프로그램을 거부하면 프로그래머는 불편을 겪겠지만 치명적인 일은 발생하지 않습니다. 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>이 불변인 경우에도 RefCell<T> 내부의 값을 변경할 수 있습니다.불변 값 내부의 값을 변경하는 것이 내부 가변성 (interior mutability) 패턴입니다. 내부 가변성이 유용한 상황을 살펴보고 어떻게 가능한지 살펴보겠습니다.
차용 규칙의 결과는 불변 값을 가지고 있을 때 가변적으로 차용할 수 없다는 것입니다. 예를 들어, 이 코드는 컴파일되지 않습니다.
파일 이름: src/main.rs
fn main() {
let x = 5;
let y = &mut x;
}
이 코드를 컴파일하려고 하면 다음과 같은 오류가 발생합니다.
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
그러나 값의 메서드에서 자체적으로 변경되지만 다른 코드에는 불변으로 보이는 상황이 있습니다. 값의 메서드 외부의 코드는 값을 변경할 수 없습니다. RefCell<T>를 사용하는 것은 내부 가변성을 갖는 한 가지 방법이지만, RefCell<T>는 차용 규칙을 완전히 우회하지 않습니다. 컴파일러의 차용 검사기 (borrow checker) 는 이 내부 가변성을 허용하며, 차용 규칙은 대신 런타임에 확인됩니다. 규칙을 위반하면 컴파일러 오류 대신 panic!이 발생합니다.
RefCell<T>를 사용하여 불변 값을 변경하고 그 이유가 무엇인지 알 수 있는 실용적인 예제를 살펴보겠습니다.
때때로 프로그래머는 특정 동작을 관찰하고 올바르게 구현되었는지 확인하기 위해 다른 타입 대신에 한 타입을 사용합니다. 이 자리 표시자 타입은 *테스트 더블 (test double)*이라고 합니다. 영화 제작에서 스턴트 배우와 같은 의미로 생각해보세요. 스턴트 배우는 배우를 대신하여 특히 까다로운 장면을 수행합니다. 테스트 더블은 테스트를 실행할 때 다른 타입을 대신합니다. *모의 객체 (mock objects)*는 테스트 중에 발생하는 내용을 기록하여 올바른 작업이 수행되었는지 확인할 수 있는 특정 유형의 테스트 더블입니다.
Rust 는 다른 언어에서 객체를 사용하는 것과 같은 방식으로 객체를 가지고 있지 않으며, 다른 언어에서처럼 표준 라이브러리에 모의 객체 기능이 내장되어 있지 않습니다. 그러나 모의 객체와 동일한 목적을 수행하는 구조체를 확실히 만들 수 있습니다.
다음은 테스트할 시나리오입니다. 최대 값에 대한 값을 추적하고 현재 값이 최대 값에 얼마나 가까운지에 따라 메시지를 보내는 라이브러리를 만들 것입니다. 이 라이브러리는 예를 들어 사용자가 허용된 API 호출 횟수에 대한 할당량을 추적하는 데 사용할 수 있습니다.
저희 라이브러리는 값의 최대 값에 대한 근접성을 추적하고 어떤 시점에 어떤 메시지를 보내야 하는지에 대한 기능만 제공합니다. 저희 라이브러리를 사용하는 애플리케이션은 메시지를 보내는 메커니즘을 제공해야 합니다. 애플리케이션은 애플리케이션에 메시지를 넣거나, 이메일을 보내거나, 문자 메시지를 보내거나, 다른 작업을 수행할 수 있습니다. 라이브러리는 그 세부 사항을 알 필요가 없습니다. 필요한 것은 Messenger라는 트레이트를 구현하는 것입니다. Listing 15-20 은 라이브러리 코드를 보여줍니다.
파일 이름: src/lib.rs
pub trait Messenger {
1 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,
}
}
2 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: You're at 90% of your quota!");
} else if percentage_of_max >= 0.75 {
self.messenger
.send("Warning: You're at 75% of your quota!");
}
}
}
Listing 15-20: 값의 최대 값에 대한 근접성을 추적하고 값이 특정 수준에 도달하면 경고하는 라이브러리
이 코드의 중요한 부분 중 하나는 Messenger 트레이트가 self에 대한 불변 참조와 메시지 텍스트를 받는 send라는 메서드를 하나 가지고 있다는 것입니다 [1]. 이 트레이트는 모의 객체가 실제 객체와 동일한 방식으로 사용될 수 있도록 모의 객체가 구현해야 하는 인터페이스입니다. 또 다른 중요한 부분은 LimitTracker의 set_value 메서드의 동작을 테스트하려는 것입니다 [2]. value 매개변수에 전달하는 값을 변경할 수 있지만, set_value는 어떠한 것도 반환하지 않으므로 어떠한 단언도 할 수 없습니다. Messenger 트레이트를 구현하는 것과 max에 대한 특정 값을 사용하여 LimitTracker를 생성하면, value에 대해 다른 숫자를 전달할 때 메신저가 적절한 메시지를 보내도록 할 수 있기를 원합니다.
send를 호출할 때 이메일이나 문자 메시지를 보내는 대신, 보내도록 지시받은 메시지만 추적하는 모의 객체가 필요합니다. 모의 객체의 새 인스턴스를 생성하고, 모의 객체를 사용하는 LimitTracker를 생성하고, LimitTracker에서 set_value 메서드를 호출한 다음, 모의 객체에 예상되는 메시지가 있는지 확인할 수 있습니다. Listing 15-21 은 이를 수행하기 위해 모의 객체를 구현하려는 시도를 보여주지만, 차용 검사기는 이를 허용하지 않습니다.
파일 이름: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
1 struct MockMessenger {
2 sent_messages: Vec<String>,
}
impl MockMessenger {
3 fn new() -> MockMessenger {
MockMessenger {
sent_messages: vec![],
}
}
}
4 impl Messenger for MockMessenger {
fn send(&self, message: &str) {
5 self.sent_messages.push(String::from(message));
}
}
#[test]
6 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를 구현하려는 시도
이 테스트 코드는 sent_messages 필드에 String 값의 Vec를 사용하여 보낼 메시지를 추적하는 MockMessenger 구조체를 정의합니다 [1, 2]. 또한 빈 메시지 목록으로 시작하는 새 MockMessenger 값을 편리하게 생성하기 위해 연결된 함수 new [3]을 정의합니다. 그런 다음 MockMessenger에 LimitTracker를 제공할 수 있도록 MockMessenger에 대한 Messenger 트레이트를 구현합니다 [4]. send 메서드의 정의에서 [5] 매개변수로 전달된 메시지를 가져와 MockMessenger의 sent_messages 목록에 저장합니다.
테스트에서 LimitTracker가 value를 max 값의 75% 이상으로 설정하도록 지시받을 때 발생하는 상황을 테스트하고 있습니다 [6]. 먼저, 빈 메시지 목록으로 시작하는 새 MockMessenger를 생성합니다. 그런 다음 새 LimitTracker를 생성하고 새 MockMessenger에 대한 참조와 max 값 100을 제공합니다. LimitTracker에서 set_value 메서드를 값 80으로 호출합니다. 이는 100 의 75% 이상입니다. 그런 다음 MockMessenger가 추적하는 메시지 목록에 이제 하나의 메시지가 있어야 한다고 단언합니다.
그러나 이 테스트에는 다음과 같이 한 가지 문제가 있습니다.
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
send 메서드가 self에 대한 불변 참조를 사용하므로 메시지를 추적하기 위해 MockMessenger를 수정할 수 없습니다. 또한 오류 텍스트에서 &mut self를 사용하라는 제안을 따를 수도 없습니다. 그렇게 하면 send의 시그니처가 Messenger 트레이트 정의의 시그니처와 일치하지 않기 때문입니다 (시도해보고 어떤 오류 메시지가 나오는지 확인해 보세요).
이것은 내부 가변성이 도움이 될 수 있는 상황입니다! sent_messages를 RefCell<T> 내에 저장하면 send 메서드가 sent_messages를 수정하여 본 메시지를 저장할 수 있습니다. Listing 15-22 는 그 모습입니다.
파일 이름: src/lib.rs
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
1 sent_messages: RefCell<Vec<String>>,
}
impl MockMessenger {
fn new() -> MockMessenger {
MockMessenger {
2 sent_messages: RefCell::new(vec![]),
}
}
}
impl Messenger for MockMessenger {
fn send(&self, message: &str) {
self.sent_messages
3 .borrow_mut()
.push(String::from(message));
}
}
#[test]
fn it_sends_an_over_75_percent_warning_message() {
--snip--
assert_eq!(
4 mock_messenger.sent_messages.borrow().len(),
1
);
}
}
Listing 15-22: 외부 값은 불변으로 간주되는 동안 내부 값을 변경하기 위해 RefCell<T> 사용
sent_messages 필드는 이제 Vec<String> 대신 RefCell<Vec<String>> 타입입니다 [1]. new 함수에서 빈 벡터 [2] 주위에 새 RefCell<Vec<String>> 인스턴스를 생성합니다.
send 메서드의 구현에서 첫 번째 매개변수는 여전히 self의 불변 차용이며, 이는 트레이트 정의와 일치합니다. self.sent_messages [3]에서 RefCell<Vec<String>>에 대해 borrow_mut을 호출하여 RefCell<Vec<String>> 내부의 값, 즉 벡터에 대한 가변 참조를 가져옵니다. 그런 다음 벡터에 대한 가변 참조에서 push를 호출하여 테스트 중에 전송된 메시지를 추적할 수 있습니다.
마지막으로 변경해야 할 사항은 단언입니다. 내부 벡터에 몇 개의 항목이 있는지 확인하려면 RefCell<Vec<String>>에서 borrow를 호출하여 벡터에 대한 불변 참조를 가져옵니다 [4].
이제 RefCell<T>를 사용하는 방법을 보았으므로, 작동 방식을 자세히 살펴보겠습니다!
<T>로 차용 추적하기불변 및 가변 참조를 생성할 때 각각 & 및 &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>의 구현은 런타임에 패닉 (panic) 을 발생시킵니다. Listing 15-23 은 Listing 15-22 에서 send의 구현을 수정한 것을 보여줍니다. RefCell<T>가 런타임에 이를 방지한다는 것을 설명하기 위해 동일한 범위에서 두 개의 가변 차용을 의도적으로 생성하려고 합니다.
파일 이름: src/lib.rs
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));
}
}
Listing 15-23: RefCell<T>가 패닉을 발생시키는 것을 확인하기 위해 동일한 범위에서 두 개의 가변 참조 생성
borrow_mut에서 반환된 RefMut<T> 스마트 포인터에 대한 변수 one_borrow를 생성합니다. 그런 다음 변수 two_borrow에서 동일한 방식으로 다른 가변 차용을 생성합니다. 이렇게 하면 동일한 범위에서 두 개의 가변 참조가 생성되는데, 이는 허용되지 않습니다. 라이브러리에 대한 테스트를 실행하면 Listing 15-23 의 코드는 오류 없이 컴파일되지만 테스트는 실패합니다.
---- tests::it_sends_an_over_75_percent_warning_message stdout ----
thread 'main' panicked at 'already borrowed: BorrowMutError', src/lib.rs:60:53
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
already borrowed: BorrowMutError 메시지와 함께 코드가 패닉된 것을 확인하세요. 이것이 RefCell<T>가 런타임에 차용 규칙 위반을 처리하는 방식입니다.
여기서와 같이 컴파일 타임 대신 런타임에 차용 오류를 잡도록 선택하면 개발 프로세스 후반에 코드에서 실수를 찾을 수 있습니다. 즉, 코드가 프로덕션에 배포될 때까지 찾지 못할 수도 있습니다. 또한 코드는 런타임에 차용을 추적하는 결과로 컴파일 타임 대신 약간의 런타임 성능 저하를 겪게 됩니다. 그러나 RefCell<T>를 사용하면 불변 값만 허용되는 컨텍스트에서 사용하는 동안 본 메시지를 추적하기 위해 자체적으로 수정할 수 있는 모의 객체를 작성할 수 있습니다. 일반 참조가 제공하는 것보다 더 많은 기능을 얻기 위해 트레이드 오프에도 불구하고 RefCell<T>를 사용할 수 있습니다.
<T> 및 RefCell<T>을 사용하여 가변 데이터의 다중 소유자 허용하기RefCell<T>를 사용하는 일반적인 방법은 Rc<T>와 함께 사용하는 것입니다. Rc<T>를 사용하면 일부 데이터의 여러 소유자를 가질 수 있지만 해당 데이터에 대한 불변 액세스만 제공한다는 것을 기억하세요. RefCell<T>를 포함하는 Rc<T>가 있는 경우 여러 소유자를 가질 수 있고 변경할 수 있는 값을 얻을 수 있습니다!
예를 들어, 여러 목록이 다른 목록의 소유권을 공유할 수 있도록 Rc<T>를 사용했던 Listing 15-18 의 cons 목록 예제를 기억하세요. Rc<T>는 불변 값만 포함하므로 목록을 생성한 후에는 목록의 값을 변경할 수 없습니다. 목록의 값을 변경할 수 있는 기능을 위해 RefCell<T>를 추가해 보겠습니다. Listing 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() {
1 let value = Rc::new(RefCell::new(5));
2 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));
3 *value.borrow_mut() += 10;
println!("a after = {:?}", a);
println!("b after = {:?}", b);
println!("c after = {:?}", c);
}
Listing 15-24: Rc<RefCell<i32>>를 사용하여 변경할 수 있는 List 생성
Rc<RefCell<i32>>의 인스턴스인 값을 생성하고 나중에 직접 액세스할 수 있도록 value라는 변수에 저장합니다 [1]. 그런 다음 value [2]를 포함하는 Cons 변형을 사용하여 a에서 List를 생성합니다. value에서 a로 소유권을 이전하거나 a가 value에서 차용하는 대신, a와 value 모두 내부 5 값의 소유권을 갖도록 value를 복제해야 합니다.
Listing 15-18 에서 했던 것처럼, b와 c 목록을 생성할 때 모두 a를 참조할 수 있도록 a 목록을 Rc<T>로 래핑합니다.
a, b, c에서 목록을 생성한 후 value [3]의 값에 10 을 더하고 싶습니다. "-> 연산자는 어디에 있습니까?"에서 논의한 자동 역참조 기능을 사용하여 Rc<T>를 내부 RefCell<T> 값으로 역참조하는 value에서 borrow_mut을 호출하여 이 작업을 수행합니다. borrow_mut 메서드는 RefMut<T> 스마트 포인터를 반환하고, 역참조 연산자를 사용하여 내부 값을 변경합니다.
a, b, c를 출력하면 모두 5 대신 수정된 값 15를 갖는 것을 볼 수 있습니다.
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 장에서 Mutex<T>에 대해 논의할 것입니다.
축하합니다! RefCell