본문 바로가기

Deref와 역참조 강제 변환

Deref를 직접 구현해 스마트 포인터를 참조처럼 역참조하고 함수 호출에서 적용되는 암묵적 역참조 변환을 이해합니다.

Deref 트레이트를 구현하면 역참조 연산자(dereference operator) *의 동작을 바꿀 수 있습니다. 곱셈 연산자나 glob 연산자와 헷갈리지 마세요. 스마트 포인터가 보통의 참조처럼 다뤄지도록 Deref를 구현하면, 참조에 동작하도록 쓴 코드를 스마트 포인터에도 쓸 수 있습니다.

먼저 역참조 연산자가 보통의 참조에 어떻게 동작하는지 보고, Box<T>처럼 동작하는 직접 만든 타입을 정의해 봅니다. 그 타입에서 역참조 연산자가 참조처럼 동작하지 않는 까닭을 보고, Deref 트레이트를 구현하면 스마트 포인터가 참조와 비슷하게 동작하게 되는 원리를 살펴봅니다. 그다음 러스트의 역참조 강제 변환(deref coercion) 기능과, 이 기능이 참조나 스마트 포인터와 함께 동작하게 해 주는 방식을 봅니다.

포인터를 따라가서 값 얻기

보통의 참조는 포인터의 한 종류이며, 포인터는 다른 곳에 저장된 값을 가리키는 화살표라고 생각할 수 있습니다. 다음 코드는 i32 값의 참조를 만들고, 역참조 연산자로 참조를 따라가 값을 얻습니다.

