0. 들어가기 전에
0.1. 소유와 참조
1. 값을 소유한다 (Ownership)
- Rust에서 값의 소유권은 단 하나의 변수만 가질 수 있습니다.
- 소유권을 가지는 변수는 메모리에서 해당 값을 관리하며, 소유권이 이동하면 원래 소유자는 더 이상 그 값을 사용할 수 없습니다.
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
println!("{}", s2);
}
- 위 코드에서
s1
은 String
의 소유권을 가집니다.
let s2 = s1;
문장에서 s1
의 소유권이 s2
로 이동하므로, s1
은 더 이상 유효하지 않습니다.
- 따라서
s1
을 다시 사용하려고 하면 컴파일 에러가 발생합니다.
2. 값을 참조한다 (Borrowing)
- 값을 참조할 때는 소유권을 이동하지 않고도 값에 접근할 수 있습니다.
- 이는 값의 주소를 참조하는 것으로, 값이 이동하지 않으므로 원래 변수도 여전히 그 값을 사용할 수 있습니다.
- 참조에는 불변 참조와 가변 참조가 있습니다.
불변 참조 (Immutable Reference)
fn main() {
let s1 = String::from("Hello");
let s2 = &s1;
println!("{}", s1);
println!("{}", s2);
}
- 이 경우
s1
과 s2
는 둘 다 Hello
를 출력할 수 있습니다. &s1
은 s1
의 참조를 가져오지만 소유권은 여전히 s1
이 가지고 있습니다.
가변 참조 (Mutable Reference)
fn main() {
let mut s1 = String::from("Hello");
let s2 = &mut s1;
s2.push_str(", world!");
println!("{}", s2);
}
- 위 코드에서
&mut s1
은 s1
의 가변 참조를 가져옵니다.
- 이를 통해
s2
를 사용해 s1
의 값을 변경할 수 있습니다. 다만, Rust에서는 특정 시점에 하나의 가변 참조만 허용하여 데이터 경합을 방지합니다.
0.2. 책 내용
- 포인터 (pointer)
메모리의 주솟값을 담고 있는 변수
에 대한 일반적인 개념
- 이 주솟값은 어떤 다른 데이터를 참조(‘가리킵니다.’)
- 러스트에서 가장 흔한 종류의 포인터: 참조자
- 참조자는 & 심볼로 표시하고 이들이 가리키고 있는 값을 빌려옵니다.
- 이들은 값을 참조하는 것 외에 다른 어떤 특별한 능력은 없으며, 오버헤드도 없습니다.
- 스마트 포인터 (smart pointer)
- 포인터(
메모리의 주솟값을 담고 있는 변수
)처럼 작동할 뿐만 아니라
- 추가적인 메타데이터와 능력들도 가지고 있는 데이터 구조
- 러스트의 표준 라이브러리에는 다양한 종류의 스마트 포인터들이 정의
- 예: 참조 카운팅 (reference counting) 스마트 포인터
- 이 포인터는
소유자의 개수를 계속 추적
하고,
- 더 이상 소유자가 없으면 데이터를 정리하는 방식으로,
- 어떤 데이터에 대한 여러 소유자를 만들 수 있게 해 줍니다.
- 참조자가 데이터를 빌리기만 하는 반면, 대부분의 경우 스마트 포인터는 가리킨 데이터를 소유
- 스마트 포인터는
데이터 메모리의 주솟값을 담고 있으
면서도, 그 데이터를 소유
- String과
Vec<T>
: 스마트 포인터
- 이들이 어느 정도의 메모리를 소유하고 이를 다룰 수 있게 해 주기 때문
- 그들은 또한
메타데이터
와 추가 능력
또는 보장성
을 갖고 있습니다.
- 예를 들어 String은
- 자신의 용량을 메타데이터로 저장하고
- 자신의 데이터가 언제나 유효한 UTF-8 임을 보증
- 스마트 포인터는 보통 구조체를 이용하여 구현
- 스마트 포인터는
Deref와 Drop 트레이트를 구현
- Deref 트레이트
- 스마트 포인터가 참조자처럼 동작하도록 하여, 참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줍니다.
- Drop 트레이트
스마트 포인터의 인스턴스
가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징
- 아래의 것들도 스마트 포인터 (
데이터 메모리의 주솟값을 담고 있으
면서도, 그 데이터를 소유
)
- 값을 힙에 할당하기 위한
Box<T>
- 복수 소유권을 가능하게 하는 참조 카운팅 타입인
Rc<T>
- 대여 규칙을 컴파일 타임 대신 런타임에 강제하는 타입인,
RefCell<T>
를 통해 접근 가능한 Ref<T>
와 RefMut<T>
내부 가변성 (interior mutability) 패턴
: 불변 타입이 내부 값을 변경하기 위하여 API를 노출
- 순환 참조 (reference cycles) 가 어떤 식으로 메모리가 새어나가게 할 수 있으며, 이를 어떻게 방지하는지에 대해서도 논의해 보겠습니다.
1. Box <T>
를 사용하여 힙에 있는 데이터 가리키기
- 박스는 스택이 아니라 힙에 데이터를 저장할 수 있도록 해줍니다.
- 스택에 남는 것은 힙 데이터를 가리키는 포인터
- 박스 3가지 사용 목적
- 컴파일 타임에는 크기를 알 수 없는 타입이 있는데, 정확한 크기를 요구하는 컨텍스트 내에서 그 타입의 값을 사용하고 싶을 때
- 예를 들어, 재귀적으로 정의된 자료구조에서는 크기를 컴파일 타임에 알 수 없기 때문에 이 타입을 스택에 직접 올릴 수 없습니다.
- 이럴 때
Box<T>
를 사용하여 힙에 데이터를 저장할 수 있습니다.
- 커다란 데이터를 가지고 있고 소유권을 옮기고 싶지만, 그렇게 했을 때 데이터가 복사되지 않을 것을 보장하고 싶을 때
- 방대한 양의 데이터의 소유권 옮기기는 긴 시간이 소요될 수 있는데, 이는 그 데이터가 스택 상에서 복사되기 때문
- 이러한 상황에서 성능을 향상시킬 목적으로 박스 안의 힙에 그 방대한 양의 데이터를 저장할 수 있음
- 그러면 작은 양의 포인터 데이터만 스택 상에서 복사되고, 이 포인터가 참조하는 데이터는 힙의 한 곳에 머물게 됨
- 어떤 값을 소유하고, 이 값의 구체화된 타입보다는 특정 트레이트를 구현한 타입이라는 점만 신경 쓰고 싶을 때
1.1. Box<T>
을 사용하여 힙에 데이터 저장하기
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
- b가 main의 끝에 도달하는 것처럼 어떤 박스가 스코프를 벗어날 때, 다른 어떤 소유된 값과 마찬가지로 할당은 해제될 것입니다.
- 할당 해제는
(스택에 저장된) 박스 (b)
와 이것이 가리키고 있는 (힙에 저장된) 데이터 (5)
모두에게 일어납니다.
1.2. 박스로 재귀적 타입 가능하게 하기
- 재귀적 타입 (recursive type) 의 값은 자신 안에 동일한 타입의 또 다른 값을 담을 수 있습니다.
- 러스트는 컴파일 타임에 어떤 타입이 얼마만큼의 공간을 차지하는지 알아야 하기 때문에 재귀적 타입은 문제를 일으킵니다.
- 재귀적 타입의 값 중첩은 이론적으로 무한히 계속될 수 있으므로, 러스트는 이 값에 얼마만큼의 공간이 필요한지 알 수 없습니다.
- 박스는 알려진 크기를 갖고 있으므로, 재귀적 타입의 정의에 박스를 집어넣어서 재귀적 타입을 가능하게 할 수 있습니다.
- 재귀적 타입의 예제로, 콘스 리스트 (cons list)
1.2.1. 콘스 리스트에 대한 더 많은 정보
- 1, 2, 3 리스트를 담고 있는 콘스 리스트를 각각의 쌍을 괄호로 묶어서 표현한 의사 코드
(1, (2, (3, Nil)))
- 콘스 리스트의 각 아이템은 두 개의 요소를 담고 있습니다: 현재 아이템의 값과 다음 아이템이지요.
- 리스트의 마지막 아이템은 다음 아이템 없이 Nil 이라 불리는 값을 담고 있습니다.
- 콘스 리스트는 cons 함수를 재귀적으로 호출함으로써 만들어집니다.
- 재귀의 기본 케이스를 의미하는 표준 이름이 바로 Nil입니다.
- 6장의 ‘널 (null)’ 혹은 ‘닐 (nil)’ 개념과 동일하지 않다는 점을 주의하세요.
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
- 여기서 마지막의 List는 Nil로써, 리스트의 끝을 알리는 비재귀적인 배리언트
- Cons는 i32의 크기에 List 크기를 더한 만큼의 공간을 필요로 합니다.
1.2.2. 비재귀적 타입의 크기 계산하기
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
- Message 값을 할당하기 위해 필요한 공간의 양을 결정하기 위해서,
- 러스트는 각 배리언트들의 내부를 보면서
- 어떤 배리언트가 가장 많은 공간을 필요로 하는지를 알아봅니다.
- 하나의 배리언트만 사용될 것이기 때문에, Message 값이 필요로 하는 가장 큰 공간은
- 배리언트 중에서 가장 큰 것을 저장하는 데 필요한 공간
1.2.3. Box<T>
를 이용하여 알려진 크기를 가진 재귀적 타입 만들기
Box<T>
가 포인터이기 때문에, 러스트는 언제나 Box<T>
가 필요로 하는 공간이 얼마인지 알고 있습니다: 포인터의 크기는 그것이 가리키고 있는 데이터의 양에 따라 변경되지 않습니다.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
- Cons 배리언트에는 i32와 박스의 포인터 데이터를 저장할 공간을 더한 크기가 필요합니다.
- 박스는 그저 간접 및 힙 할당만을 제공할 뿐
Box<T>
타입은 Deref 트레이트를 구현하고 있기 때문에 스마트 포인터이며,
- 이는
Box<T>
값이 참조자와 같이 취급되도록 허용해 줍니다.
Box<T>
값이 스코프 밖으로 벗어날 때, 박스가 가리키고 있는 힙 데이터도 마찬가지로 정리되는데 이는 Drop 트레이트의 구현 때문에 그렇습니다.
2. Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기
- Deref 트레이트의 deref 메서드가 자동으로 호출되는 시점은 아래 2가지
*(역참조 연산자)
가 호출될 때
- 함수의 인수로
&참조자
가 호출될 때
- Deref 트레이트
- 스마트 포인터가 참조자처럼 동작하도록 하여, 참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줍니다.
2.1. 포인터를 따라가서 값 얻기
*(역참조 연산자)
가 보통의 참조자에 대해 동작하는 방식을 살펴봅시다.
- 보통의 참조자는 포인터의 한 종류이고, 포인터에 대해 생각하는 방법 하나는 어딘가에 저장된 값을 가리키는 화살표처럼 생각하는 것
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
- 숫자와 숫자에 대한 참조자를 비교하는 것은 이 둘이 서로 다른 타입이므로 허용되지 않습니다.
*
를 사용하여 해당 참조자를 따라가서 그것이 가리키고 있는 값을 얻어내야 합니다.
2.2. Box<T>
를 참조자처럼 사용하기
- 참조자: 데이터의 주소를 가리킨다 + 데이터를 대여한다
Box<T>
에 사용된 역참조 연산자는, 예제 15-6의 참조자에 사용된 역참조 연산자와 동일한 방식으로 기능

