RefCell<T>와 내부 가변성 패턴

**내부 가변성(Interior Mutability)**은 러스트의 디자인 패턴 중 하나로, 데이터에 대한 불변 참조가 존재할 때도 데이터를 변경할 수 있게 해준다. 일반적으로 이러한 동작은 러스트의 빌림 규칙에 의해 금지된다. 이 패턴은 데이터 구조 내부에 unsafe 코드를 사용해 러스트의 일반적인 뮤테이션과 빌림 규칙을 유연하게 적용한다. unsafe 코드는 컴파일러에게 우리가 직접 규칙을 확인하고 있음을 알려주며, 컴파일러가 이를 대신 확인하지 않도록 한다. unsafe 코드에 대해서는 20장에서 더 자세히 다룰 것이다.

내부 가변성 패턴을 사용하는 타입은 컴파일러가 보장할 수 없더라도 런타임에 빌림 규칙이 지켜질 것임을 확신할 수 있을 때만 사용할 수 있다. 이때 관련된 unsafe 코드는 안전한 API로 감싸지며, 외부 타입은 여전히 불변성을 유지한다.

이 개념을 이해하기 위해 내부 가변성 패턴을 따르는 RefCell<T> 타입을 살펴보자.

RefCell<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>가 불변일 때도 내부 값을 변경할 수 있다.