src/main.rs
fn main() {
    let x = 5;
    let y = &x;

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

변수 x는 i32 값 5를 가집니다. y는 x의 참조로 정했습니다. x가 5와 같다고 단언할 수 있습니다. 하지만 y의 값에 대해 단언하려면, *y로 참조를 따라가 그것이 가리키는 값까지 가야 합니다. 그래서 역참조가 필요합니다. y를 역참조하면 y가 가리키는 정수 값에 접근해 5와 비교할 수 있습니다. 대신 assert_eq!(5, y);라고 쓰면 다음 컴파일 오류가 납니다.

error[E0277]: can't compare `{integer}` with `&{integer}`
 --> src/main.rs:6:5
  |
6 |     assert_eq!(5, y);
  |     ^^^^^^^^^^^^^^^^ no implementation for `{integer} == &{integer}`
  |
  = help: the trait `PartialEq<&{integer}>` is not implemented for `{integer}`
  = help: the following other types implement trait `PartialEq<Rhs>`:
            f128
            f16
            f32
            f64
            i128
            i16
            i32
            i64
          and 8 others

For more information about this error, try `rustc --explain E0277`.

숫자와 숫자의 참조는 서로 다른 타입이므로 비교할 수 없습니다. 참조가 가리키는 값을 따라가려면 역참조 연산자를 써야 합니다.

Box<T>를 참조자처럼 사용하기

앞의 코드에서 참조 대신 Box<T>를 쓰도록 다시 쓸 수 있습니다. 다음 코드처럼 Box<T>에 쓰는 역참조 연산자는 참조에 쓰는 역참조 연산자와 같은 방식으로 동작합니다.

src/main.rs
fn main() {
    let x = 5;
    let y = Box::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}

앞의 코드와 다른 점은, y를 x의 값을 가리키는 참조가 아니라 x의 값을 복사한 것을 가리키는 박스의 인스턴스로 정했다는 것뿐입니다. 마지막 단언에서는 y가 참조였을 때와 같은 방식으로 역참조 연산자를 써서 박스의 포인터를 따라갈 수 있습니다. 이제 Box<T>가 무엇이 특별해서 역참조 연산자를 쓸 수 있는지, 직접 박스 타입을 정의하며 알아봅니다.

자체 스마트 포인터 정의하기

표준 라이브러리가 제공하는 Box<T> 타입과 비슷한 래퍼(wrapper) 타입을 만들어, 스마트 포인터 타입이 기본으로는 참조와 다르게 동작한다는 것을 경험해 봅니다. 그다음 역참조 연산자를 쓸 수 있게 하는 방법을 봅니다.

이제부터 만들 MyBox<T> 타입과 실제 Box<T> 사이에는 큰 차이가 하나 있습니다. 이 교재의 버전은 데이터를 힙에 저장하지 않습니다. 이 예제는 Deref에 집중하므로, 데이터가 실제로 어디에 저장되는지는 포인터 같은 동작보다 덜 중요합니다.

Box<T> 타입은 결국 원소 하나를 가진 튜플 구조체로 정의되므로, 다음 코드는 같은 방식으로 MyBox<T> 타입을 정의합니다. Box<T>에 정의된 new 함수에 맞춰 new 함수도 정의합니다.

src/main.rs
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

MyBox라는 구조체를 정의하고, 어떤 타입의 값이든 담을 수 있도록 제네릭 매개변수 T를 선언했습니다. MyBox 타입은 T 타입의 원소 하나를 가진 튜플 구조체입니다. MyBox::new 함수는 T 타입의 매개변수 하나를 받아, 넘겨받은 값을 담은 MyBox 인스턴스를 돌려줍니다.

앞의 코드의 main 함수에서 Box<T> 대신 MyBox<T> 타입을 써 봅니다. 다음 코드는 러스트가 MyBox를 어떻게 역참조하는지 모르므로 컴파일되지 않습니다.

src/main.rs (컴파일되지 않음)
struct MyBox<T>(T);

impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

fn main() {
    let x = 5;
    let y = MyBox::new(x);

    assert_eq!(5, x);
    assert_eq!(5, *y);
}
error[E0614]: type `MyBox<{integer}>` cannot be dereferenced
  --> src/main.rs:14:19
   |
14 |     assert_eq!(5, *y);
   |                   ^^ can't be dereferenced

For more information about this error, try `rustc --explain E0614`.

MyBox<T> 타입은 역참조할 수 없습니다. 그 기능을 이 타입에 구현하지 않았기 때문입니다. * 연산자로 역참조할 수 있게 하려면 Deref 트레이트를 구현합니다.

Deref 트레이트를 구현하여 임의의 타입을 참조자처럼 다루기

트레이트와 공통 동작에서 이야기했듯, 트레이트를 구현하려면 트레이트에 필요한 메서드의 구현을 제공해야 합니다. 표준 라이브러리가 제공하는 Deref 트레이트는 self를 빌려 안쪽 데이터의 참조를 돌려주는 deref 메서드 하나를 구현하라고 요구합니다. 다음은 MyBox<T>의 정의에 더할 Deref의 구현입니다.

src/main.rs
use std::ops::Deref;

impl<T> Deref for MyBox<T> {
    type Target = T;

    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

type Target = T; 문법은 Deref 트레이트가 쓸 연관 타입을 정의합니다. 연관 타입은 제네릭 매개변수를 선언하는 방법과 조금 다른데, 지금은 신경 쓰지 않아도 되며 고급 트레이트에서 자세히 다룹니다.

deref 메서드의 본문은 &self.0으로 채워, deref가 * 연산자로 접근하고 싶은 값의 참조를 돌려주게 했습니다. 구조체 정의 및 인스턴스화에서 보았듯 .0은 튜플 구조체의 첫 번째 값에 접근합니다. 앞에서 MyBox<T> 값에 *를 부르던 main 함수는 이제 컴파일되고, 단언도 통과합니다.

Deref 트레이트가 없으면 컴파일러는 & 참조만 역참조할 수 있습니다. deref 메서드는 컴파일러에게 Deref를 구현한 어떤 타입의 값이든 받아, deref 메서드를 불러 자신이 역참조하는 방법을 아는 참조를 얻는 능력을 줍니다.

앞의 코드에서 *y라고 썼을 때, 러스트는 내부적으로 실제로 다음 코드를 실행했습니다.

*(y.deref())

러스트는 * 연산자를 deref 메서드 호출과 보통의 역참조로 바꾸므로, deref 메서드를 불러야 하는지 생각하지 않아도 됩니다. 이 러스트의 기능 덕분에 보통의 참조가 있든 Deref를 구현한 타입이 있든 똑같이 동작하는 코드를 쓸 수 있습니다.

deref 메서드가 값의 참조를 돌려주고, *(y.deref())의 괄호 바깥에서 보통의 역참조가 여전히 필요한 까닭은 소유권 시스템 때문입니다. deref 메서드가 값의 참조 대신 값을 직접 돌려준다면, 값이 self 밖으로 옮겨질 것입니다. 이 경우와 역참조 연산자를 쓰는 대부분의 경우에, MyBox<T> 안의 값의 소유권을 가져가고 싶지는 않습니다.

* 연산자는 deref 메서드 호출과 * 연산자 호출로 단 한 번만 바뀐다는 점도 알아 두세요. 코드에서 *를 쓸 때마다 그렇습니다. * 연산자의 치환이 끝없이 되풀이되지 않으므로, 결국 i32 타입의 데이터에 도달하며, 이것은 앞의 assert_eq!의 5와 맞습니다.

함수와 메서드를 이용한 암묵적 역참조 강제 변환

역참조 강제 변환(deref coercion)은 Deref 트레이트를 구현한 타입의 참조를 다른 타입의 참조로 바꿉니다. 예를 들어 String이 &str을 돌려주도록 Deref 트레이트를 구현하므로, 역참조 강제 변환은 &String을 &str로 바꿀 수 있습니다. 역참조 강제 변환은 러스트가 함수와 메서드의 인수에 해 주는 편의 기능으로, Deref 트레이트를 구현한 타입에만 동작합니다. 함수나 메서드의 정의에 있는 매개변수의 타입과 맞지 않는 특정 타입의 값의 참조를 인수로 넘길 때 자동으로 일어납니다. 일련의 deref 메서드 호출이, 넘긴 타입을 매개변수에 필요한 타입으로 바꿉니다.

역참조 강제 변환이 러스트에 더해진 것은, 함수와 메서드를 호출할 때 프로그래머가 &와 *로 명시적인 참조와 역참조를 그렇게 많이 쓰지 않아도 되게 하기 위해서입니다. 또 이 기능 덕분에 참조에도 스마트 포인터에도 동작하는 코드를 더 많이 쓸 수 있습니다.

역참조 강제 변환이 동작하는 것을 보려면, 앞에서 정의한 MyBox<T> 타입과 Deref의 구현을 씁니다. 다음은 문자열 슬라이스 타입의 매개변수를 가진 함수의 정의입니다.

src/main.rs
fn hello(name: &str) {
    println!("Hello, {name}!");
}

hello 함수는 hello("Rust");처럼 문자열 슬라이스를 인수로 불러 부를 수 있습니다. 역참조 강제 변환 덕분에, 다음 코드처럼 MyBox<String> 타입 값의 참조로 hello를 부를 수도 있습니다.

src/main.rs
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&m);
}