2.3. 자체 스마트 포인터 정의하기
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
- x는 Copy 트레이트를 구현한 i32 타입이기 때문에, MyBox::new(x)를 호출할 때 값이 복사됩니다.
- Copy된 값은 새로운 위치에 동일한 값을 가지게 되고, 원래 변수 x는 여전히 유효하고 동일한 값을 유지합니다.
- Copy 트레이트를 구현하지 않은 타입
- 힙 메모리에 데이터를 저장하는 타입들
- String / Vec /
HashMap<K, V>
등
Box<T>
/ Rc<T>
/ Mutex<T>
- 값이 다른 변수에 할당되면 소유권이 이동하게 됩니다.
- 이로 인해 원래 변수를 더 이상 사용할 수 없게 되어, 소유권이 한 번에 하나의 변수에만 유지되도록 보장됩니다.
2.4. Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기
- 어떤 트레이트를 구현하기 위해서는
- 그 트레이트가 요구하는 메서드에 대한 구현체를 제공해야 합니다.
- 표준 라이브러리가 제공하는 Deref 트레이트는 deref라는 이름의 메서드 하나를 구현하도록 요구하는데,
- 이 함수는 self를 빌려와서 내부 데이터의 참조자를 반환

- .0이 튜플 구조체의 첫 번째 값에 접근한다는 것을 상기하세요.
- Deref 트레이트가 없으면 컴파일러는 오직 & 참조자들만 역참조할 수 있습니다.
*y
에 들어서면 러스트 뒤편에서는 실제로 아래와 같은 코드가 동작
- deref 메서드가 값의 참조자를 반환하고,
*(y.deref())
에서의 괄호 바깥의 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 함께 작동시키기 위해서입니다.
- 만일 deref 메서드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동할 것입니다.
- 위의 경우 혹은 역참조 연산자를 사용하는 대부분의 경우에서는
MyBox<T>
내부의 값에 대한 소유권을 얻으려는 것이 아닙니다.
2.5. 함수와 메서드를 이용한 암묵적 역참조 강제 변환
- Deref 트레이트의 deref 메서드가 자동으로 호출되는 시점은 아래 2가지
*(역참조 연산자)
가 호출될 때
- 함수의 인수로
&참조자
가 호출될 때 (역참조 강제 변환)
- 역참조 강제 변환 (deref coercion) 은
Deref를 구현한 어떤 타입의 참조자
를 다른 타입의 참조자
로 바꿔줍니다.
- 예를 들어, 역참조 강제 변환은 &String을 &str로 바꿔줄 수 있는데,
- 이는 String의 Deref 트레이트 구현이 그렇게 &str을 반환하도록 했기 때문
- 역참조 강제 변환은 러스트가 함수와 메서드의 인수에 대해 수행해 주는 편의성 기능이고,
- Deref 트레이트를 구현한 타입에 대해서만 동작합니다.
- 이는 어떤 특정한 타입값에 대한 참조자를 함수 혹은 메서드의 인수로 전달하는데
- 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생
- 일련의 deref 메서드 호출이 인수로 제공한 타입을 매개변수로서 필요한 타입으로 변경해 줌
fn hello(name: &str) {
println!("Hello, {name}!");
}