불변 값 내부의 값을 변경하는 것을 내부 가변성(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 객체

테스트를 진행할 때 프로그래머는 특정 동작을 관찰하고 구현이 올바른지 확인하기 위해 한 타입을 다른 타입으로 대체할 수 있다. 이 대체 타입을 **테스트 더블(test double)**이라고 한다. 영화 촬영에서 스턴트 더블이 배우를 대신해 어려운 장면을 수행하는 것과 비슷하다고 생각하면 된다. 테스트 더블은 테스트를 실행할 때 다른 타입을 대신한다. Mock 객체는 테스트 중에 발생한 일을 기록해 올바른 동작이 수행되었는지 확인할 수 있도록 하는 특수한 테스트 더블이다.

Rust는 다른 언어와 같은 의미의 객체를 가지고 있지 않으며, 표준 라이브러리에 Mock 객체 기능이 내장되어 있지 않다. 하지만 Mock 객체와 동일한 목적을 수행할 수 있는 구조체를 직접 만들 수 있다.

테스트할 시나리오는 다음과 같다: 최댓값과 현재 값을 비교해 얼마나 가까운지 추적하고, 그에 따라 메시지를 보내는 라이브러리를 만든다. 이 라이브러리는 사용자가 허용된 API 호출 횟수를 추적하는 등 다양한 용도로 활용할 수 있다.

우리가 만드는 라이브러리는 최댓값에 얼마나 가까운지 추적하고, 특정 시점에 어떤 메시지를 보내야 하는지 결정하는 기능만 제공한다. 이 라이브러리를 사용하는 애플리케이션은 메시지를 보내는 방식을 직접 제공해야 한다. 애플리케이션은 메시지를 화면에 표시하거나, 이메일을 보내거나, 문자 메시지를 보내는 등 다양한 방식으로 메시지를 전달할 수 있다. 라이브러리는 이러한 세부 사항을 알 필요가 없다. 단지 우리가 제공할 Messenger라는 트레이트를 구현한 무언가만 필요하다. 다음은 라이브러리 코드이다.

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 메서드를 가지고 있다는 점이다. 이 트레이트는 Mock 객체가 실제 객체와 동일한 방식으로 사용될 수 있도록 구현해야 하는 인터페이스이다. 또 다른 중요한 부분은 LimitTrackerset_value 메서드의 동작을 테스트하려 한다는 점이다. value 매개변수에 전달하는 값을 변경할 수 있지만, set_value는 우리가 확인할 수 있는 값을 반환하지 않는다. 우리는 Messenger 트레이트를 구현한 객체와 특정 max 값을 가진 LimitTracker를 생성한 후, value에 다른 숫자를 전달했을 때 메신저가 적절한 메시지를 보내도록 하는지 확인하고 싶다.

우리는 send를 호출할 때 이메일이나 문자 메시지를 보내는 대신, 전달받은 메시지를 기록만 하는 Mock 객체가 필요하다. Mock 객체의 새 인스턴스를 생성하고, 이 Mock 객체를 사용하는 LimitTracker를 만든 후, LimitTrackerset_value 메서드를 호출하고, Mock 객체가 예상한 메시지를 가지고 있는지 확인할 수 있다. 다음은 이를 구현하려는 시도이지만, 빌림 검사기가 이를 허용하지 않는다.

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 구현 시도

이 테스트 코드는 MockMessenger 구조체를 정의한다. 이 구조체는 전달받은 메시지를 추적하기 위해 String 값의 Vec을 가진 sent_messages 필드를 가지고 있다. 또한, 빈 메시지 목록으로 시작하는 MockMessenger 값을 쉽게 생성할 수 있도록 new 연관 함수를 정의한다. 그리고 MockMessengerMessenger 트레이트를 구현해 LimitTrackerMockMessenger를 전달할 수 있게 한다. send 메서드의 정의에서는 매개변수로 전달된 메시지를 MockMessengersent_messages 목록에 저장한다.

테스트에서는 LimitTrackermax 값의 75% 이상인 value를 설정하도록 요청했을 때 어떤 일이 발생하는지 확인한다. 먼저, 빈 메시지 목록으로 시작하는 새로운 MockMessenger를 생성한다. 그런 다음, 새로운 LimitTracker를 만들고, 새로운 MockMessenger에 대한 참조와 max 값으로 100을 전달한다. LimitTrackerset_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 메서드와 trait 정의 모두에서 &mut self를 사용할 수도 없다. 테스트를 위해서만 Messenger 트레이트를 변경하고 싶지 않다. 대신, 기존 설계와 함께 테스트 코드가 올바르게 작동하도록 하는 방법을 찾아야 한다.

이런 상황에서 **내부 가변성(interior mutability)**이 도움이 될 수 있다! sent_messagesRefCell<T> 안에 저장하면, send 메서드가 sent_messages를 수정해 우리가 본 메시지를 저장할 수 있다. 다음은 이를 구현한 코드이다.

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를 호출해 테스트 중에 전송된 메시지를 추적할 수 있다.

마지막으로 변경해야 할 부분은 assertion이다. 내부 벡터에 몇 개의 항목이 있는지 확인하기 위해 RefCell<Vec<String>>borrow를 호출해 벡터에 대한 불변 참조를 얻는다.

이제 RefCell<T>를 사용하는 방법을 살펴봤으니, 이제 RefCell<T>가 어떻게 동작하는지 자세히 알아보자!

RefCell<T>를 사용해 런타임에 대여 상태 추적하기

불변 참조와 가변 참조를 만들 때 각각 &&mut 구문을 사용한다. RefCell<T>를 사용할 때는 borrowborrow_mut 메서드를 사용한다. 이 메서드들은 RefCell<T>의 안전한 API에 속한다. borrow 메서드는 스마트 포인터 타입인 Ref<T>를 반환하고, borrow_mutRefMut<T>를 반환한다. 두 타입 모두 Deref를 구현하므로 일반 참조처럼 사용할 수 있다.

RefCell<T>는 현재 활성화된 Ref<T>RefMut<T> 스마트 포인터의 수를 추적한다. borrow를 호출할 때마다 RefCell<T>는 불변 대여의 수를 증가시킨다. Ref<T> 값이 스코프를 벗어나면 불변 대여의 수가 1 감소한다. 컴파일 타임의 대여 규칙과 마찬가지로, RefCell<T>는 여러 불변 대여 또는 하나의 가변 대여를 허용한다.

이 규칙을 위반하려고 하면, 일반 참조에서와 같이 컴파일러 에러가 발생하는 대신 RefCell<T>의 구현이 런타임에 패닉을 일으킨다. 리스트 15-23은 리스트 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 변수에 또 다른 가변 대여를 생성한다. 이렇게 하면 같은 스코프에서 두 개의 가변 참조가 생기는데, 이는 허용되지 않는다. 라이브러리의 테스트를 실행하면 리스트 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:
already borrowed: BorrowMutError
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>는 데이터에 여러 소유자를 허용하지만, 데이터에 대해 불변 접근만 제공한다. 만약 Rc<T>RefCell<T>를 가지고 있다면, 여러 소유자를 가질 수 있고 동시에 데이터를 변경할 수 있는 값을 얻을 수 있다.

예를 들어, 15장 18번 예제에서 Rc<T>를 사용해 여러 리스트가 다른 리스트의 소유권을 공유할 수 있게 한 cons 리스트 예제를 떠올려보자. Rc<T>는 불변 값만 보유하므로, 리스트를 생성한 후에는 리스트 내의 값을 변경할 수 없다. 이제 RefCell<T>를 추가해 리스트 내의 값을 변경할 수 있게 해보자. 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: Rc<RefCell<i32>>를 사용해 수정 가능한 List 생성

Rc<RefCell<i32>>의 인스턴스인 값을 생성하고, 이를 value라는 변수에 저장해 나중에 직접 접근할 수 있게 한다. 그런 다음 Cons 변형을 사용해 value를 보유하는 Lista에 생성한다. value를 복제해 avalue가 내부의 5 값을 공유하도록 해야 한다. 이렇게 하면 value에서 a로 소유권이 이전되거나 avalue에서 빌리는 일이 없어진다.

리스트 aRc<T>로 감싸서 리스트 bc를 생성할 때 둘 다 a를 참조할 수 있게 한다. 이는 15장 18번 예제에서 했던 것과 동일하다.

리스트 a, b, c를 생성한 후, value의 값에 10을 더하고 싶다. 이를 위해 valueborrow_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>의 스레드 안전 버전이며, 16장에서 Mutex<T>에 대해 논의할 것이다.