여기서는 MyBox<String> 값의 참조인 &m을 인수로 hello 함수를 부릅니다. MyBox<T>에 Deref 트레이트를 구현했으므로, 러스트는 deref를 불러 &MyBox<String>을 &String으로 바꿀 수 있습니다. 표준 라이브러리는 String에 문자열 슬라이스를 돌려주는 Deref의 구현을 제공하며, 이것은 Deref의 API 문서에 나와 있습니다. 러스트는 deref를 다시 불러 &String을 &str로 바꾸며, 이것은 hello 함수의 정의와 맞습니다. 아래 그림은 이 두 번의 변환을 보여 줍니다.

RUST · TWO DEREF TARGETS

hello(&m)는 두 Deref를 거쳐 &str을 빌린다

요청된 참조 타입이 변환의 목표입니다. 본문의 MyBox<String>에서 String, str로 이어지는 실제 Target 관계를 따라갑니다.

MyBox String에서 str로 이어지는 두 Derefhello는 str 참조를 요구한다. MyBox Target String, String Target str을 따라 두 번 빌림 타입이 바뀐다. 소유권은 이동하지 않는다.&MyBox<String>MyBox: Target = String&StringString: Target = str&strhello의 매개변수Deref #1Deref #2
공유 빌림의 두 Deref 변환MyBox의 Target은 String이고 String의 Target은 str이므로 &MyBox<String>에서 &String을 거쳐 hello가 요구하는 &str을 얻으며, 문자열의 소유권이나 내용은 복제하지 않는다.
&MyBox<String>MyBox: Target = String
Deref #1Deref #1&MyBox<String> · MyBox: Target = String → &String · String: Target = str
&StringString: Target = str
Deref #2Deref #2&String · String: Target = str → &str · hello의 매개변수
&strhello의 매개변수