- 인수로 넣어진 타입에 대해 Deref 트레이트가 정의되어 있다면, 러스트는 해당 타입을 분석하고 Deref::deref를 필요한 만큼 사용하여 매개변수 타입과 일치하는 참조자를 얻을 것입니다. - Deref::deref가 추가되어야 하는 횟수는 컴파일 타임에 분석되므로, 역참조 강제 변환의 이점을 얻는 데에 관해서 어떠한 런타임 페널티도 없습니다!
2.6. 역참조 강제 변환이 가변성과 상호작용하는 법
- Deref 트레이트를 사용하여
불변 참조자에 대한 *
를 오버라이딩하는 방법과 비슷한 방식으로,
- DerefMut 트레이트를 사용하여
가변 참조자에 대한 * 연산자
를 오버라이딩
- 중요
- T: Deref<Target=U>일 때 &T에서 &U로
- T: DerefMut<Target=U>일 때 &mut T에서 &mut U로
- T: Deref<Target=U>일 때 &mut T에서 &U로
- 러스트는 가변 참조자를 불변 참조자로 강제할 수도 있습니다.
- 하지만 그 역은 불가능하며, 불변 참조자는 가변 참조자로 결코 강제되지 않을 것입니다.
- 대여 규칙에 의거하여, 가변 참조자가 있을 경우에는 그 가변 참조자가 해당 데이터에 대한 유일한 참조자여야 합니다.
3. Drop 트레이트로 메모리 정리 코드 실행하기
- 스마트 포인터 패턴에서 중요한 트레이트 그 두 번째는 Drop인데, 이는 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할지 커스터마이징하게끔 해줍니다.
- 어떠한 타입이든 Drop 트레이트를 구현할 수 있고, 이 코드가 파일이나 네트워크 연결 같은 자원 해제에 사용되게 할 수 있습니다.
- 스마트 포인터에 대한 맥락에서 Drop을 소개하는 이유
- Drop 트레이트의 기능이 스마트 포인터를 구현할 때 거의 항상 이용되기 때문
- 예를 들어
Box<T>
가 버려질 때는 이 박스가 가리키고 있는 힙 공간의 할당을 해제
- 러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되는 특정 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 삽입해 줄 것
- Drop 트레이트는 drop이라는 이름의 메서드 하나를 구현해야 하는데,
- 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖습니다.
- drop 메서드 = 소멸자 (destructor)
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
3.1. std::mem::drop 으로 값을 일찍 버리기
- 가끔은 어떤 값을 일찍 정리하고 싶을 때도 있습니다.
- 한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다:
- 강제로 drop 메서드를 실행하여 락을 해제해서, 같은 스코프의 다른 코드에서 해당 락을 얻도록 하고 싶을 수도 있지요.
- c.drop() 처럼, 명시적 호출이 불가능
std::mem::drop 함수
는 Drop 트레이트에 있는 drop 메서드
와는 다릅니다.
std::mem::drop 함수
는 프렐루드에 구현되어 있음!
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
4. Rc<T>
, 참조 카운트 스마트 포인터
- Rc: Reference Count
Rc<T>
- 복수 소유권을 가능하게 하는 참조 카운팅 타입
- 소유자의 개수를 계속 추적하고, 더 이상 소유자가 없으면 데이터를 정리
- 하나의 값이 여러 개의 소유자를 가질 수 있는 경우도 있습니다.
- 예를 들어,
- 그래프 데이터 구조에서 여러 에지가 동일한 노드를 가리킬 수도 있고,
- 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지에 의해 소유됨
Rc<T>
타입은 어떤 값의 참조자 개수를 계속 추적하여 해당 값이 계속 사용 중인지를 판단합니다.
- 만일 어떤 값에 대한 참조자가 0개라면 이 값의 메모리 정리를 하더라도 유효하지 않은 참조자가 발생하지 않을 수 있습니다.
Rc<T>
를 거실의 TV라고 상상해 봅시다.
- 한 사람이 TV를 보러 들어올 때 TV를 켭니다.
- 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다.
- 마지막 사람이 거실을 나선다면, TV는 더 이상 사용되고 있지 않으므로 끕니다.
Rc<T>는 오직 싱글스레드 시나리오용이라는 점을 주의
4.1. Rc<T>
를 사용하여 데이터 공유하기


- 위 코드 컴파일 안되는 이유?
- Cons 배리언트는 자신이 들고 있는 데이터를 소유하므로,
- b 리스트를 만들 때 a는 b 안으로 이동되어 b의 소유가 됩니다.
- 그다음 c를 생성할 때 a를 다시 사용하려 할 경우는 허용되지 않는데, 이미 a가 이동되었기 때문
Box<T>
의 자리에 Rc<T>
를 이용하는 형태로 List의 정의를 바꾸겠습니다.
- Rc::new 를 하면 ->
Rc<List>
가 생성됨
- b를 만들 때는 a의 소유권을 얻는 대신, a를 가지고 있는
Rc<List>
를 클론할 것인데,
- 이는 참조자의 개수를 하나에서 둘로 증가시키고
- a와 b가
Rc<List>
안에 있는 데이터의 소유권을 공유하도록 해줍니다.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
4.2. Rc<T>
를 클론하는 것은 참조 카운트를 증가시킵니다

- Rc::clone를 호출하여 참조 카운트를 증가시켜야 했던 것과 달리
- 참조 카운트를 감소시키기 위해 어떤 함수를 호출할 필요는 없습니다:
Rc<T>
값이 스코프 밖으로 벗어나면, Drop 트레이트의 구현체가 자동으로 참조 카운트를 감소시킵니다.
Rc<T>
는 불변 참조자를 통하여 읽기 전용으로 프로그램의 여러 부분에서 데이터를 공유하도록 해줍니다.
5. RefCell<T>
와 내부 가변성 패턴
- 내부 가변성 (interior mutability)
어떤 데이터에 대한 불변 참조자가 있을 때
라도, 데이터를 변경할 수 있게
해주는 러스트의 디자인 패턴
- 보통 이러한 동작은 대여 규칙에 의해 허용되지 않습니다.
- 안전하지 않은 코드는 이 규칙들을 지키고 있는지에 대한 검사를
- 컴파일러에게 맡기는 대신
- 수동으로 하는 중임을 컴파일러에게 알립니다
- 컴파일러는 대여 규칙을 준수함을 보장할 수 없을지라도, 우리가 이를 런타임에 보장할 수 있는 경우라면
내부 가변성 패턴을 쓰는 타입
을 사용할 수 있습니다.
- 여기에 포함된 unsafe 코드는 안전한 API로 감싸져 있고,
- 바깥쪽 타입은 여전히 불변
5.1. RefCell<T>
으로 런타임에 대여 규칙 집행하기
Rc<T>
와는 다르게, RefCell<T> 타입
은 가지고 있는 데이터에 대한 단일 소유권
- 참조자와
Box<T>
를 이용할 때,
- 대여 규칙의 불변성은 컴파일 타임에 집행됩니다.
- 컴파일 타임의 대여 규칙 검사는 아래 2가지 장점
- 개발 과정에서 에러를 더 일찍 잡을 수 있다는 점,
- 그리고 이 모든 분석이 사전에 완료되기 때문에 런타임 성능에 영향이 없다
RefCell<T>
를 이용할 때,
- 이 불변성은 런타임에 집행됩니다.
특정 메모리 안정성 시나리오가 허용
된다는 장점
- 여러분의 코드가 대여 규칙을 준수한다는 것을 (컴파일러는 이해하거나 보장할 수 없지만) 여러분이 확신하는 경우 유용
Rc<T>
와 유사하게, RefCell<T>
은 싱글스레드 시나리오 내에서만 사용 가능
-Rc<T>
는 동일한 데이터에 대해 복수 소유자를 가능하게 합니다;
Box<T>
와 RefCell<T>
은 단일 소유자만 갖습니다.
Box<T>
는 컴파일 타임에 검사 되는 불변 혹은 가변 대여를 허용합니다;
Rc<T>
는 오직 컴파일 타임에 검사 되는 불변 대여만 허용합니다;
RefCell<T>
는 런타임에 검사
되는 불변 혹은 가변 대여를 허용합니다.


RefCell<T>
이 런타임에 검사 되는 가변 대여를 허용하기 때문에
,
RefCell<T>
이 불변일 때라도 RefCell<T>
내부의 값을 변경할 수 있습니다.
5.2. 내부 가변성: 불변값에 대한 가변 대여
- 불변값 내부의 값을 변경하는 것이 내부 가변성 패턴

