Rust의 빌림 검사기 관점에서 ‘대출(loan)’이 어떤 제약을 만들고, &own, &uninit, &pin 계열 같은 새로운 참조 타입 아이디어가 서로 어떻게 상호작용하는지 표로 정리한다.
URL: https://nadrieril.github.io/blog/2025/12/21/the-algebra-of-loans-in-rust.html
Rust 빌림 검사기의 핵심은 이것입니다. 어떤 빌림(borrow)이 발생하면, 그 빌림이 만료되기 전까지는 빌린 place에 대한 접근이 제한됩니다. 예를 들어 어떤 place가 가변으로 빌려져 있는 동안에는 그 place에서 읽을 수 없습니다.
최근 몇 달 동안, 빌림 검사기가 단순히 “공유 빌림(shared borrow)”/“가변 빌림(mutable borrow)”만이 아니라 더 많은 것들을 이해하도록 가르치자는 활발한 논의가 있어 왔습니다. 이제 몇 가지 아이디어가 떠돌기 시작했고, 서로 어떻게 상호작용하는지 보기 위해 표로 모두 정리해 두면 좋겠다고 생각했습니다. 먼저 표부터 제시하고, 그 다음에 새 참조 타입들을 설명하겠습니다.
제가 사용할 용어를 명확히 하자면:
x, *x, x.field 같은 표현으로 나타냅니다1.&place, &mut place, &own place 등을 평가해서 값으로 만드는 동작입니다.주의: 이 표들만으로 이 새 참조들을 전부 이해할 수 있는 것은 아닙니다. 예를 들어 &uninit에 쓴(write) 후에는 그것을 &own으로 다시 빌릴(reborrow) 수 있고 그 반대도 가능한데, 이는 표에 반영되어 있지 않습니다. 또 다른 예로, 어떤 place의 내용을 drop하면 그 place에 걸려 있던 pinning 제한이 제거됩니다.
이 표를 읽는 법: 내가 가진 참조가 열(column)을 결정하고, 내가 하고 싶은 동작이 행(row)을 결정합니다(참조 타입이 쓰여 있는 동작들은 그 타입으로의 reborrow를 의미합니다).
예를 들어 &own T를 가지고 있다면 &mut T로는 reborrow할 수 있지만 &pin mut T로는 할 수 없습니다. &mut T를 가지고 있다면 새 값을 쓸 수는 있지만 내용을 drop할 수는 없습니다.
“Move out”은 “새 값을 넣지 않고 값을 밖으로 move하기”를 뜻합니다. “Drop”은 “그 자리에서(drop in-place) drop 코드를 실행하기”를 뜻합니다.
| & | &mut | &own | &pin | &pin mut | &pin own | &uninit | |
|---|---|---|---|---|---|---|---|
| & | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| &mut | ❌ | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |
| &own | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| &pin | ❌ | ❌ | ❌ | ✅ | ✅ | ✅ | ❌ |
| &pin mut | ❌ | ❌ | ❌ | ❌ | ✅ | ✅ | ❌ |
| &pin own | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
| &uninit | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ✅ |
| 읽기(Read) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ❌ |
| 쓰기(Write) | ❌ | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
| Move out | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ | ❌ |
| Drop | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ❌ |
이 표를 읽는 법: place p에 대한 빌림이 취해졌고 아직 살아 있습니다. 빌림의 종류가 열(column)을 결정합니다. 그 빌림을 “통하지 않고” place 자체에 대해 내가 여전히 할 수 있는 연산이 행(row)입니다.
예를 들어 & 빌림이 취해져 있고 살아 있다면, 그 place를 여전히 읽을 수 있습니다.
| & | &mut | &own | &pin | &pin mut | &pin own | &uninit | |
|---|---|---|---|---|---|---|---|
| & | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| &mut | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &own | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &pin | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| &pin mut | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &pin own | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &uninit | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| 읽기(Read) | ✅ | ❌ | ❌ | ✅ | ❌ | ❌ | ❌ |
| 쓰기(Write) | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Move out | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Drop | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ | ❌ |
놀랍지 않게도, 대부분의 경우 place에 대해 다른 일을 할 수 없습니다. 왜냐하면 빌림이 배타적(exclusive)이기 때문입니다.
이 표를 읽는 법: place p에 대한 빌림이 취해졌고 이제 만료되었습니다. 빌림의 종류가 열(column)을 결정합니다. 이제 그 place에 대해 가능한 연산이 행(row)입니다.
예를 들어 &own 빌림이 취해졌다가 만료되면, 이제는 그 place를 더 이상 읽을 수 없습니다.
| & | &mut | &own | &pin | &pin mut | &pin own | &uninit | |
|---|---|---|---|---|---|---|---|
| & | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| &mut | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &own | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| &pin | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| &pin mut | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| &pin own | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| &uninit | ❌ | ❌ | ✅ | ❌ | ❌ | ✅ | ✅ |
| 읽기(Read) | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
| 쓰기(Write) | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| Move out | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ | ❌ |
| Drop | ✅ | ✅ | ❌ | ✅ | ✅ | ❌ | ❌ |
&own과 &uninit은 둘 다 “만료될 때 그 place가 초기화되지 않은(uninitialized) 것으로 간주된다”는 성질을 갖습니다. 그래서 가능한 동작은 초기화되지 않은 place에서 동작하는 것들(쓰기와 &uninit 빌림)뿐입니다.
또 하나 주목할 점은 pinning 대출입니다. pinning 대출이 만료된 뒤에는, 그 값이 drop될 때까지 같은 place에 대해 non-pinning 가변 대출을 다시 취할 수 없습니다.
이 모든 것은 추측(speculative) 아이디어이지만, 이 시점에서는 꽤 많이 공유되어 왔기 때문에 제법 탄탄하다고 볼 수 있습니다.
&own T&own T2(&move T라고도 불림)는 “이 값에 대한 완전한 소유권을 내가 가진다. 특히 drop할 책임이 나에게 있다”를 뜻하는 참조입니다. 값이 놓인 할당(allocation)을 우리가 제어하지 못한다는 점을 제외하면 Box처럼 느껴집니다. 특히, 다음과 같은 형태의 참조에서 T를 밖으로 move할 수 있습니다.
&own
T
Box처럼 &own T도 그것이 drop될 때 내부 값을 drop합니다.
어떤 place를 &own x로 소유 빌림(owning-borrow)하면, 나는 그 값에 대한 완전한 소유권을 포기한 것이며, 따라서 그 빌림이 만료되면 place가 초기화되지 않았다고 가정해야 합니다.
흥미로운 API를 만들 수 있습니다:
rustimpl<T> Vec<T> { // 마지막 값을 pop하고, 그것을 직접 move해서 돌려주는 대신 // 그 값에 대한 참조를 반환한다. fn pop_own(&mut self) -> Option<&own T> { .. } // 담긴 값들을 순회하면서 Vec를 비워 나간다. fn drain_own(&mut self) -> impl Iterator<Item = &own T> { .. } }
&uninit T&uninit T3(&out T라고도 불림)는 할당은 되어 있지만 아직 초기화되지 않은 위치에 대한 참조입니다. let x;를 했을 때와 비슷하게, 그 참조로 할 수 있는 일은 오직 쓰기(write)뿐입니다. 한 번 값을 써서 초기화하고 나면, 원하는 무엇이든지로 다시 빌릴(reborrow) 수 있습니다.
rust// 전형적인 용례는 값을 초기화하는 것: impl MyType { fn init(&uninit self) -> &own Self { *self = new_value(); &own *self } } let x: MyType; let ptr: &own MyType = MyType::init(&uninit x); // `ptr`는 `Box`처럼 사용할 수 있다. // 하지만 현재 함수에서 반환할 수는 없다.
&own T와도 좋은 시너지가 있습니다. &uninit T에 값을 써서 &own T로 갈 수 있고, &own T에서 값을 밖으로 move해서 &uninit T로 갈 수도 있습니다.
둘 다 “만료될 때 원래 place가 미초기화로 간주된다”는 성질을 갖습니다4.
&pin T / &pin mut T / &pin own TPinning은 악명 높을 정도로 미묘한 개념입니다. 익숙하지 않다면 이 주제에 대한 std 문서를 추천합니다. 익숙하다면, 개념에 대한 독창적인 관점을 비추는 제가 쓴 블로그 글도 즐길 수 있을 겁니다.
Pinning 참조는 기존 참조 타입들의 변형으로, 빌린 place에 “pinning 요구사항”을 추가합니다. 이 요구사항은 값을 밖으로 move하거나, 값을 먼저 Drop 실행 없이 그 place를 해제(deallocate)하는 것을 금지합니다. 이 금지는 pinning 빌림이 만료된 이후에도 적용됩니다.
&pin mut T5는 이들 중 가장 흔한 형태로, 오늘날 Rust에서는 Pin<&mut T>로 존재하며 async 스토리에 핵심적입니다. &pin T는 Pin<&T>가 될 것입니다. 얼마나 유용한지는 덜 명확하지만, 용례를 상상할 수는 있습니다.
마지막으로 &pin own T는 소유(owning) 변형입니다. 이것에는 까다로운 요구사항이 하나 있는데, mem::forget에 넘기면 drop 불변식(invariant)이 깨지므로 그렇게 넘겨서는 안 된다는 것입니다. 오늘날 Rust에서는 이런 것이 불가능하지만, forget 불가능(non-forgettable) 타입은 다른 목적에서도 필요하기 때문에 수년간 여러 제안이 있었습니다.
&pin own T의 재미있는 점 하나는, &own T를 &pin own T로 reborrow하는 것이 괜찮다고 생각된다는 것입니다. 왜냐하면 그 빌림이 만료되는 유일한 방식은 가리키는 값을 drop하는 것이고, 그러면 pin 보장을 깨뜨릴 방법이 없기 때문입니다. 따라서 그 참조를 어떻게 얻었는지는 중요하지 않습니다.
제가 &pin uninit T를 나열하지 않은 것을 눈치챘을 겁니다. pinning은 값(value)의 성질이고, &pin uninit T는 값을 가리키지 않기 때문입니다. 값을 pin-초기화하려면 &uninit T에 값을 쓴 다음 &uninit T -> &own T -> &pin own T로 reborrow하면 됩니다.
place에 대한 더 자세한 내용은 이 주제에 대한 제 블로그 글에서 볼 수 있습니다.↩
여러 번 제안된 바 있습니다. 제가 이 주제에서 자주 참고하는 글은 Daniel Henry-Mantilla가 쓴 이 글입니다.↩
이것 역시 몇 번 제안된 바 있습니다. 여기서는 비교적 온건한(tame) 형태로 소개하지만, 제안들은 보통 더 많은 기능을 담고 있습니다. 제가 가장 잘 아는 제안은 in-place-init 작업 그룹이 쓴 이 문서입니다.↩
위 코드에서 &own MyType을 사용해 x가 이제 초기화되었음을 borrowck에 증명할 수 있게 해 주는 제안들도 있지만, 여기서는 다루지 않겠습니다.↩
Pinning은 Rust에 몇 년 전부터 존재해 왔지만, 이제 이를 언어에 제대로 통합하려는 추진이 있으며, 제가 사용한 문법은 거기에서 가져온 것입니다.↩