&MyBox<String> → &String

본문 MyBox의 Deref::Target은 String이며 deref는 &self.0을 반환합니다.

&String → &str

String의 Target은 str입니다. hello의 요청 타입에 도달하면 추가 변환 없이 빌려 호출합니다.

본문의 hello(&m) · 결과는 Hello, Rust!

let m = MyBox::new(
    String::from("Rust")
);
hello(&m);

MyBox와 hello는 앞의 본문 정의를 사용합니다. MyBox<T>(T)는 이 수업의 튜플 래퍼이며 데이터를 힙에 저장하지 않습니다. m의 String 소유권을 hello에 넘기지 않습니다.

변환 단계는 컴파일 때 결정됩니다. 사용자 정의 deref의 실행 비용까지 없어지는 것은 아니며, 암시적 호출에 적합하도록 저렴하고 예상 가능한 동작으로 설계합니다. Target은 ?Sized일 수 있습니다.

hello가 요구하는 &str이 변환의 목표입니다. MyBox의 Deref::Target은 String이고 deref는 &self.0을 돌려주므로 첫 변환으로 &String을 얻고, String의 Target은 str이므로 두 번째 변환으로 &str에 이릅니다. 요청한 타입에 도달하면 더 바꾸지 않고 빌려서 호출하며, 결과로 Hello, Rust!가 출력됩니다. m의 String 소유권을 hello에 넘기거나 문자열의 내용을 복제하지는 않습니다.

러스트에 역참조 강제 변환이 없었다면, &MyBox<String> 타입의 값으로 hello를 부르려면 다음과 같은 코드를 써야 했을 것입니다.

src/main.rs
fn main() {
    let m = MyBox::new(String::from("Rust"));
    hello(&(*m)[..]);
}

(*m)은 MyBox<String>을 역참조해 String으로 만듭니다. 그다음 &와 [..]는 String 전체에 해당하는 문자열 슬라이스를 얻어 hello의 시그니처와 맞춥니다. 역참조 강제 변환이 없는 이 코드는 이 기호들 때문에 읽기도, 쓰기도, 이해하기도 더 어렵습니다. 역참조 강제 변환은 러스트가 이런 변환을 자동으로 처리하게 해 줍니다.

관련된 타입에 Deref 트레이트가 정의되어 있으면, 러스트는 타입을 분석해 매개변수의 타입에 맞는 참조를 얻을 때까지 필요한 만큼 Deref::deref를 씁니다. Deref::deref를 몇 번 넣어야 하는지는 컴파일 시점에 정해지므로, 역참조 강제 변환을 쓴다고 런타임 비용이 생기지 않습니다. 다만 직접 구현한 deref 자체가 하는 일의 비용까지 없어지는 것은 아니므로, 암묵적으로 불려도 괜찮도록 deref는 가볍고 예측 가능하게 동작하도록 만듭니다.

역참조 강제 변환이 가변성과 상호작용하는 법

불변 참조의 * 연산자를 바꾸는 데 Deref 트레이트를 쓰는 것과 비슷하게, 가변 참조의 * 연산자를 바꾸는 데는 DerefMut 트레이트를 쓸 수 있습니다. 러스트는 다음 세 경우에 타입과 트레이트 구현을 찾으면 역참조 강제 변환을 합니다.

변환조건
&T에서 &U로T: Deref<Target = U>일 때
&mut T에서 &mut U로T: DerefMut<Target = U>일 때
&mut T에서 &U로T: Deref<Target = U>일 때

첫 두 경우는 두 번째가 가변성을 구현한다는 점만 빼면 서로 같습니다. 첫 번째 경우는 &T가 있고 T가 어떤 타입 U로 Deref를 구현하면 &U를 투명하게 얻을 수 있다는 뜻입니다. 두 번째 경우는 가변 참조에 대해 같은 역참조 강제 변환이 일어난다는 뜻입니다. 아래 그림은 세 경우와, 허용되지 않는 네 번째 방향을 나란히 보여 줍니다.

RUST · BORROW DIRECTION CONTRACTS

Deref는 공유 빌림, DerefMut는 가변 Target 빌림을 제공한다