- TODO
- 하지만,
어떤 값이 자신의 메서드 내부에서는 변경
되지만, 다른 코드에서는 불변으로 보이게 하는 것이 유용한 경우
가 있습니다.
- 그 값의 메서드 바깥쪽 코드에서는 값을 변경할 수 없을 것입니다.
RefCell<T>
을 이용하는 것이 내부 가변성의 기능을 얻는 한 가지 방법이지만,
RefCell<T>
이 대여 규칙을 완벽하게 피하는 것은 아닙니다:
- 컴파일러의 borrow checker는 이러한 내부 가변성을 허용하고,
- 만일 이 규칙을 위반하면, 컴파일러 에러 대신 panic!을 얻을 것입니다.
5.2.1. 내부 가변성에 대한 용례: 목 객체
- 테스트 중 종종 프로그래머는 어떤 타입 대신 다른 타입을 사용하게 되는데, 이러한 자리 표시형 타입을
테스트 더블 (test double)
이라고 합니다.
- 목 객체 (mock object) 는 테스트 더블의 특정한 형태로서, 테스트 중 어떤 일이 일어났는지 기록
- 우리의 라이브러리는
어떤 값이 최댓값에 얼마나 근접했는지를 추적
하고
- 어떤 메시지를 언제 보내야 할지에 대한 기능만 제공
- 이 라이브러리를 사용하는 애플리케이션이
- 메시지를 전송하는 것에 대한 메커니즘을 제공할 예정입니다:
- 이 애플리케이션은 메시지를 애플리케이션 내에 집어넣거나, 이메일을 보내거나, 문자 메시지를 보내거나, 혹은 그 밖의 것들을 할 수 있습니다.
- 라이브러리는 그런 자세한 사항을 알 필요가 없습니다.
- 필요한 모든 것은 우리가 제공하게 될 Messenger라는 이름의 트레이트를 구현하는 것
5.2.2. RefCell<T>
로 런타임에 대여 추적하기
5.2.3. Rc<T>
와 RefCell<T>
를 조합하여 가변 데이터의 복수 소유자 만들기
6. 순환 참조는 메모리 누수를 발생시킬 수 있습니다
------# 0. 들어가기 전에
0.1. 소유와 참조
1. 값을 소유한다 (Ownership)
- Rust에서 값의 소유권은 단 하나의 변수만 가질 수 있습니다.
- 소유권을 가지는 변수는 메모리에서 해당 값을 관리하며, 소유권이 이동하면 원래 소유자는 더 이상 그 값을 사용할 수 없습니다.
fn main() {
let s1 = String::from("Hello");
let s2 = s1;
println!("{}", s2);
}
- 위 코드에서
s1
은 String
의 소유권을 가집니다.
let s2 = s1;
문장에서 s1
의 소유권이 s2
로 이동하므로, s1
은 더 이상 유효하지 않습니다.
- 따라서
s1
을 다시 사용하려고 하면 컴파일 에러가 발생합니다.
2. 값을 참조한다 (Borrowing)
- 값을 참조할 때는 소유권을 이동하지 않고도 값에 접근할 수 있습니다.
- 이는 값의 주소를 참조하는 것으로, 값이 이동하지 않으므로 원래 변수도 여전히 그 값을 사용할 수 있습니다.
- 참조에는 불변 참조와 가변 참조가 있습니다.
불변 참조 (Immutable Reference)
fn main() {
let s1 = String::from("Hello");
let s2 = &s1;
println!("{}", s1);
println!("{}", s2);
}
- 이 경우
s1
과 s2
는 둘 다 Hello
를 출력할 수 있습니다. &s1
은 s1
의 참조를 가져오지만 소유권은 여전히 s1
이 가지고 있습니다.
가변 참조 (Mutable Reference)
fn main() {
let mut s1 = String::from("Hello");
let s2 = &mut s1;
s2.push_str(", world!");
println!("{}", s2);
}
- 위 코드에서
&mut s1
은 s1
의 가변 참조를 가져옵니다.
- 이를 통해
s2
를 사용해 s1
의 값을 변경할 수 있습니다. 다만, Rust에서는 특정 시점에 하나의 가변 참조만 허용하여 데이터 경합을 방지합니다.
0.2. 책 내용
- 포인터 (pointer)
메모리의 주솟값을 담고 있는 변수
에 대한 일반적인 개념
- 이 주솟값은 어떤 다른 데이터를 참조(‘가리킵니다.’)
- 러스트에서 가장 흔한 종류의 포인터: 참조자
- 참조자는 & 심볼로 표시하고 이들이 가리키고 있는 값을 빌려옵니다.
- 이들은 값을 참조하는 것 외에 다른 어떤 특별한 능력은 없으며, 오버헤드도 없습니다.
- 스마트 포인터 (smart pointer)
- 포인터(
메모리의 주솟값을 담고 있는 변수
)처럼 작동할 뿐만 아니라
- 추가적인 메타데이터와 능력들도 가지고 있는 데이터 구조
- 러스트의 표준 라이브러리에는 다양한 종류의 스마트 포인터들이 정의
- 예: 참조 카운팅 (reference counting) 스마트 포인터
- 이 포인터는
소유자의 개수를 계속 추적
하고,
- 더 이상 소유자가 없으면 데이터를 정리하는 방식으로,
- 어떤 데이터에 대한 여러 소유자를 만들 수 있게 해 줍니다.
- 참조자가 데이터를 빌리기만 하는 반면, 대부분의 경우 스마트 포인터는 가리킨 데이터를 소유
- 스마트 포인터는
데이터 메모리의 주솟값을 담고 있으
면서도, 그 데이터를 소유
- String과
Vec<T>
: 스마트 포인터
- 이들이 어느 정도의 메모리를 소유하고 이를 다룰 수 있게 해 주기 때문
- 그들은 또한
메타데이터
와 추가 능력
또는 보장성
을 갖고 있습니다.
- 예를 들어 String은
- 자신의 용량을 메타데이터로 저장하고
- 자신의 데이터가 언제나 유효한 UTF-8 임을 보증
- 스마트 포인터는 보통 구조체를 이용하여 구현
- 스마트 포인터는
Deref와 Drop 트레이트를 구현
- Deref 트레이트
- 스마트 포인터가 참조자처럼 동작하도록 하여, 참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줍니다.
- Drop 트레이트
스마트 포인터의 인스턴스
가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징
- 아래의 것들도 스마트 포인터 (
데이터 메모리의 주솟값을 담고 있으
면서도, 그 데이터를 소유
)
- 값을 힙에 할당하기 위한
Box<T>
- 복수 소유권을 가능하게 하는 참조 카운팅 타입인
Rc<T>
- 대여 규칙을 컴파일 타임 대신 런타임에 강제하는 타입인,
RefCell<T>
를 통해 접근 가능한 Ref<T>
와 RefMut<T>
내부 가변성 (interior mutability) 패턴
: 불변 타입이 내부 값을 변경하기 위하여 API를 노출
- 순환 참조 (reference cycles) 가 어떤 식으로 메모리가 새어나가게 할 수 있으며, 이를 어떻게 방지하는지에 대해서도 논의해 보겠습니다.
1. Box <T>
를 사용하여 힙에 있는 데이터 가리키기
- 박스는 스택이 아니라 힙에 데이터를 저장할 수 있도록 해줍니다.
- 스택에 남는 것은 힙 데이터를 가리키는 포인터
- 박스 3가지 사용 목적
- 컴파일 타임에는 크기를 알 수 없는 타입이 있는데, 정확한 크기를 요구하는 컨텍스트 내에서 그 타입의 값을 사용하고 싶을 때
- 예를 들어, 재귀적으로 정의된 자료구조에서는 크기를 컴파일 타임에 알 수 없기 때문에 이 타입을 스택에 직접 올릴 수 없습니다.
- 이럴 때
Box<T>
를 사용하여 힙에 데이터를 저장할 수 있습니다.
- 커다란 데이터를 가지고 있고 소유권을 옮기고 싶지만, 그렇게 했을 때 데이터가 복사되지 않을 것을 보장하고 싶을 때
- 방대한 양의 데이터의 소유권 옮기기는 긴 시간이 소요될 수 있는데, 이는 그 데이터가 스택 상에서 복사되기 때문
- 이러한 상황에서 성능을 향상시킬 목적으로 박스 안의 힙에 그 방대한 양의 데이터를 저장할 수 있음
- 그러면 작은 양의 포인터 데이터만 스택 상에서 복사되고, 이 포인터가 참조하는 데이터는 힙의 한 곳에 머물게 됨
- 어떤 값을 소유하고, 이 값의 구체화된 타입보다는 특정 트레이트를 구현한 타입이라는 점만 신경 쓰고 싶을 때
1.1. Box<T>
을 사용하여 힙에 데이터 저장하기
fn main() {
let b = Box::new(5);
println!("b = {}", b);
}
- b가 main의 끝에 도달하는 것처럼 어떤 박스가 스코프를 벗어날 때, 다른 어떤 소유된 값과 마찬가지로 할당은 해제될 것입니다.
- 할당 해제는
(스택에 저장된) 박스 (b)
와 이것이 가리키고 있는 (힙에 저장된) 데이터 (5)
모두에게 일어납니다.
1.2. 박스로 재귀적 타입 가능하게 하기
- 재귀적 타입 (recursive type) 의 값은 자신 안에 동일한 타입의 또 다른 값을 담을 수 있습니다.
- 러스트는 컴파일 타임에 어떤 타입이 얼마만큼의 공간을 차지하는지 알아야 하기 때문에 재귀적 타입은 문제를 일으킵니다.
- 재귀적 타입의 값 중첩은 이론적으로 무한히 계속될 수 있으므로, 러스트는 이 값에 얼마만큼의 공간이 필요한지 알 수 없습니다.
- 박스는 알려진 크기를 갖고 있으므로, 재귀적 타입의 정의에 박스를 집어넣어서 재귀적 타입을 가능하게 할 수 있습니다.
- 재귀적 타입의 예제로, 콘스 리스트 (cons list)
1.2.1. 콘스 리스트에 대한 더 많은 정보
- 1, 2, 3 리스트를 담고 있는 콘스 리스트를 각각의 쌍을 괄호로 묶어서 표현한 의사 코드
(1, (2, (3, Nil)))
- 콘스 리스트의 각 아이템은 두 개의 요소를 담고 있습니다: 현재 아이템의 값과 다음 아이템이지요.
- 리스트의 마지막 아이템은 다음 아이템 없이 Nil 이라 불리는 값을 담고 있습니다.
- 콘스 리스트는 cons 함수를 재귀적으로 호출함으로써 만들어집니다.
- 재귀의 기본 케이스를 의미하는 표준 이름이 바로 Nil입니다.
- 6장의 ‘널 (null)’ 혹은 ‘닐 (nil)’ 개념과 동일하지 않다는 점을 주의하세요.
enum List {
Cons(i32, List),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Cons(2, Cons(3, Nil)));
}
- 여기서 마지막의 List는 Nil로써, 리스트의 끝을 알리는 비재귀적인 배리언트
- Cons는 i32의 크기에 List 크기를 더한 만큼의 공간을 필요로 합니다.
1.2.2. 비재귀적 타입의 크기 계산하기
enum Message {
Quit,
Move { x: i32, y: i32 },
Write(String),
ChangeColor(i32, i32, i32),
}
- Message 값을 할당하기 위해 필요한 공간의 양을 결정하기 위해서,
- 러스트는 각 배리언트들의 내부를 보면서
- 어떤 배리언트가 가장 많은 공간을 필요로 하는지를 알아봅니다.
- 하나의 배리언트만 사용될 것이기 때문에, Message 값이 필요로 하는 가장 큰 공간은
- 배리언트 중에서 가장 큰 것을 저장하는 데 필요한 공간
1.2.3. Box<T>
를 이용하여 알려진 크기를 가진 재귀적 타입 만들기
Box<T>
가 포인터이기 때문에, 러스트는 언제나 Box<T>
가 필요로 하는 공간이 얼마인지 알고 있습니다: 포인터의 크기는 그것이 가리키고 있는 데이터의 양에 따라 변경되지 않습니다.
enum List {
Cons(i32, Box<List>),
Nil,
}
use crate::List::{Cons, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
- Cons 배리언트에는 i32와 박스의 포인터 데이터를 저장할 공간을 더한 크기가 필요합니다.
- 박스는 그저 간접 및 힙 할당만을 제공할 뿐
Box<T>
타입은 Deref 트레이트를 구현하고 있기 때문에 스마트 포인터이며,
- 이는
Box<T>
값이 참조자와 같이 취급되도록 허용해 줍니다.
Box<T>
값이 스코프 밖으로 벗어날 때, 박스가 가리키고 있는 힙 데이터도 마찬가지로 정리되는데 이는 Drop 트레이트의 구현 때문에 그렇습니다.
2. Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기
- Deref 트레이트의 deref 메서드가 자동으로 호출되는 시점은 아래 2가지
*(역참조 연산자)
가 호출될 때
- 함수의 인수로
&참조자
가 호출될 때
- Deref 트레이트
- 스마트 포인터가 참조자처럼 동작하도록 하여, 참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줍니다.
2.1. 포인터를 따라가서 값 얻기
*(역참조 연산자)
가 보통의 참조자에 대해 동작하는 방식을 살펴봅시다.
- 보통의 참조자는 포인터의 한 종류이고, 포인터에 대해 생각하는 방법 하나는 어딘가에 저장된 값을 가리키는 화살표처럼 생각하는 것
fn main() {
let x = 5;
let y = &x;
assert_eq!(5, x);
assert_eq!(5, *y);
}
- 숫자와 숫자에 대한 참조자를 비교하는 것은 이 둘이 서로 다른 타입이므로 허용되지 않습니다.
*
를 사용하여 해당 참조자를 따라가서 그것이 가리키고 있는 값을 얻어내야 합니다.
2.2. Box<T>
를 참조자처럼 사용하기
- 참조자: 데이터의 주소를 가리킨다 + 데이터를 대여한다
Box<T>
에 사용된 역참조 연산자는, 예제 15-6의 참조자에 사용된 역참조 연산자와 동일한 방식으로 기능

2.3. 자체 스마트 포인터 정의하기
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y);
}
- x는 Copy 트레이트를 구현한 i32 타입이기 때문에, MyBox::new(x)를 호출할 때 값이 복사됩니다.
- Copy된 값은 새로운 위치에 동일한 값을 가지게 되고, 원래 변수 x는 여전히 유효하고 동일한 값을 유지합니다.
- Copy 트레이트를 구현하지 않은 타입
- 힙 메모리에 데이터를 저장하는 타입들
- String / Vec /
HashMap<K, V>
등
Box<T>
/ Rc<T>
/ Mutex<T>
- 값이 다른 변수에 할당되면 소유권이 이동하게 됩니다.
- 이로 인해 원래 변수를 더 이상 사용할 수 없게 되어, 소유권이 한 번에 하나의 변수에만 유지되도록 보장됩니다.
2.4. Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기
- 어떤 트레이트를 구현하기 위해서는
- 그 트레이트가 요구하는 메서드에 대한 구현체를 제공해야 합니다.
- 표준 라이브러리가 제공하는 Deref 트레이트는 deref라는 이름의 메서드 하나를 구현하도록 요구하는데,
- 이 함수는 self를 빌려와서 내부 데이터의 참조자를 반환

