Drop과 자원 정리
값이 스코프를 벗어날 때 Drop으로 자원을 자동 정리하고 std::mem::drop을 사용해 필요한 시점에 일찍 해제합니다.
스마트 포인터 패턴에서 중요한 두 번째 트레이트는 Drop입니다. 이 트레이트로 값이 스코프를 벗어나려 할 때 무슨 일을 할지 정할 수 있습니다. Drop 트레이트는 어떤 타입에든 구현할 수 있으며, 그 코드로 파일이나 네트워크 연결 같은 자원을 해제할 수 있습니다.
Drop을 스마트 포인터의 맥락에서 소개하는 까닭은, 스마트 포인터를 구현할 때 Drop 트레이트의 기능이 거의 언제나 쓰이기 때문입니다. 예를 들어 Box<T>가 버려지면 박스가 가리키는 힙의 공간을 해제합니다.
어떤 언어에서는 몇몇 타입의 경우, 프로그래머가 그 타입의 인스턴스를 다 쓸 때마다 메모리나 자원을 해제하는 코드를 직접 불러야 합니다. 파일 핸들, 소켓, 락 같은 것이 그 예입니다. 프로그래머가 잊으면 시스템에 부담이 쌓이고 멈출 수도 있습니다. 러스트에서는 값이 스코프를 벗어날 때마다 실행할 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 넣어 줍니다. 그래서 특정 타입의 인스턴스를 다 쓴 곳마다 정리 코드를 넣는 일에 신경 쓰지 않아도 되며, 그래도 자원이 새지 않습니다.
Drop 트레이트로 정리 코드 실행하기
값이 스코프를 벗어날 때 실행할 코드는 Drop 트레이트를 구현해 지정합니다. Drop 트레이트는 self의 가변 참조를 받는 drop이라는 메서드 하나를 구현하라고 요구합니다. 러스트가 언제 drop을 부르는지 보기 위해, 지금은 println! 문으로 drop을 구현해 봅니다.
다음 코드는 인스턴스가 스코프를 벗어날 때 Dropping CustomSmartPointer!를 출력하는 것이 유일한 기능인 CustomSmartPointer 구조체로, 러스트가 언제 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!("CustomSmartPointers created");
}Drop 트레이트는 프렐류드(prelude)에 들어 있으므로 스코프로 가져오지 않아도 됩니다. CustomSmartPointer에 Drop 트레이트를 구현하고, println!을 부르는 drop 메서드의 구현을 제공했습니다. drop 함수의 본문에는 타입의 인스턴스가 스코프를 벗어날 때 실행하고 싶은 로직을 둡니다. 여기서는 러스트가 언제 drop을 부르는지 보여 주려고 텍스트를 출력합니다.
main에서는 CustomSmartPointer의 인스턴스 둘을 만들고 CustomSmartPointers created를 출력합니다. main이 끝나면 CustomSmartPointer의 인스턴스들이 스코프를 벗어나고, 러스트는 drop 메서드에 넣은 코드를 불러 마지막 메시지를 출력합니다. drop 메서드를 직접 부르지 않았다는 점을 보세요. 이 프로그램을 실행하면 다음과 같이 출력됩니다.
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!인스턴스가 스코프를 벗어나자 러스트가 자동으로 drop을 불러 지정한 코드를 실행했습니다. 변수는 만든 순서의 반대로 버려지므로, d가 c보다 먼저 버려졌습니다. 이 예의 목적은 drop 메서드가 어떻게 동작하는지 눈으로 보여 주는 것입니다. 실제로는 출력 대신 그 타입이 실행해야 하는 정리 코드를 지정합니다.
정상적으로 정리될 때는 Drop::drop이 먼저 실행되고, 그 뒤 구조체의 필드들이 선언된 순서대로 정리됩니다. 같은 스코프의 지역 변수가 선언의 반대 순서로 버려지는 것과는 다른 규칙입니다. 이 예에서는 drop이 끝난 뒤 data 필드의 String이 정리됩니다.
std::mem::drop으로 값을 일찍 버리기
아쉽게도 자동 drop 기능을 끄는 것은 간단하지 않습니다. 대개는 drop을 끌 필요도 없습니다. Drop 트레이트의 요점은 자동으로 처리된다는 것이기 때문입니다. 하지만 가끔 값을 일찍 정리하고 싶을 때가 있습니다. 락을 관리하는 스마트 포인터를 쓸 때가 한 예입니다. 락을 푸는 drop 메서드를 강제로 불러, 같은 스코프의 다른 코드가 락을 얻을 수 있게 하고 싶을 수 있습니다. 러스트는 Drop 트레이트의 drop 메서드를 직접 부르지 못하게 합니다. 대신 스코프가 끝나기 전에 값을 강제로 버리고 싶다면 표준 라이브러리가 제공하는 std::mem::drop 함수를 불러야 합니다.
앞의 코드의 main 함수를 다음처럼 바꿔 Drop 트레이트의 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("some data"),
};
println!("CustomSmartPointer created");
c.drop();
println!("CustomSmartPointer dropped before the end of main");
}error[E0040]: explicit use of destructor method
--> src/main.rs:16:7
|
16 | c.drop();
| ^^^^ explicit destructor calls not allowed
|
help: consider using `drop` function
|
16 - c.drop();
16 + drop(c);
|
For more information about this error, try `rustc --explain E0040`.이 오류 메시지는 drop을 직접 부르는 것이 허용되지 않는다고 말합니다. 오류 메시지는 인스턴스를 정리하는 함수를 가리키는 일반적인 프로그래밍 용어인 소멸자(destructor)라는 말을 씁니다. 소멸자는 인스턴스를 만드는 생성자(constructor)와 대비됩니다. 러스트의 drop 함수가 특정한 소멸자의 하나입니다.
러스트가 drop을 직접 부르지 못하게 하는 까닭은, main이 끝날 때 러스트가 여전히 값에 자동으로 drop을 부를 것이기 때문입니다. 그러면 같은 값을 두 번 정리하려 하므로 이중 해제(double free) 오류가 생깁니다.
값이 스코프를 벗어날 때 자동으로 drop을 넣는 것을 끌 수 없고, drop 메서드를 직접 부를 수도 없습니다. 그러므로 값을 일찍 정리해야 한다면 std::mem::drop 함수를 씁니다.
std::mem::drop 함수는 Drop 트레이트의 drop 메서드와 다릅니다. 강제로 버리고 싶은 값을 인수로 넘겨 부릅니다. 이 함수는 프렐류드에 있으므로, 앞의 코드의 main을 다음처럼 drop 함수를 부르도록 고칠 수 있습니다.
fn main() {
let c = CustomSmartPointer {
data: String::from("some data"),
};
println!("CustomSmartPointer created");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of mainCustomSmartPointer created와 CustomSmartPointer dropped before the end of main 사이에 Dropping CustomSmartPointer with data `some data`!가 출력되었습니다. 그 시점에 drop 메서드의 코드가 불려 c를 버렸다는 뜻입니다. 아래 그림은 값을 만들어 일찍 버리기까지의 순서를 보여 줍니다.
Drop 스코프와 조기 해제 순서
생성→소유 이동→std mem drop 소비→Drop 구현→선언 순서 필드 정리의 실제 자원 생애이다. 이동 시점 정리와 c.drop 직접 호출을 구분한다.
SEQUENCE · OWNERSHIP RELEASE
drop(c)는 값을 소비하여 스코프 끝보다 일찍 정리한다
std::mem::drop(value)는 값을 소비하는 일반 함수다. 정상 정리에서는 Drop::drop이 먼저 실행되고 이어 필드가 선언 순서대로 정리되며, 같은 스코프의 지역 변수는 보통 선언의 역순으로 끝난다.
- 생성과 이동
값을 만들고 다른 바인딩으로 이동하면 새 소유자가 나중 정리를 맡습니다.
- 조기 종료
std::mem::drop(value)는 값을 소비해 소유권을 즉시 끝냅니다.
- Destructor
정상 경로에서 Drop::drop이 먼저 호출됩니다.
- 필드 정리
그 뒤 구조체 필드가 선언 순서대로 정리됩니다.
- 증거 경계
c.drop()은 E0040, drop(c) 이후 c 사용은 소유권 이동으로 거부됩니다.
본문 예제 14-14 · 정상 스코프 종료
지역 변수 c를 먼저, d를 나중에 생성합니다. main 끝에서는 d의 other stuff를 먼저 출력하고 c의 my stuff를 뒤에 출력합니다. 이는 필드 선언 순서와 구분되는 지역 변수의 정리 순서입니다.
본문 예제 14-16 · drop(c)
println!("created");
drop(c);
println!("dropped before main end");본문의 긴 출력 문자열을 줄인 핵심 예입니다. 두 출력 사이에 c의 Drop 메시지가 나타납니다. c.drop() 메서드를 직접 호출하는 시도와 다릅니다.
이동 시점에 원래 값이 drop되는 것이 아니라 새 소유자가 나중에 정리된다. Drop::drop 직접 호출은 이중 정리를 막기 위해 거부되며 Rust 2024의 임시 값 스코프와 비정상 종료는 별도 경계다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원래 가로 도표의 4참여자·6메시지를 세로 도표에서는 6개의 독립적인 송신/수신 쌍으로 반복 표현한다. 번호는 시간 순서, 화살표는 각 실제 메시지 방향이며 그룹 사이에 새 간선은 없다. 마지막 쌍은 필드 자원→스코프로 돌아오는 완료 통지로 원래 반환 방향을 보존한다. Drop 구현이 먼저 실행되고 이어 필드를 정리한다. mem::drop은 일반적인 값 소비 함수이며 참조만 버리면 참조 대상이 소멸하는 것이 아니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 소유 값 생성·이동 소유 값을 만든 뒤 새 이름에 이동한다. |
|
|
| 조기 소비 std::mem::drop이 소유 값을 소비한다. |
|
|
| 정리 순서 Drop 구현 뒤 필드가 선언 순서로 정리된다. |
|
|
Drop 스코프와 조기 해제 순서
생성→소유 이동→std mem drop 소비→Drop 구현→선언 순서 필드 정리의 실제 자원 생애이다. 이동 시점 정리와 c.drop 직접 호출을 구분한다.
SEQUENCE · OWNERSHIP RELEASE
drop(c)는 값을 소비하여 스코프 끝보다 일찍 정리한다
std::mem::drop(value)는 값을 소비하는 일반 함수다. 정상 정리에서는 Drop::drop이 먼저 실행되고 이어 필드가 선언 순서대로 정리되며, 같은 스코프의 지역 변수는 보통 선언의 역순으로 끝난다.
- 생성과 이동
값을 만들고 다른 바인딩으로 이동하면 새 소유자가 나중 정리를 맡습니다.
- 조기 종료
std::mem::drop(value)는 값을 소비해 소유권을 즉시 끝냅니다.
- Destructor
정상 경로에서 Drop::drop이 먼저 호출됩니다.
- 필드 정리
그 뒤 구조체 필드가 선언 순서대로 정리됩니다.
- 증거 경계
c.drop()은 E0040, drop(c) 이후 c 사용은 소유권 이동으로 거부됩니다.
본문 예제 14-14 · 정상 스코프 종료
지역 변수 c를 먼저, d를 나중에 생성합니다. main 끝에서는 d의 other stuff를 먼저 출력하고 c의 my stuff를 뒤에 출력합니다. 이는 필드 선언 순서와 구분되는 지역 변수의 정리 순서입니다.
본문 예제 14-16 · drop(c)
println!("created");
drop(c);
println!("dropped before main end");본문의 긴 출력 문자열을 줄인 핵심 예입니다. 두 출력 사이에 c의 Drop 메시지가 나타납니다. c.drop() 메서드를 직접 호출하는 시도와 다릅니다.
이동 시점에 원래 값이 drop되는 것이 아니라 새 소유자가 나중에 정리된다. Drop::drop 직접 호출은 이중 정리를 막기 위해 거부되며 Rust 2024의 임시 값 스코프와 비정상 종료는 별도 경계다.
이 예의 조건
- 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
- 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
| 범위 | 원문 |
|---|---|
| 관계·조건 설명 | 원래 가로 도표의 4참여자·6메시지를 세로 도표에서는 6개의 독립적인 송신/수신 쌍으로 반복 표현한다. 번호는 시간 순서, 화살표는 각 실제 메시지 방향이며 그룹 사이에 새 간선은 없다. 마지막 쌍은 필드 자원→스코프로 돌아오는 완료 통지로 원래 반환 방향을 보존한다. Drop 구현이 먼저 실행되고 이어 필드를 정리한다. mem::drop은 일반적인 값 소비 함수이며 참조만 버리면 참조 대상이 소멸하는 것이 아니다. |
| 관찰·상황 | 값·보충 정보 | 변화·이유 |
|---|---|---|
| 소유 값 생성·이동 소유 값을 만든 뒤 새 이름에 이동한다. |
|
|
| 조기 소비 std::mem::drop이 소유 값을 소비한다. |
|
|
| 정리 순서 Drop 구현 뒤 필드가 선언 순서로 정리된다. |
|
|
값을 만들어 다른 이름으로 옮기면, 옮긴 시점에 원래 값이 버려지는 것이 아니라 새 소유자가 나중에 정리를 맡습니다. std::mem::drop(value)는 값을 받아 소비하는 평범한 함수로, 소유권을 그 자리에서 끝냅니다. 그러면 정상적인 정리 과정에 따라 Drop::drop이 먼저 불리고, 그 뒤 필드가 선언 순서대로 정리됩니다. 그림에서 예제 14-14라고 적은 칸은 이 글의 첫 c와 d 예제로, main이 끝날 때 d의 other stuff가 먼저, c의 my stuff가 나중에 출력됩니다. 예제 14-16이라고 적은 칸은 방금 본 drop(c) 예제의 출력 문자열을 줄인 것입니다. c.drop()은 E0040으로 거부되고, drop(c) 뒤에 c를 쓰는 것은 이미 옮겨 간 값을 쓰는 것이라 거부됩니다. 참조만 넘겨 drop해서는 참조가 가리키는 값이 버려지지 않습니다.
Drop 트레이트 구현에 지정한 코드로 정리를 편하고 안전하게 할 수 있습니다. 예를 들어 직접 메모리 할당자를 만드는 데 쓸 수도 있습니다. Drop 트레이트와 러스트의 소유권 시스템 덕분에 정리를 기억할 필요가 없습니다. 러스트가 자동으로 하기 때문입니다.
아직 쓰고 있는 값을 실수로 정리해 생기는 문제도 걱정하지 않아도 됩니다. 참조가 언제나 유효하도록 보장하는 소유권 시스템이, 값이 더는 쓰이지 않을 때 drop이 한 번만 불리도록 보장하기도 합니다. 다만 std::mem::forget이나 ManuallyDrop을 쓰거나 프로세스가 비정상적으로 끝나면 이 정상적인 정리 과정이 실행되지 않을 수 있습니다.
더 깊이: _로 받은 값과 임시 값은 언제 버려질까
값을 일찍 버리는 방법으로 let _ = 값;을 떠올리기 쉽지만, 이 문장은 오른쪽에 무엇이 오느냐에 따라 다르게 동작합니다. Reference의 패턴 설명에 따르면 와일드카드 패턴 _는 이름 있는 패턴과 달리 맞춘 값을 복사하지도, 옮기지도, 빌리지도 않습니다. 그래서 let _ = c;는 c를 옮기지 않고, c는 그대로 남아 원래대로 스코프가 끝날 때 버려집니다.
반면 = 오른쪽이 CustomSmartPointer { ... }나 함수 호출처럼 새 값을 만드는 식이라면, 그 값은 어떤 변수에도 묶이지 않은 임시 값(temporary)이 됩니다. Reference의 소멸자 규칙에 따르면 let 문의 초기화 식에서 생긴 임시 값은, 수명 연장이 적용되지 않는 한 그 문장이 끝날 때 버려집니다. 그래서 let _ = CustomSmartPointer { ... };는 그 문장에서 곧바로 drop이 불리고, let _guard = ...;처럼 _로 시작하는 이름에 묶으면 스코프가 끝날 때까지 살아 있습니다. 공유 상태 동시성에서 볼 락 가드를 let _ = m.lock().unwrap();로 받으면 락이 그 자리에서 곧바로 풀리는 것도 이 차이 때문입니다.
정리
Drop트레이트의drop(&mut self)에 값이 스코프를 벗어날 때 실행할 정리 코드를 두면, 컴파일러가 자동으로 불러 줍니다.- 같은 스코프의 변수는 만든 순서의 반대로 버려지고, 한 값 안에서는
Drop::drop이 먼저 실행된 뒤 필드가 선언 순서대로 정리됩니다. drop메서드를 직접 부르면 이중 해제를 막기 위해 E0040 오류가 납니다.- 값을 일찍 버리려면 프렐류드에 있는
std::mem::drop(value)로 값을 넘겨 소유권을 끝냅니다. 그 뒤에는 그 값을 쓸 수 없습니다. - 소유권 시스템 덕분에
drop은 값이 더는 쓰이지 않을 때 한 번만 불립니다.
연습 문제
1. 출력 순서
다음 코드는 무엇을 어떤 순서로 출력하나요? 앞의 CustomSmartPointer를 쓴다고 합시다.
fn main() {
let a = CustomSmartPointer { data: String::from("a") };
{
let b = CustomSmartPointer { data: String::from("b") };
println!("inner");
}
let c = CustomSmartPointer { data: String::from("c") };
drop(a);
println!("end");
}해설 보기
먼저 inner가 출력되고, 안쪽 블록이 끝나며 b가 버려져 Dropping CustomSmartPointer with data `b`!가 나옵니다. 그다음 drop(a)가 a를 일찍 버려 `a`의 메시지가, 이어서 end가 출력되고, main이 끝날 때 남은 c가 버려져 `c`의 메시지가 마지막에 나옵니다. a는 이미 버려졌으므로 main의 끝에서 다시 버려지지 않습니다.
2. 참조를 넘기면
let r = &c; drop(r);처럼 참조를 drop에 넘기면 c가 정리되나요?
해설 보기
정리되지 않습니다. drop은 넘겨받은 값을 소비할 뿐이므로, 여기서 버려지는 것은 참조 r이고 c가 가리키는 값은 그대로 남습니다. 참조를 버리는 것은 아무 정리도 하지 않으며, c는 스코프가 끝날 때 버려집니다. 값을 일찍 정리하려면 drop(c)처럼 소유한 값 자체를 넘겨야 합니다.
다음 글 Rc<T>, 참조 카운트 스마트 포인터에서는 하나의 값을 여러 소유자가 함께 가질 수 있게 해 주는 Rc<T>를 살펴봅니다.