입력과 출력의 가변성을 함께 비교합니다. 아래 네 관계는 역참조 강제 변환의 방향이며, 명시적 trait 구현이 없는 임의 타입 변환을 뜻하지 않습니다.

역참조 강제 변환의 네 방향네방향은서로독립된비교이며위아래순차단계가아니다. 공유→공유와가변→공유는Deref,가변→가변은DerefMut구현을요구한다. 공유→가변은이coercion으로허용되지않으며화살촉없는점선과X로표시한다. 8표현노드·3허용간선·1금지제약은desktop/mobile동일하고별도내부가변성API와혼동하지않는다.&T&UDeref&mut T&mut UDerefMut&mut T&UDeref&T&mut U불가
독립된 네 빌림 방향네방향은서로독립된비교이며위아래순차단계가아니다. 공유→공유와가변→공유는Deref,가변→가변은DerefMut구현을요구한다. 공유→가변은이coercion으로허용되지않으며화살촉없는점선과X로표시한다. 8표현노드·3허용간선·1금지제약은desktop/mobile동일하고별도내부가변성API와혼동하지않는다.1 · 독립 방향&T&UDeref2 · 독립 방향&mut T&mut UDerefMut3 · 독립 방향&mut T&UDeref4 · 독립 방향&T&mut U불가

공유 → 공유 · Deref

&T → &U
T: Deref<Target = U>

입력의 공유 빌림을 유지하며 Target을 빌립니다. 본문의 &MyBox<String> → &String → &str이 이 방향입니다.

가변 → 가변 · DerefMut

&mut T → &mut U
T: DerefMut<Target = U>

가변 Target을 빌리는 구현이 필요합니다. 본문의 MyBox는 Deref만 구현하므로 이 가변 경로는 아직 제공하지 않습니다.

가변 → 공유 · Deref

&mut T → &U
T: Deref<Target = U>

가변 빌림을 읽기용 공유 빌림으로 낮출 수 있습니다. 빌림이 유효한 동안 기존 소유자의 접근은 계속 대여 규칙을 따릅니다.

공유 → 가변 · 허용되지 않음

&T ⇏ &mut U

Deref coercion은 공유 빌림에서 새로운 배타 빌림을 만들지 않습니다. 내부 가변성은 Cell/RefCell/Mutex 같은 별도의 API 계약입니다.

DerefMut는 Deref를 함께 요구합니다. 공유에서 가변으로 강제 변환하는 경로는 없습니다.

네 방향은 서로 독립된 비교이며 위에서 아래로 이어지는 단계가 아닙니다. 공유에서 공유로의 변환은 입력의 공유 대여를 유지하며 Target을 빌리고, 이 글의 &MyBox<String>에서 &String, &str로 가는 변환이 이 방향입니다. 가변에서 가변으로의 변환에는 가변 Target을 빌리는 DerefMut 구현이 필요하며, 이 글의 MyBox는 Deref만 구현하므로 이 경로는 아직 없습니다. DerefMut를 구현하려면 Deref도 함께 구현해야 합니다.

세 번째 경우는 더 미묘합니다. 러스트는 가변 참조를 불변 참조로도 강제 변환합니다. 하지만 그 반대는 가능하지 않습니다. 불변 참조는 결코 가변 참조로 강제 변환되지 않습니다. 대여 규칙 때문에, 가변 참조가 있다면 그 가변 참조는 그 데이터의 유일한 참조여야 합니다. 그렇지 않으면 프로그램이 컴파일되지 않습니다. 가변 참조 하나를 불변 참조 하나로 바꾸는 것은 대여 규칙을 결코 어기지 않습니다. 불변 참조를 가변 참조로 바꾸려면 처음의 불변 참조가 그 데이터의 유일한 불변 참조여야 하는데, 대여 규칙은 그것을 보장하지 않습니다. 그래서 러스트는 불변 참조를 가변 참조로 바꾸는 것이 가능하다고 가정할 수 없습니다. 공유된 값을 바꿔야 할 때 쓰는 Cell, RefCell, Mutex 같은 내부 가변성은 역참조 강제 변환과는 별개의 API이며, RefCell<T>와 내부 가변성 패턴에서 다룹니다.

더 깊이: 언제 Deref를 구현해도 될까