- .0이 튜플 구조체의 첫 번째 값에 접근한다는 것을 상기하세요.
- Deref 트레이트가 없으면 컴파일러는 오직 & 참조자들만 역참조할 수 있습니다.
*y
에 들어서면 러스트 뒤편에서는 실제로 아래와 같은 코드가 동작
- deref 메서드가 값의 참조자를 반환하고,
*(y.deref())
에서의 괄호 바깥의 일반 역참조가 여전히 필요한 이유는 소유권 시스템과 함께 작동시키기 위해서입니다.
- 만일 deref 메서드가 값의 참조자 대신 값을 직접 반환했다면, 그 값은 self 바깥으로 이동할 것입니다.
- 위의 경우 혹은 역참조 연산자를 사용하는 대부분의 경우에서는
MyBox<T>
내부의 값에 대한 소유권을 얻으려는 것이 아닙니다.
2.5. 함수와 메서드를 이용한 암묵적 역참조 강제 변환
- Deref 트레이트의 deref 메서드가 자동으로 호출되는 시점은 아래 2가지
*(역참조 연산자)
가 호출될 때
- 함수의 인수로
&참조자
가 호출될 때 (역참조 강제 변환)
- 역참조 강제 변환 (deref coercion) 은
Deref를 구현한 어떤 타입의 참조자
를 다른 타입의 참조자
로 바꿔줍니다.
- 예를 들어, 역참조 강제 변환은 &String을 &str로 바꿔줄 수 있는데,
- 이는 String의 Deref 트레이트 구현이 그렇게 &str을 반환하도록 했기 때문
- 역참조 강제 변환은 러스트가 함수와 메서드의 인수에 대해 수행해 주는 편의성 기능이고,
- Deref 트레이트를 구현한 타입에 대해서만 동작합니다.
- 이는 어떤 특정한 타입값에 대한 참조자를 함수 혹은 메서드의 인수로 전달하는데
- 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생
- 일련의 deref 메서드 호출이 인수로 제공한 타입을 매개변수로서 필요한 타입으로 변경해 줌
fn hello(name: &str) {
println!("Hello, {name}!");
}

