# Rust의 std::pin::Pin은 무엇인가?

> Clean Markdown view of GeekNews topic #30974. Use the original source for factual precision when an external source URL is present.

## Metadata

- GeekNews HTML: [https://news.hada.io/topic?id=30974](https://news.hada.io/topic?id=30974)
- GeekNews Markdown: [https://news.hada.io/topic/30974.md](https://news.hada.io/topic/30974.md)
- Type: GN+
- Author: [neo](https://news.hada.io/@neo)
- Published: 2026-07-01T00:04:49+09:00
- Updated: 2026-07-01T00:04:49+09:00
- Original source: [vrong.me](https://vrong.me/blog/what-is-pinning-in-rust/)
- Points: 1
- Comments: 1

## Topic Body

- `std::pin::Pin`은 포인터가 가리키는 값이 그 포인터를 통해 이동되지 않는다는 **타입 수준 보장**을 표현하며, 자기 자신 내부를 참조하는 타입처럼 주소가 안정적이어야 하는 값 때문에 필요함
- `async`/`await`에서는 `.await`를 넘어 살아남는 지역 변수와 참조가 컴파일러 생성 **상태 머신**의 필드가 될 수 있어, 폴링 이후 future 이동을 막기 위해 `Future::poll`이 `Pin<&mut Self>`를 요구함
- `Pin&lt;P&gt;`는 고정된 값을 **안전한 코드로 이동**하는 일을 막지만 일반적인 변경까지 금지하지는 않으며, `T: Unpin`이 아니면 안전하게 `Pin<&mut T>`에서 `&mut T`를 꺼낼 수 없음
- Rust 타입 대부분은 기본적으로 **Unpin**이므로, 이동되면 안 되는 자기 참조 구조체는 보통 `PhantomPinned` 필드를 넣어 `!Unpin`으로 만들어야 함
- 실제로는 future를 직접 `poll`하거나 pinned future를 요구하는 API에 넘길 때 `Box::pin` 또는 `std::pin::pin!`을 쓰며, 직접 `Future`나 저수준 async 원시 타입을 구현할 때는 `unsafe` 불변식까지 다뤄야 함

---

### `Pin`이 필요한 이유
- `std::pin::Pin`은 **포인터 래퍼**로, 포인터가 가리키는 값이 그 포인터를 통해 이동되지 않는다는 보장을 나타냄
- 핵심 문제는 **자기 참조 타입**에서 생김
  - 예시 구조체 `SelfRef`는 `data: i32`와 `ptr: *const i32`를 가지며, `ptr`은 `self.data`를 가리킴
  - 구조체 인스턴스를 다른 변수로 이동하거나 함수에서 반환하면 메모리 주소가 바뀔 수 있음
  - 원시 포인터 `ptr`은 이전 메모리 위치를 계속 가리켜 **댕글링 포인터**가 됨
- 자기 참조가 설정된 뒤에는 해당 값이 다시 이동되지 않도록 막는 장치가 필요함

### `async`/`await`와 `Future`에서 생기는 문제
- `async`/`await`와 **Future**는 `Pin`이 자주 등장하는 대표적인 영역임
- `.await` 지점을 넘어 살아남는 지역 변수는 컴파일러가 생성하는 상태 머신의 필드가 됨
- 어떤 지역 변수에 대한 참조도 같은 `.await`를 넘어 살아남으면, 생성된 future가 **자기 참조적**일 수 있음
- 폴링이 시작된 뒤 future는 자기 내부의 다른 필드를 가리키는 참조에 의존할 수 있음
  - 이 상태에서 future가 이동되면 해당 참조가 무효화됨
- 이를 막기 위해 `Future::poll`은 `&mut self` 대신 `Pin<&mut Self>`를 받음

```rust
pub trait Future {
    type Output;
    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll&lt;Self::Output&gt;;
}
```

- 호출자는 `poll`을 부른 뒤 future가 이동되지 않는다는 보장을 제공해야 함

### `Pin`이 막는 것과 허용하는 것
- `Pin&lt;P&gt;`는 고정된 포인터를 통해 가리키는 값을 **안전한 코드로 이동**하지 못하게 막음
- 값의 일반적인 변경은 허용됨
  - 고정된 타입의 메서드는 필드를 변경할 수 있음
  - 다만 pinning에 의존하는 필드를 값 밖으로 이동시키면 안 됨
- ## `&mut T`의 한계
  - `&mut T`가 있으면 `mem::replace`, `mem::swap`, 대입 같은 연산으로 해당 메모리 위치의 값을 재배치할 수 있음
  - `Pin`은 일반적인 가변 참조를 되찾는 일을 제한함
  - `T: Unpin`이 아니면 안전한 코드로 `Pin<&mut T>`에서 `&mut T`를 꺼낼 수 없음

  ```rust
  impl<'a, T: ?Sized> Pin<&'a T> {
      pub const fn get_ref(self) -> &'a T { ... }
  }
  
  impl<'a, T: ?Sized> Pin<&'a mut T> {
      pub const fn get_mut(self) -> &'a mut T
      where
          T: Unpin
      { ... }
  }
  ```

  - 타입이 `Unpin`을 구현하지 않는 `!Unpin`이면, 안전한 코드만으로는 `&mut T`를 얻을 수 없음
  - 이 경우 `Pin::get_unchecked_mut` 같은 **unsafe 메서드**를 써야 하며, 값이 그 참조 밖으로 이동되지 않는다는 약속을 코드가 지켜야 함

### `Unpin`과 `PhantomPinned`
- `Unpin`을 구현하는 타입은 메모리 안전성을 위해 pinning에 의존하지 않음

```rust
// std::marker
pub auto trait Unpin {}
```

- Rust의 대부분 타입은 이동되어도 문제가 없어 기본적으로 `Unpin`임
  - 예: `i32`, `String`, `Vec`
- `Unpin`은 명시적으로 `!Unpin`을 만들지 않는 한 모든 타입에 자동 구현됨
- `std::marker::PhantomPinned`는 명시적으로 **`!Unpin`** 인 마커 구조체임
  - auto trait은 자동 전파되므로, `PhantomPinned` 필드를 포함한 구조체도 자동으로 `!Unpin`이 됨

```rust
use std::marker::PhantomPinned;

struct SelfRef {
    data: i32,
    ptr: *const i32,
    _phantom: PhantomPinned, // makes the entire struct !Unpin
}
```

- 사용자 정의 구조체가 고정된 뒤 이동되면 안전하지 않다는 점을 선언하는 표준 방식임
- 컴파일러는 보통 unsafe 원시 포인터로 만들어지는 자기 참조를 자동 감지할 수 없음
- 따라서 개발자가 자기 참조 구조체에 대해 명시적으로 `Unpin`을 포기해야 함
  - 보통 `PhantomPinned` 필드를 포함하는 방식으로 처리함
- 자기 참조 타입이 실수로 `Unpin` 상태로 남아 있으면, 안전한 코드가 `Pin`에서 가변 참조를 꺼내 값을 이동할 수 있음
  - 그러면 자기 참조를 만든 unsafe 코드의 가정이 깨짐

### `Pin`을 만드는 방법
- `Pin` 자체가 값을 고정하는 것은 아님
- `Pin`을 만든다는 것은 해당 pointee가 pin의 수명 동안 **안정적인 메모리 위치**에 남는다는 점을 증명하는 일임
- ## `Pin::new`
  - 가장 단순한 생성 방식은 `Pin::new`임

  ```rust
  let mut value = 42;
  let pinned = Pin::new(&mut value);
  ```

  - 이 생성자는 `T: Unpin`일 때만 사용할 수 있음
  - `Unpin` 타입은 pinning에 의존하지 않으므로, `Pin`으로 감싸도 항상 안전함
  - 이 경우 pinning 보장은 사실상 **no-op**임
- ## `std::pin::pin!`
  - 힙 할당 없이 지역적으로 값을 pin해야 할 때 `pin!` 매크로를 사용할 수 있음

  ```rust
  use std::pin::pin;
  
  let future = pin!(async {
      println!("Hello");
  });
  ```

  - 이 매크로는 지역 변수를 만들고, 그 변수를 가리키는 `Pin<&mut T>`를 반환함
  - 컴파일러가 해당 지역 변수를 남은 수명 동안 이동되지 않게 보장하므로, 스택에서 `!Unpin` 값을 안전하게 pin할 수 있음
  - 이름과 달리 `pin!`은 **스택 메모리 자체**를 pin하지 않음
  - 지역 변수에 묶인 고정 참조를 만들 뿐이며, 변수가 스코프를 벗어나면 pinning 보장도 끝남
- ## `Box::pin`
  - `!Unpin` 타입에서 가장 흔한 생성자는 `Box::pin`임

  ```rust
  let pinned = Box::pin(SelfRef { ... });
  ```

  - `pin!`은 지역 변수에 묶인 `Pin<&mut T>`를 만들지만, `Box::pin`은 `Box`가 소유하는 `Pin<Box&lt;T&gt;>`를 반환함
  - 힙 할당 자체는 이동하지 않으므로 pointee는 `Box`의 수명 동안 안정적인 메모리 위치를 가짐
  - `Box` 자체를 이동해도 소유한 값은 이동하지 않고, `Box` 안의 포인터만 이동됨
  - 힙 할당은 같은 주소에 남음
- ## `Pin::new_unchecked`
  - 안전한 생성자가 값이 제자리에 남는다는 점을 증명할 수 없을 때는 unsafe 코드로 `Pin`을 직접 만들 수 있음

  ```rust
  let pinned = unsafe { Pin::new_unchecked(ptr) };
  ```

  - `Pin::new_unchecked` 호출자는 반환된 `Pin`의 수명 동안 pointee가 어떤 포인터를 통해서도 다시 이동되지 않는다고 약속함
  - 이 약속이 깨지면 pinning 보장에 의존하는 코드에서 **정의되지 않은 동작**이 발생할 수 있음
  - 따라서 보통 이 불변식을 지킬 수 있는 저수준 추상화를 구현할 때만 사용됨

### 실제로 신경 써야 하는 경우
- 대부분의 Rust 개발자에게 `Pin`과 `Unpin`은 배경에서 조용히 동작함
- 직접 신경 써야 하는 경우는 주로 두 가지임
  - **async 코드 소비**: future를 직접 `poll`하거나 pinned future를 요구하는 API에 전달해야 하면 `Box::pin(future)`로 힙에 pin하거나 `std::pin::pin!(future)`로 로컬 스택에 pin함
  - **Future 직접 구현**: 사용자 정의 상태 머신이나 저수준 async 원시 타입을 작성할 때 `Pin<&mut Self>`를 다뤄야 하며, pinning 불변식을 지키기 위해 `PhantomPinned`와 unsafe 코드가 필요할 수 있음
- `Pin`은 주소 민감 타입 문제를 다루는 Rust의 **zero-cost** 해법임
- 이를 통해 Rust는 garbage collector 없이 메모리 안전성 보장을 유지하면서 `async`/`await`와 다른 자기 참조 추상화를 사용할 수 있음

## Comments



### Comment 60872

- Author: neo
- Created: 2026-07-01T00:04:50+09:00
- Points: 1

###### [Lobste.rs 의견들](https://lobste.rs/s/ltzfkv/what_is_std_pin_pin_rust) 
- `std::pin::Pin`은 Rust 세계의 **Monad** 같음. 일단 이해하고 나면 블로그 글을 쓰지 않을 수 없게 됨
  - 그런 글들은 대개 [monad tutorial fallacy](https://byorgey.wordpress.com/2009/01/12/abstraction-intuition-and-the-monad-tutorial-fallacy/)에 걸리기 쉽다
  - Monad 때와 마찬가지로, 그런 블로그 글들이 실제로는 아무것도 제대로 설명하지 못한다는 뜻인가?

- `Pin`을 이해하려고 할 때 나와 다른 사람들이 걸렸던 몇 가지를 다루면 좋을 듯함  
  `Unpin`이라는 이름은 별로 좋지 않음. 더 정확하지만 역시 별로인 이름으로는 `MovableWhenPinned`나 `PinIsNoOp`가 있었을 것임  
  nightly의 **`!Unpin` 이중 부정**은 이상해 보이지만, 기존 타입을 99%의 기본 경우로 두려면 타입이 빠져나갈 수 있는 자동 트레이트 `Unpin`을 추가해야 해서 그렇게 됨. `!MovableWhenPinned`이라고 생각하면 더 말이 됨  
  안정 버전의 대안인 `PhantomPinned`도 이름이 좋지는 않은데, pinned 상태는 pinned 참조가 있어서 생기는 일시적 상태이지 타입의 특성이 아니기 때문임. 대안 이름은 `PhantomNotMovableWhenPinned` 정도가 됐을 것임  
  이런 식으로 머릿속에서 번역하기 시작하니 훨씬 이해가 잘 됐음. 물론 여전히 헷갈리는데 운이 좋았던 것일 수도 있음
  - 완전히 동의함. 예전에는 `!Unpin`이 머리를 아프게 했는데, `Unpin`을 **`SafeToUnpin`** 으로 읽기 시작하니 조금 편해졌음

- 예전에 이 질문을 했고 누군가 사려 깊게 답해줬던 것 같은데 기억이 안 남. 내가 이해한 `Pin`은 async에서 나왔고, 지역 변수 참조가 특정 함수의 상태 기계를 나타내는 데이터 덩어리 안에서 **자기 참조**가 되는 문제였음  
  async 상태가 이동하면 그 지역 변수 참조들은 예전의 잘못된 위치를 가리키게 됨  
  그런데 그건 참조가 완전한 절대 주소를 가진 실제 포인터이기 때문에만 그런 것 아닌가? 왜 해법이 참조를 상대 주소로 만드는 것이 아니라, 이동 능력을 제거하는 것이었는지 궁금함  
  답이 대체로 “컴파일러, CPU, OS가 포인터를 아주 잘 다루도록 수백만 엔지니어-년이 들어갔기 때문에 포인터가 여러 면에서 더 낫고, 그래서 여기저기 `Pin`을 쓰는 편이 낫다”인 건지, 아니면 상대 참조가 대안으로 실제로 성립하지 않는 딱딱한 이유가 있는 건지 궁금함
  - async 상태 안의 지역 변수가 같은 상태 안의 다른 지역 변수를 직접 참조하는 것만의 문제는 아님. 그 경우라면 컴파일러가 모든 지역 변수를 알고 있으니 접근을 상대적으로 만들 수 있음. 하지만 한 타입 깊숙한 곳의 참조가 다른 타입 깊숙한 곳의 값을 가리키면 훨씬 까다로워짐  
    참조가 상대적이라면, 그 타입들은 async 상태 안에서 쓰이는지 아닌지에 따라 **메모리 표현**이 달라져야 하고, 상대 참조에서 실제 포인터를 복원하기 위해 함께 전달해야 하는 기준 포인터 개념도 필요해짐  
    pinned 참조 안의 중첩 객체들은 루트 객체가 pinned되어 있어도 여전히 자유롭게 이동할 수 있으므로, 가상의 상대 참조들이 모두 같은 기준 포인터에 상대적이라고도 할 수 없음  
    결국 절대 포인터가 필요하고 상대 참조는 잘 맞지 않음. 그렇다면 Rust 컴파일러가 여기 있는 타입을 다 아니까, 전체 객체 그래프를 추적해서 이동한 객체를 가리키는 참조를 새 위치로 고치는 방식으로 객체를 이동 가능하게 만들면 어떨까? 그러면 사실상 **추적 가비지 컬렉터**를 만든 셈임  
    게다가 Rust 컴파일러는 객체 그래프의 모든 타입을 알지 못함. 참조는 FFI를 통해 전달될 수 있고 외부 라이브러리가 그 참조를 보관할 수 있음. FFI 경계를 넘는 이동 참조를 고치는 건 사실상 다루기 어려운 문제임  
    그래서 정말 까다롭다. 객체 이동 자체도 비교적 새로운 기법이라는 점도 중요함. 대부분의 C/C++ 프로그램에서는 모든 객체가 암묵적으로 pinned됐다고 볼 수 있음. 그쪽에서 pinning이 덜 논의되는 이유는 객체가 그냥 이동하지 않거나, 이동하더라도 매달린 참조가 남지 않게 하는 책임이 프로그래머에게 있기 때문임
  - `Pin`은 Rust가 메모리를 불투명한 비트 덩어리처럼 마음대로 옮길 수 없는 다른 언어와의 **상호 운용성**에도 필요함  
    내가 이해하기로 C++ 상호 운용성의 문제 중 하나는 객체가 자유롭게 옮길 수 있는 단순한 비트 덩어리가 아니라는 점이고, 결국 꽤 많은 타입에 pinning이 필요해지며 그 사용성이 불편해짐  
    다만 최소 6개월 전쯤 이 작업을 하던 사람들과 나눈 대화를 바탕으로 한 것이라, 그 뒤로 상황이 얼마나 나아졌는지는 모름

- 전반적으로 공식 Rust 문서에 더해 읽기 좋은 설명이라고 봄. 문제로 들어가는 방식이 조금 더 부드럽다  
  다만 **자기 참조 구조체**로 시작하는 것은 차라리 빼는 편보다 더 헷갈리게 만든다고 봄. 특히 도입부의 “따라서 그런 자기 참조가 만들어진 뒤에는 `SelfRef`의 이동을 막을 방법이 필요하다”는 문장은, 핵심보다 “이동을 완전히 막는 문제”를 떠올리게 했음  
  실제 핵심은 훨씬 뒤에 나오는 “`Pin`은 값을 물리적으로 이동하지 못하게 막지 않는다. 대신 그 포인터를 통해 값이 이동되지 않는다는 타입 수준 보장이다”에 있음  
  이동 자체를 막을 수는 없으므로, 안전한 API에서 자기 참조 데이터를 독점 참조 뒤에만 노출하기 위해 `Pin`을 쓰는 것임. 내가 이미 `Pin`을 너무 많이 이해한 상태일 수도 있지만, 설명 방식을 조금 다듬으면 독자가 덜 헤맬 듯함
  - 글을 바꿔서 표현해보겠음  
    이 글은 pinning에 대한 내 노트에서 가져온 것이고, 처음에는 나도 그렇게 이해했음. “이동을 막는다” 같은 문제를 **타입 수준 보장**으로 풀 수 있다는 점이 아름답게 느껴졌음  
    물론 그게 `Pin`이 실제로 하는 일은 아니므로, 그 부분이 드러나도록 글을 고치는 게 맞음

- 이 글 어딘가에 **`!UnPin`은 nightly Rust에서만 표현 가능**하다는 점을 적어둘 만함. 그게 `PhantomPinned`이 존재하는 주된 이유임

- “포인터 래퍼”라고 하는데, Rust에서도 포인터를 다룰 일이 거의 없음. 왜 써야 하는지 모르겠음  
  `*const`는 Google에서 Rust 문서를 찾기 어려운데, 문서화되어 있는지 궁금함  
  “컴파일러가 생성한 상태 기계의 필드가 된다”는 것도 알아야 하는 건가? 아니면 황당한 컴파일러 오류가 실제로 그런 일이 일어났다고 말하려는 건가?  
  “생성된 future가 자기 참조가 된다”는 것도 future를 쓰면 암묵적으로 일어나는 일인가?  
  `Future::poll`은 직접 써본 적이 없는 것 같음  
  “안전한 코드는 일반 `&mut T`를 복구할 수 없다”면서 “일반적인 변경은 허용한다”고 하는데, 그러면 어떻게 한다는 건가?  
  이런 것들 때문에 Rust를 더 파고드는 걸 그만두게 됐음
  - 원시 포인터는 Rust의 **원시 타입** 중 하나임. 문서는 [여기](https://doc.rust-lang.org/std/primitive.pointer.html)와 [여기](https://doc.rust-lang.org/std/ptr/index.html)에 있음  
    다만 저수준으로 내려가지 않으면 쓸 일이 거의 없는 것도 맞음. 나도 C 라이브러리를 호출하려고 할 때야 알게 됨  
    `Future::poll`은 Rust 비동기 코드의 기본임. 직접 호출하는 것이 아니라 실행기(executor)가 호출함. Rust에는 기본 실행기가 없어서 Tokio, smol, pollster 같은 것을 추가해야 하고, 이들이 `Future` 트레이트에 정의된 `poll` 같은 메서드를 사용해 일을 처리함
  - 원글 작성자는 아니고 이것들이 유일한 이유도 아니지만, Rust에서 포인터를 다뤄야 했던 이유는 **FFI**와 그래프 같은 **자기 참조 자료구조**였음  
    문서는 [여기](https://doc.rust-lang.org/core/primitive.pointer.html)를 포함해 여러 곳에 있음  
    다른 사람들이 오직 본인이 필요로 했던 것만 설명해야 한다고 기대하는 건 좀 과함  
    “그래서 어떻게?”에서 무엇을 묻는 건지 잘 모르겠음