역참조 강제 변환은 편리하지만, 컴파일러가 보이지 않게 deref 호출을 넣는다는 뜻이기도 합니다. 그래서 Deref 문서는 이 트레이트를 구현해도 되는 조건을 따로 적어 둡니다. 그 타입의 값이 대상 타입의 값처럼 투명하게 동작하고, deref가 값싸며, 쓰는 사람이 역참조 강제 변환 때문에 예상하지 못한 동작을 겪지 않아야 합니다.

반대로 deref가 예상하지 못하게 실패할 수 있거나, 그 타입의 메서드가 대상 타입의 메서드와 이름이 겹치기 쉽거나, 역참조 강제 변환을 공개 API의 약속으로 삼고 싶지 않다면 구현하지 말라고 권합니다. 메서드 이름이 겹치면 메서드 문법에서 본 것처럼 받는 쪽의 후보를 차례로 찾다가 바깥 타입의 메서드가 먼저 찾아지므로, 쓰는 사람이 어느 메서드가 불리는지 헷갈리기 쉽습니다. 뉴타입에 Deref를 구현해 안쪽 타입의 메서드를 모두 드러내는 방법이 고급 트레이트에 나오는데, 그때도 이 기준에 비추어 고를 일입니다.

문서는 deref가 예상하지 못하게 실패해서는 안 된다고 따로 강조합니다. 컴파일러가 deref 호출을 저절로 넣는 일이 많아서, 보이지 않는 곳에서 실패하면 원인을 찾기가 아주 어렵기 때문입니다. 다만 이 성질은 컴파일러가 강제하지 않으므로, unsafe 코드는 다른 타입의 deref가 실패하지 않는다는 데 기대어 안전성을 지켜서는 안 됩니다.

정리

  • 역참조 연산자 *는 참조를 따라가 가리키는 값을 얻으며, 숫자와 숫자의 참조는 다른 타입이라 그대로 비교할 수 없습니다.
  • 직접 만든 MyBox<T>는 Deref를 구현하기 전에는 *로 역참조할 수 없어 E0614 오류가 납니다.
  • Deref는 연관 타입 Target과 fn deref(&self) -> &Self::Target을 구현하며, *y는 *(y.deref())로 한 번만 바뀝니다.
  • 역참조 강제 변환은 함수와 메서드의 인수에서 &MyBox<String>을 &String, &str로 바꾸듯 필요한 만큼 deref를 넣으며, 컴파일 시점에 정해지므로 런타임 비용이 없습니다.
  • &T에서 &U로, &mut T에서 &mut U로, &mut T에서 &U로는 변환되지만, 불변 참조가 가변 참조로 변환되지는 않습니다.

연습 문제

1. 몇 번 변환될까

fn takes(s: &str)가 있을 때 let b: Box<String> = Box::new(String::from("hi"));와 let r: &&String = &&String::from("hi");에 대해, takes(&b)와 takes(r)는 컴파일되나요? 각각 어떤 변환이 일어나나요?

해설 보기

둘 다 컴파일됩니다. takes(&b)는 &Box<String>에서 Box의 Deref로 &String을 얻고, String의 Deref로 &str을 얻어 두 번 변환됩니다. takes(r)는 &&String에서 참조의 Deref로 &String을 얻고, 다시 String의 Deref로 &str을 얻습니다. 몇 번 넣을지는 컴파일 시점에 정해집니다.

2. 가변 참조로 바꿀 수 있을까

fn push_x(s: &mut String)가 있고, let m = MyBox::new(String::new());에서 push_x(&mut m)을 부르려 합니다. 무엇이 필요한가요? let r = &m; push_x(r);처럼 불변 참조를 넘기면 어떻게 되나요?

해설 보기

먼저 m을 let mut m으로 선언해야 &mut m을 만들 수 있고, &mut MyBox<String>을 &mut String으로 바꾸려면 MyBox<T>에 DerefMut를 구현해야 합니다. 이 글의 MyBox는 Deref만 구현하므로 그대로는 컴파일되지 않습니다. 불변 참조 r을 넘기는 것은 DerefMut를 구현해도 안 됩니다. 불변 참조는 가변 참조로 강제 변환되지 않기 때문입니다.

다음 글 Drop과 자원 정리에서는 값이 스코프를 벗어날 때 실행되는 코드를 Drop 트레이트로 정하는 방법을 살펴봅니다.