- 인수로 넣어진 타입에 대해 Deref 트레이트가 정의되어 있다면, 러스트는 해당 타입을 분석하고 Deref::deref를 필요한 만큼 사용하여 매개변수 타입과 일치하는 참조자를 얻을 것입니다. - Deref::deref가 추가되어야 하는 횟수는 컴파일 타임에 분석되므로, 역참조 강제 변환의 이점을 얻는 데에 관해서 어떠한 런타임 페널티도 없습니다!
2.6. 역참조 강제 변환이 가변성과 상호작용하는 법
- Deref 트레이트를 사용하여
불변 참조자에 대한 *
를 오버라이딩하는 방법과 비슷한 방식으로,
- DerefMut 트레이트를 사용하여
가변 참조자에 대한 * 연산자
를 오버라이딩
- 중요
- T: Deref<Target=U>일 때 &T에서 &U로
- T: DerefMut<Target=U>일 때 &mut T에서 &mut U로
- T: Deref<Target=U>일 때 &mut T에서 &U로
- 러스트는 가변 참조자를 불변 참조자로 강제할 수도 있습니다.
- 하지만 그 역은 불가능하며, 불변 참조자는 가변 참조자로 결코 강제되지 않을 것입니다.
- 대여 규칙에 의거하여, 가변 참조자가 있을 경우에는 그 가변 참조자가 해당 데이터에 대한 유일한 참조자여야 합니다.
3. Drop 트레이트로 메모리 정리 코드 실행하기
- 스마트 포인터 패턴에서 중요한 트레이트 그 두 번째는 Drop인데, 이는 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할지 커스터마이징하게끔 해줍니다.
- 어떠한 타입이든 Drop 트레이트를 구현할 수 있고, 이 코드가 파일이나 네트워크 연결 같은 자원 해제에 사용되게 할 수 있습니다.
- 스마트 포인터에 대한 맥락에서 Drop을 소개하는 이유
- Drop 트레이트의 기능이 스마트 포인터를 구현할 때 거의 항상 이용되기 때문
- 예를 들어
Box<T>
가 버려질 때는 이 박스가 가리키고 있는 힙 공간의 할당을 해제
- 러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되는 특정 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 삽입해 줄 것
- Drop 트레이트는 drop이라는 이름의 메서드 하나를 구현해야 하는데,
- 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖습니다.
- drop 메서드 = 소멸자 (destructor)
struct CustomSmartPointer {
data: String,
}
impl Drop for CustomSmartPointer {
fn drop(&mut self) {
println!("Dropping CustomSmartPointer with data `{}`!", self.data);
}
}
fn main() {
let c = CustomSmartPointer {
data: String::from("my stuff"),
};
let d = CustomSmartPointer {
data: String::from("other stuff"),
};
println!("CustomSmartPointers created.");
}
CustomSmartPointers created.
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
3.1. std::mem::drop 으로 값을 일찍 버리기
- 가끔은 어떤 값을 일찍 정리하고 싶을 때도 있습니다.
- 한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다:
- 강제로 drop 메서드를 실행하여 락을 해제해서, 같은 스코프의 다른 코드에서 해당 락을 얻도록 하고 싶을 수도 있지요.
- c.drop() 처럼, 명시적 호출이 불가능
std::mem::drop 함수
는 Drop 트레이트에 있는 drop 메서드
와는 다릅니다.
std::mem::drop 함수
는 프렐루드에 구현되어 있음!
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created.");
drop(c);
println!("CustomSmartPointer dropped before the end of main.");
}
4. Rc<T>
, 참조 카운트 스마트 포인터
- Rc: Reference Count
Rc<T>
- 복수 소유권을 가능하게 하는 참조 카운팅 타입
- 소유자의 개수를 계속 추적하고, 더 이상 소유자가 없으면 데이터를 정리
- 하나의 값이 여러 개의 소유자를 가질 수 있는 경우도 있습니다.
- 예를 들어,
- 그래프 데이터 구조에서 여러 에지가 동일한 노드를 가리킬 수도 있고,
- 그 노드는 개념적으로 해당 노드를 가리키는 모든 에지에 의해 소유됨
Rc<T>
타입은 어떤 값의 참조자 개수를 계속 추적하여 해당 값이 계속 사용 중인지를 판단합니다.
- 만일 어떤 값에 대한 참조자가 0개라면 이 값의 메모리 정리를 하더라도 유효하지 않은 참조자가 발생하지 않을 수 있습니다.
Rc<T>
를 거실의 TV라고 상상해 봅시다.
- 한 사람이 TV를 보러 들어올 때 TV를 켭니다.
- 다른 사람들은 거실로 들어와서 TV를 볼 수 있습니다.
- 마지막 사람이 거실을 나선다면, TV는 더 이상 사용되고 있지 않으므로 끕니다.
Rc<T>는 오직 싱글스레드 시나리오용이라는 점을 주의
4.1. Rc<T>
를 사용하여 데이터 공유하기


- 위 코드 컴파일 안되는 이유?
- Cons 배리언트는 자신이 들고 있는 데이터를 소유하므로,
- b 리스트를 만들 때 a는 b 안으로 이동되어 b의 소유가 됩니다.
- 그다음 c를 생성할 때 a를 다시 사용하려 할 경우는 허용되지 않는데, 이미 a가 이동되었기 때문
Box<T>
의 자리에 Rc<T>
를 이용하는 형태로 List의 정의를 바꾸겠습니다.
- Rc::new 를 하면 ->
Rc<List>
가 생성됨
- b를 만들 때는 a의 소유권을 얻는 대신, a를 가지고 있는
Rc<List>
를 클론할 것인데,
- 이는 참조자의 개수를 하나에서 둘로 증가시키고
- a와 b가
Rc<List>
안에 있는 데이터의 소유권을 공유하도록 해줍니다.
enum List {
Cons(i32, Rc<List>),
Nil,
}
use crate::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Rc::new(Cons(10, Rc::new(Nil)))));
let b = Cons(3, Rc::clone(&a));
let c = Cons(4, Rc::clone(&a));
}
4.2. Rc<T>
를 클론하는 것은 참조 카운트를 증가시킵니다

