포인터
메모리의 주소값을 담고 있는 변수. 이 주소값은 어떤 다른 데이터를 참조
러스트에서 가장 흔한 종류의 포인터는 참조자(&)로 변수가 가리키고 있는 값을 빌려옴
이들은 값을 참조하는 것 외에 다른 특별한 능력은 없으며 오버헤드도 없음
스마트 포인터
포인터처럼 작동할 뿐만 아니라 추가적인 메타데이터와 능력을 가진 데이터 구조
스마트 포인터의 개념은 러스트의 고유적인 것은 아니며, C++로 부터 유래됨
참조자가 데이터를 빌리기만 하는 반면 대부분의 경우 스마트 포인터는 가리킨 데이터를 소유함
주요 스마트 포인터
- 참조 카운팅 (reference counting)
소유자의 개수를 계속 추적하고 더 이상 소유자가 없으면 데이터를 정리하는 방식으로 어떤 데이터에 대한 여러 소유자를 만들게 해주는 스마트 포인터
- String과 Vec<T>
어느 정도 메모리를 소유하고 이를 다룰 수 있으면서, 메타데이터와 추가 능력 또는 보장성을 가지기 때문에 스마트 포인터의 일종으로 볼 수 있음
- Box<T>
값을 힙에 할당. 컴파일 타임에 검사되는 불변 혹은 가변 대여를 허용
- Rc<T>
복수 소유권을 가능하게 하는 참조 카운팅 타입
컴파일 타임에 검사되는 불변 대여만 허용
- RefCell<T>
대여 규칙을 컴파일 타임 대신 런타임에 강제하는 타입
런타임에 검사되는 불변 혹은 가변 대여를 허용
- Ref<T> / RefMut<T>
RefCell<T>를 통해 접근 가능한 타입
스마트 포인터의 트레이트
스마트 포인터는 보통 구조체를 이용하여 구현
보통의 구조체와는 달리 Deref와 Drop 트레티르를 구현
- Deref 트레이트
스마트 포인터 구조체의 인스턴스가 참조자처럼 작동하도록 하여
참조자 혹은 스마트 포인터와 함께 작동하는 코드를 작성할 수 있도록 해줌
- Drop 트레이트
스마트 포인터의 인스턴스가 스코프 밖으로 벗어났을 때 실행되는 코드를 커스터마이징 가능하도록 해줌
Box<T>를 사용하여 힙에 있는 데이터 가리키기
박스는 가장 직관적인 스마트 포인터로 스택이 아닌 힙에 데이터를 저장할 수 있도록 해줌
스택에 남는 것은 힙 데이터를 가리키는 포인터로 오버헤드를 가지지 않음
자주 사용하는 상황
- 컴파일 타임에는 크기를 알 수 없는 타입이 있는데, 정확한 크기를 요구하는 콘텍스트 내에서 그 타입의 값을 사용하고 싶을 때
- 커다란 데이터를 가지고 있고 소유권을 옮기고 싶지만 그렇게 했을 때 데이터가 복사되지 않을 것을 보장하고 싶을 때
- 어떤 값을 소유하고 이 값의 구체화된 타입보다는 특정 트레이트를 구현한 타입이라는 점만 신경 쓰고 싶을 때
Box<T>를 사용하여 힙에 데이터 저장하기
fn main() {
let b = Box::new(5); // 5는 힙에 할당됨
println!("b = {}", b);
}
이 경우 박스안에 있는 데이터를 스택에 있는 것처럼 접근 가능
b가 main 끝에 도달하는 것처럼 어떤 박스가 스코프를 벗어날 때, 다른 어떤 소유된 값과 마찬가지로 할당은 해제됨
할당 해제는 박스와 박스가 가리키고 있는 힙에 저장된 데이터 모두에게 일어남
그러나 단일 값을 힙에 집어넣는 것은 그다지 유용하지 않으므로 자주 쓰이지 않을 방식임
박스로 재귀적 타입 가능하게 하기
재귀적 타입 (recursive type)의 값은 자신 안에 동일한 타입의 또 다른 값을 담을 수 있음
러스트는 컴파일 타임에 어떤 타입이 얼마만큼의 공간을 차지하는지 알아야하기 때문에 재귀적 타입은 문제를 일으킴
박스는 알려진 크기를 갖고 있으므로 재귀적 타입은 정의에 박스를 집어넣어서 구현할 수 있음
콘스 리스트 (cons list)
Lisp 프로그래밍 언어 및 그의 파생 언어들로 부터 유래된 데이터 구조로서 중첩된 쌍으로 구성되며 연결 리스트의 Lisp 버전
이 이름은 Lisp의 생성함수인 cons로 부터 유래되었는데
이 함수는 두 개의 인수로 부터 새로운 쌍을 생성
콘스 리스트의 각 아이템은 두 개의 요소를 담고 있음 (현재 아이템의 값과 다음 아이템)
리스트의 마지막 아이템은 다음 아이템이 없이 Nill 이라는 값을 담음
콘스 리스트는 러스트에서 흔히 사용되는 데이터 구조는 아니고, 아이템 리스트를 쓰는 대부분의 경우에는 Vec<T>가 더 나은 선택
enum List {
Cons(i32, Box<List>),
Nil,
}
use create::List::{Conse, Nil};
fn main() {
let list = Cons(1, Box::new(Cons(2, Box::new(Cons(3, Box::new(Nil))))));
}
Deref 트레이트로 스마트 포인터를 보통의 참조자처럼 취급하기
Deref 트레이트를 구현하면 역참조 연산자(dereference operator) * 동작의 커스터마이징이 가능
스마트 포인터가 보통의 참조자처럼 취급될 수 있도록 구현함으로써 작성된 코드가 스마트 포인터에도 사용되게 할 수 있음
use std::ops::Deref;
struct MyBox<T>(T);
impl<T> Deref for MyBox<T> {
type Target = T; // Deref 트레이트가 사용할 연관 타입 정의
fn deref(&self) -> &Self::Target {
&self.0 // 튜플 구조체의 첫번째 항목
}
}
fn main() {
let x = 5;
let y = MyBox::new(x);
assert_eq!(5, x);
assert_eq!(5, *y); // *y => *(y.deref())
}
함수와 메서드를 이용한 암묵적 역참조 강제 (deref coercion)
역참조 강제는 Deref를 구현한 어떤 타입의 참조자를 다른 타입의 참조자로 바꿔줌
예를들어 &String을 &str로 바꿔줄 수 있는건 String의 Deref 트레이트 구현이 &str을 반환하도록 되어있기 때문
역참조 강제는 러스트가 함수와 메서드의 인수에 대해 수행해주는 편의성 기능이고 Deref 트레이트를 구현한 타입에 대해서만 작동함
이는 어떤 특정한 타입 값에 대한 참조자를 함수 혹은 메서드의 인수로 전달할 때, 이 함수나 메서드의 정의에는 그 매개변수 타입이 맞지 않을 때 자동으로 발생
역참조 강제는 함수와 메서드 호출을 작성하는 프로그래머들이 &와 *를 사용하여 수많은 명시적인 참조 및 역참조를 추가할 필요가 없도록 하기 위해 도입됨
또한 참조자나 스마트 포인터 둘 중 어느 경우라도 작동되는 코드를 쉽게 작성할 수 있도록 해줌
fn main() {
// 역참조 강제를 사용한 케이스
let m1 = MyBox::new(String::from("Rust"));
hello(&m1);
// 역참조 강제가 없었을 경우 작성했어야 하는 코드
let m2 = MyBox::new(String::from("Rust"));
hello(&(*m)[..]);
}
역참조 강제가 가변성과 상호작용하는 법
Deref 트레이트와 DerefMut 트레이트를 사용하여 불변 및 가변 참조자에 대한 *를 오버라이딩 할 수 있음
- T: Deref<Target=U> 일 때 &T에서 &U로
- T: DerefMut<Target=U> 일 때 &mut T에서 &mut U로
- T: Deref<Target=U> 일 때 &mut T에서 &mut U로
세 번째 예시와 같이 러스트는 가변 참조자를 불변 참조자로 강제할 수 있지만, 그 반대로는 불가능함
Drop 트레이트로 메모리 정리 코드 실행하기
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!("CustomSmartPointer created");
}
// result
// CustomSmartPointer created
// Dropping CustomSmartPointer with data: other stuff
// Dropping CustomSmartPointer with data: my stuff
러스트는 인스턴스가 스코프 밖으로 벗어났을 때 drop을 호출했고 지정해두었던 코드를 실행함
변수는 만들어진 순서의 역순으로 버려지므로 d가 c보다 먼저 버려짐
std::mem::drop으로 값을 일찍 버리기
러스트에서는 drop을 명시적으로 호출하는 것을 허용하지 않음
이는 러스트가 main의 끝 부분에서 그 값에 대한 drop 호출을 자동으로 할 것이기 때문임
때문에 동일한 값에 대한 두 번 메모리 정리로 인해 중복 해제 에러가 발생할 수 있음
어떤 값이 스코프 밖으로 벗어났을 때의 자동적인 drop 호출을 비활성화 할 수 없고 drop 메서드를 명시적으로 호출할 수 없음
어떤 값에 대한 메모리 강제를 일찍하기 원할 때는 std::mem::drop 함수를 이용
이 함수는 Drop 트레이트에 있는 drop 메서드와는 다름
이 함수는 프렐루드에 포함되어 있어서 아래와 같이 사용 가능
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer Created");
drop(c);
println!("CustomSmartPointer dropped");
}
// result
// CustomSmartPointer Created
// Dropping CustomSmartPointer with data: some data
// CustomSmartPointer dropped
Rc<T> 참조 카운트 스마트 포인터
명시적으로 복수 소유권을 가능하게 하려면 러스트의 Rc<T> 타입을 이용해야 하는데
이는 참조 카운팅 (reference counting)의 약자임
Rc<T> 타입은 어떤 값의 참조자 개수를 추적하여 해당 값이 계속 사용 중인지를 판단
만일 어떤 값의 대한 참조자가 0개라면 해당 값은 참조 유효성 문제 없이 메모리에서 정리될 수 있음
Rc<T> 타입은 프로그램의 여러 부분에서 읽을 데이터를 힙에 할당하고 싶은데 컴파일 타임에는 어떤 부분이 그 데이터를 마지막에 이용하게 될지 알 수 없는 경우 사용
만일 어떤 부분이 마지막으로 사용하는지 알았다면 그 해당 부분을 데이터의 소유자로 만들면 되고 보통의 소유권 규칙이 컴파일 타임에 수행되어 효력을 발생시킬 것임
Rc<T>는 싱글스레드 시나리오용으로, 멀티 스레드 프로그램에서는 다른 방법으로 참조 카운팅을 해야함
Rc<T>를 사용하여 데이터 공유하기
enum List {
Cons(i32, Rc<List>),
Nil,
}
use create::List::{Cons, Nil};
use std::rc::Rc;
fn main() {
let a = Rc::new(Cons(5, Box::new(Cons(10, Box::new(Nil)))));
let b = Cons(3, Rc::clone(&a)); // a.clone을 사용할 수 있지만 러스트의 관례 상 Rc::clone를 사용
let c = Cons(4, Rc::clone(&a));
println!("count {}", Rc::strong_count(&a));
}
Rc<T>를 클론하는 것은 참조카운트를 증가시킴
내부 가변성 패턴 (interior mutability)
어떤 데이터에 대한 불변 참조자가 있을 때라도 데이터를 변경할 수 있게 해주는 러스트의 디자인패턴
불변 값 내부의 값을 변경하기 위해 사용
보통 이러한 동작은 대여규칙에 의해 허용되지 않지만,
이 패턴을 통해 안전하지 않은 (unsafe) 코드를 사용하여 변경과 대여를 지배하는 러스트의 일반적인 규칙을 우회
안전하지 않은 코드는 이 규칙들을 지키고 있는지에 대한 검사를 컴파일러 대신 수동으로 함을 컴파일러에게 알림
RefCell<T>로 런타임에 대여 규칙 집행하기
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 over your quota");
} else if percentage_of_max >= 0.9 {
self.messenger.send("Urgent warning: you over 90% of quota");
} else if percentage_of_max >= 0.75 {
self.messenger.send("Warning: you over 75% of quota");
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
struct MockMessenger {
send_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);
}
}
borrow를 호출할 때 마다 RefCell<T>는 불변 참조자가 활성화된 개수를 증가시킴
Ref<T> 값이 스코프 밖으로 벗어날때는 불변 대여의 개수가 하나 감소
컴파일 타임에서의 대여 규칙과 똑같이 RefCell<T>는 어떤 시점에서든 여러 개의 불변 대여 혹은 하나의 가변 대여를 가질 수 있도록 해줌
대여를 컴파일 타임이 아닌 런타임에 잡게하면 개발 과정 이후에 코드에서의 실수를 발견할 수 있게됨
또한 런타임에 대여를 추적하는 결과로 약간의 런타임 성능 패널티를 초래함
그러나 RefCell<T>를 이용하는 것은 오직 불변 값만 허용된 콘텍스트 안에서 사용하는 중에 본 메시지를 추적하기 위해서 스스로를 변경할 수 있는 목 객체 작성을 가능하게 해줌
트레이드오프가 있더라도 RefCell<T>를 사용하여 일반적인 참조자가 제공하는 것보다 더 많은 기능을 얻을 수 있음
Rc<T>와 RefCell<T>를 조합하여 가변 데이터의 복수 소유자 만들기
RefCell<T>를 사용하는 일반적인 방법은 Rc<T>와 조합하는 것임
Rc<T>가 어떤 데이터에 대해 복수의 소유자를 허용하지만, 그 데이터에 대한 불변 접근만 제공함
그런데 RefCell<T>를 들고 있는 Rc<T>를 가지게 되면 가변이면서 동시에 복수의 소유자를 갖는 값을 만들 수 있음
#[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));
// a, b, c 리스트가 생성된 이후 value 값에 10을 더함
// 자동 역참조를 통해 Rc<T>를 역참조하여 RefCell<T> 값을 얻어옴
// borrow_mut는 RefMut<T> 스마트 포인터를 반환하고 여기에 역참조 연산자를 사용한 다음 내부 값을 변경
*value.borrow_mut() += 10;
println!("a after = {:?}", a); // a after = Cons(RefCell { value: 15 }, Nil)
println!("b after = {:?}", b); // b after = Cons(RefCell { value: 3 }, Cons(RefCell { value: 15 }, Nil))
println!("c after = {:?}", c); // a after = Cons(RefCell { value: 4 }, Cons(RefCell { value: 15 }, Nil))
}
Rc<T> 및 RefCell<T>를 사용하면 메모리 누수가 허용될 수 있기 때문에 순환 참조가 발생하지 않도록 주의해야함
내부 가변성 및 참조 카운팅 기능이 있는 타입들의 중첩된 조합을 사용한다면 개발자가 직접 순환을 만들지 않음을 보장해야함
순환 참조자를 피하는 또 다른 해결책은 데이터 구조를 재구성하여 어떤 참조자는 소유권을 갖고, 어떤 참조자는 그렇지 않도록 하는 것임
순환 참조 방지하기: Rc<T>를 Weak<T>로 바꾸기
use std::cell::RefCell;
use std::rc::{Rc, Weak};
#[derive(Debug)]
struct Node {
value: i32,
parent: RefCell<Weak<Node>>,
children: RefCell<Weak<Node>>,
}
fn main() {
let leaf = Rc::new(Node {
value: 3,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![]),
});
// upgrade 메서드를 사용하여 leaf의 부모에 대한 참조자를 얻는 시도 시 Non 값을 얻음
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
let branch = Rc::new(Node, {
value: 5,
parent: RefCell::new(Weak::new()),
children: RefCell::new(vec![Rc::clone(&leaf)]),
});
// branch의 Node 인스턴스를 갖게되면 leaf를 수정하여 자기 부모에 대한 Weak<Node> 참조자를 갖도록 할 수 있음
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);
// branch를 갖고 있는 Some 배리언트를 얻게됨
println!("leaf parent = {:?}", leaf.parent.borrow().upgrade());
}
// Result
// leaf parent = Some(Node { value: 5, parent: RefCell { value: (Weak) },
// chlidren: RefCell { value: [Node, { value: 3, parent: RefCell { value: (Weak) },
// chlidren: RefCell { value: [] } }] } })
Rc::downgrade에 Rc<T>의 참조자를 넣어서 호출하면 Rc<T> 인스턴스 내의 값을 가리키는 약한 참조를 만드는 것도 가능
강한 참조는 Rc<T> 인스턴스의 소유권을 공유할 수 있는 방법
약한 참조는 소유권 관계를 표현하지 않음
약한 참조의 개수는 Rc<T> 인스턴스가 제거되는 경우에 영향을 주지 않기 때문에 약한 참조가 포함된 순환 참ㅈ모는 그 값의 강한 참조 개수를 0으로 만드는 순간 깨지게 되면서 순환 참조를 일으키지 않음
Rc::downgrade를 호출하면 Weak<T> 타입의 스마트 포인터를 얻게됨
Rc::downgrade는 Rc<T> 인스턴스의 strong_count를 1 증가시키지 않고 weak_count를 1 증가시킴
여기서 Rc<T> 인스턴스가 제거되기 위해 weak_count가 0일 필요는 없음
Weak<T>가 참조하고 있는 값이 이미 버려졌을지도 모르기 때문에 Weak<T>가 가리키고 있는 값으로 어떤 일을 하기 위해서는
반드시 그 값이 여전히 존재하는지 확인해야함
이를 위해서는 Weak<T>의 upgrade 메서드를 호출하는데, 이 메서드는 Option<Rc<T>>를 반환
값이 버려지지 않았다면 Some, 값이 버려졌다면 None 값을 얻게됨
'Programming > Rust' 카테고리의 다른 글
| 러스트의 객체 지향 프로그래밍 기능 (0) | 2025.10.11 |
|---|---|
| 러스트 동시성 (0) | 2025.10.09 |
| 러스트 카고와 crates.io (0) | 2025.10.09 |
| 러스트 반복자와 클로저 (0) | 2025.10.08 |
| 러스트 커맨드라인 프로그램 만들기 (0) | 2025.09.12 |