- Rc::clone를 호출하여 참조 카운트를 증가시켜야 했던 것과 달리
- 참조 카운트를 감소시키기 위해 어떤 함수를 호출할 필요는 없습니다:
Rc<T>
값이 스코프 밖으로 벗어나면, Drop 트레이트의 구현체가 자동으로 참조 카운트를 감소시킵니다.
Rc<T>
는 불변 참조자를 통하여 읽기 전용으로 프로그램의 여러 부분에서 데이터를 공유하도록 해줍니다.
5. RefCell<T>
와 내부 가변성 패턴
- 내부 가변성 (interior mutability)
어떤 데이터에 대한 불변 참조자가 있을 때
라도, 데이터를 변경할 수 있게
해주는 러스트의 디자인 패턴
- 보통 이러한 동작은 대여 규칙에 의해 허용되지 않습니다.
- 안전하지 않은 코드는 이 규칙들을 지키고 있는지에 대한 검사를
- 컴파일러에게 맡기는 대신
- 수동으로 하는 중임을 컴파일러에게 알립니다
- 컴파일러는 대여 규칙을 준수함을 보장할 수 없을지라도, 우리가 이를 런타임에 보장할 수 있는 경우라면
내부 가변성 패턴을 쓰는 타입
을 사용할 수 있습니다.
- 여기에 포함된 unsafe 코드는 안전한 API로 감싸져 있고,
- 바깥쪽 타입은 여전히 불변
5.1. RefCell<T>
으로 런타임에 대여 규칙 집행하기
Rc<T>
와는 다르게, RefCell<T> 타입
은 가지고 있는 데이터에 대한 단일 소유권
- 참조자와
Box<T>
를 이용할 때,
- 대여 규칙의 불변성은 컴파일 타임에 집행됩니다.
- 컴파일 타임의 대여 규칙 검사는 아래 2가지 장점
- 개발 과정에서 에러를 더 일찍 잡을 수 있다는 점,
- 그리고 이 모든 분석이 사전에 완료되기 때문에 런타임 성능에 영향이 없다
RefCell<T>
를 이용할 때,
- 이 불변성은 런타임에 집행됩니다.
특정 메모리 안정성 시나리오가 허용
된다는 장점
- 여러분의 코드가 대여 규칙을 준수한다는 것을 (컴파일러는 이해하거나 보장할 수 없지만) 여러분이 확신하는 경우 유용
Rc<T>
와 유사하게, RefCell<T>
은 싱글스레드 시나리오 내에서만 사용 가능
-Rc<T>
는 동일한 데이터에 대해 복수 소유자를 가능하게 합니다;
Box<T>
와 RefCell<T>
은 단일 소유자만 갖습니다.
Box<T>
는 컴파일 타임에 검사 되는 불변 혹은 가변 대여를 허용합니다;
Rc<T>
는 오직 컴파일 타임에 검사 되는 불변 대여만 허용합니다;
RefCell<T>
는 런타임에 검사
되는 불변 혹은 가변 대여를 허용합니다.


RefCell<T>
이 런타임에 검사 되는 가변 대여를 허용하기 때문에
,
RefCell<T>
이 불변일 때라도 RefCell<T>
내부의 값을 변경할 수 있습니다.
5.2. 내부 가변성: 불변값에 대한 가변 대여
- 불변값 내부의 값을 변경하는 것이 내부 가변성 패턴

- TODO
- 하지만, 어떤 값이 자신의 메서드 내부에서는 변경되지만 다른 코드에서는 불변으로 보이게 하는 것이 유용한 경우가 있습니다.
- 그 값의 메서드 바깥쪽 코드에서는 값을 변경할 수 없을 것입니다.
RefCell<T>
을 이용하는 것이 내부 가변성의 기능을 얻는 한 가지 방법이지만,
RefCell<T>
이 대여 규칙을 완벽하게 피하는 것은 아닙니다:
- 컴파일러의 대여 검사기는 이러한 내부 가변성을 허용하고, 대신 대여 규칙은 런타임에 검사 됩니다.
- 만일 이 규칙을 위반하면, 컴파일러 에러 대신 panic!을 얻을 것입니다.
5.2.1. 내부 가변성에 대한 용례: 목 객체
- 테스트 중 종종 프로그래머는 어떤 타입 대신 다른 타입을 사용하게 되는데, 이러한 자리 표시형 타입을 테스트 더블 (test double) 이라고 합니다.
- 목 객체 (mock object) 는 테스트 더블의 특정한 형태
- 우리의 라이브러리는
- 어떤 값이 최댓값에 얼마나 근접했는지를 추적하고
- 어떤 메시지를 언제 보내야 할지에 대한 기능만 제공
- 이 라이브러리를 사용하는 애플리케이션이
- 메시지를 전송하는 것에 대한 메커니즘을 제공할 예정입니다:
- 이 애플리케이션은 메시지를 애플리케이션 내에 집어넣거나, 이메일을 보내거나, 문자 메시지를 보내거나, 혹은 그 밖의 것들을 할 수 있습니다.
- 라이브러리는 그런 자세한 사항을 알 필요가 없습니다.
- 필요한 모든 것은 우리가 제공하게 될 Messenger라는 이름의 트레이트를 구현하는 것
- 아래 코드는 컴파일 에러가 남

- 메시지를 추적하기 위해서 MockMessenger를 수정할 수가 없는데,
- 그 이유는 send 메서드가 self의 불변 참조자를 가져오기 때문
- 또한 에러 메시지가 제안하는 &mut self를 대신 사용하라는 것도 받아들일 수 없는데,
- 그렇게 되면 send의 시그니처가 Messenger 트레이트의 정의에 있는 시그니처와 맞지 않게 될 것이기 때문

- send 메서드의 구현부에서 첫 번째 매개변수는 여전히 self의 불변 대여 형태인데,
- self.sent_messages의
RefCell<Vec<String>>
에 있는 borrow_mut
를 호출하여
RefCell<Vec<String>>
내부 값, 즉 벡터에 대한 가변 참조자를 얻습니다.
borrow_mut
: RefMut<T>
반환
- 내부 벡터 안에 몇 개의 아이템이 있는지 보기 위해서
RefCell<Vec<String>>
의 borrow
를 호출하여 벡터에 대한 불변 참조자를 얻습니다.
borrow
: Ref<T>
반환
5.2.2. RefCell<T>
로 런타임에 대여 추적하기
RefCell<T>
는 현재 활성화된 Ref<T>와 RefMut<T>
스마트 포인터들이 몇 개나 있는지 추적
RefCell<T>
는 어떤 시점에서든
- 여러 개의 불변 대여 혹은
- 하나의 가변 대여를 가질 수 있도록 만들어 줍니다.
- 위 규칙을 위반하면, 컴파일 에러를 내는 것이 아니라, 런타임에 panic!을 일으킬 것
- 아래코드는 런타임 시점에 패닉 일으킴
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));
}
}
- 여러분의 코드는 컴파일 타임이 아닌 런타임에 대여를 추적하는 결과로 -> 약간의 런타임 성능 페널티를 초래할 것입니다.
- 하지만
RefCell<T>
를 이용하는 것은
- 오직 불변값만 허용된 컨텍스트 안에서 사용하는 중에, 본 메시지를 추적하기 위해서 스스로를 변경할 수 있는 목 객체 작성을 가능하게 해 줍니다.
5.2.3. Rc<T>
와 RefCell<T>
를 조합하여 가변 데이터의 복수 소유자 만들기
Rc<T>
- 복수 소유권을 가능하게 하는 참조 카운팅 타입
- 소유자의 개수를 계속 추적하고, 더 이상 소유자가 없으면 데이터를 정리
- 불변 접근만 허용
RefCell<T>
를 들고 있는 Rc<T>
를 가지게 되면,
- 가변이면서 동시에 복수의 소유자를 갖는 값을 얻을 수 있는 것이죠!
Rc<T>
가 오직 불변의 값만을 가질 수 있기 때문에, 일단 이것들을 만들면 리스트 안의 값들을 변경하는 것은 불가능했습니다.
RefCell<T>
를 추가하여 이 리스트 안의 값을 변경하는 능력을 얻어봅시다.

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))
6. 순환 참조는 메모리 누수를 발생시킬 수 있습니다
- 메모리 누수 (memory leak)
- 러스트의 메모리 안정성 보장은 메모리 누수 (memory leak)를 생성하기 어렵게 만들지만, 불가능하게 만드는 것은 아닙니다.
Rc<T>
및 RefCell<T>
를 사용하면 러스트에서 메모리 누수가 허용되는 것을 알 수 있습니다:
- 즉 아이템들이 서로를 순환 참조하는 참조자를 만드는 것이 가능합니다.
- 이는 메모리 누수를 발생시키는데, 그 이유는
- 순환 고리 안의 각 아이템의 참조 카운트는 결코 0이 되지 않을 것이고,
- 그러므로 값들은 버려지지 않을 것이기 때문입니다.
6.1. 순환 참조 만들기

- a를 수정하여 이것이 Nil 대신 b를 가리키도록 하였는데, 이렇게 순환이 만들어집니다.
- 이는 tail 메서드를 사용하여 a에 있는
RefCell<Rc<List>>
로부터 참조자를 얻어오는 식으로 이루어졌는데, 이것을 link라는 변수에 넣었습니다.
- 그다음
RefCell<Rc<List>>
의 borrow_mut 메서드를 사용하여
- Nil 값을 가지고 있는
Rc<List>
내부의 값을 b의 Rc<List>
로 바꾸었습니다.
- main의 끝에서 러스트는 b를 버리는데, 이는 b의
Rc<List>
참조 카운트를 2에서 1로 줄입니다.
Rc<List>
가 힙에 보유한 메모리는 이 시점에서 해제되지 않을 것인데,
- 그 이유는 참조 카운트가 0이 아닌 1이기 때문입니다.
- 그런 다음 러스트는 a를 버리고,
- 이는 마찬가지로 a의
Rc<List>
인스턴스가 가진 참조 카운트를 2에서 1로 줄입니다.
- 이 인스턴스의 메모리 또한 버려질 수 없는데, 왜냐하면 이쪽의
Rc<List>
인스턴스도 여전히 무언가를 참조하기 때문입니다.
- 순환 참조를 만드는 것은 쉽게 이루어지지는 않지만, 불가능한 것도 아닙니다.
- 만일 여러분이
Rc<T>
값을 가지고 있는 RefCell<T>
- 혹은 그와 유사하게 내부 가변성 및 참조 카운팅 기능이 있는 타입들의 중첩된 조합을 사용한다면,
- 여러분이 직접 순환을 만들지 않음을 보장해야 합니다;
- 이 순환을 찾아내는 것을 러스트에 의지할 수는 없습니다.
- 순환 참조를 피하는 또 다른 해결책은 데이터 구조를 재구성하여
- 어떤 참조자는 소유권을 갖고 어떤 참조자는 그렇지 않도록 하는 것입니다.
- 결과적으로 몇 개의 소유권 관계와 몇 개의 소유권 없는 관계로 이루어진 순환을 만들 수 있으며,
- 소유권 관계들만이 값을 버릴지 말지에 관해 영향을 주게 됩니다.
6.2. 순환 참조 방지하기: Rc<T>
를 Weak<T>
로 바꾸기
- Rc::clone을 호출하는 것은
Rc<T>
인스턴스의 strong_count를 증가시키고,
Rc<T>
인스턴스는 자신의 strong_count가 0이 된 경우에만 제거되는 것을 보았습니다.
- 강한 참조는
Rc<T>
인스턴스의 소유권을 공유할 수 있는 방법
- Rc::downgrade에
Rc<T>
의 참조자를 넣어서 호출하면
Rc<T>
인스턴스 내의 값을 가리키는 약한 참조 (weak reference) 를 만드는 것도 가능합니다.
Rc<T>
인스턴스의 weak_count를 1 증가시킵니다.
Rc::downgrade
를 호출하면 Weak<T>
타입의 스마트 포인터를 얻게 됩니다.
- 약한 참조는 소유권 관계를 표현하지 않고,
- 약한 참조의 개수는
Rc<T>
인스턴스가 제거되는 경우에 영향을 주지 않습니다.
- 약한 참조가 포함된 순환 참조는 그 값의 강한 참조 개수를 0으로 만드는 순간 깨지게 되기 때문에,
Rc<T>
타입은 strong_count와 유사한 방식으로 weak_count를 사용하여 Weak<T>
참조가 몇 개 있는지 추적
Weak<T>
가 참조하고 있는 값이 이미 버려졌을지도 모르기 때문에, Weak<T>
가 가리키고 있는 값으로 어떤 일을 하기 위해서는
- 그 값이 여전히 존재하는지를 반드시 확인해야 합니다.
- 이를 위해
Weak<T>
의 upgrade 메서드
를 호출하는데,
- 이 메서드는
Option<Rc<T>>
를 반환할 것입니다.
- 만일
Rc<T>
값이 아직 버려지지 않았다면 Some 결과를 얻게 될 것이고
Rc<T>
값이 버려졌다면 None 결괏값을 얻게 될 것입니다.
6.2.1. 트리 데이터 구조 만들기: 자식 노드를 가진 Node
- Node가 자기 자식들을 소유하도록 하고, 이 소유권을 공유하여 트리의 각 Node에 직접 접근할 수 있도록 하고 싶습니다.
Rc<Node>
타입의 값이 되도록 정의하였습니다.
- 또한 어떤 노드가 다른 노드의 자식이 되도록 수정하려고,
RefCell<T>
로 감싼 children을 갖도록 하였습니다.

- branch로부터 branch.children를 통하여 leaf까지 접근할 수 있게 되었지만,
- leaf에서부터 branch로 접근할 방법은 없습니다.
- 그 원인은 leaf가 branch에 대한 참조자를 가지고 있지 않고 이들 간의 연관성을 알지 못하기 때문입니다.
- leaf에게 branch가 자신의 부모임을 알려주고 싶습니다.
6.2.2. 자식에서 부모로 가는 참조자 추적하기

- leaf의 부모를 다시 한번 출력할 때는 branch를 가지고 있는 Some 배리언트를 얻게 될 것입니다:
- 이제 leaf는 자기 부모에 접근할 수 있습니다!
- leaf를 출력할 때 예제 15-26에서와 같이 궁극적으로 스택 오버플로우로 끝나버리는 그 순환 문제도 피하게 되었습니다;
Weak<Node>
참조자는 (Weak)로 출력됩니다:
6.2.3. strong_count와 weak_count의 변화를 시각화하기

- 참조 카운트와 값 버리기를 관리하는 모든 로직은
Rc<T>
와 Weak<T>
, 그리고 이들의 Drop 트레이트에 대한 구현부에 만들어져 있습니다.
- 자식과 부모의 관계가
Weak<T>
참조자로 있어야 함을 Node의 정의에 특정함으로써,
- 여러분은 순환 참조와 메모리 누수를 만들지 않으면서
- 자식 노드를 가리키는 부모 노드 혹은 그 반대의 것을 만들 수 있습니다.