`Drop` 트레이트로 정리할 코드 실행하기
Drop 트레이트로 정리할 코드 실행하기
스마트 포인터 패턴에서 중요한 두 번째 트레이트는 Drop이에요. 값이 스코프를 벗어나려 할 때 무엇을 할지 커스터마이즈할 수 있게 해 주죠. 어떤 타입에든 Drop 트레이트 구현을 제공할 수 있고, 그 코드로 파일이나 네트워크 연결 같은 리소스를 해제할 수 있어요. Drop 트레이트의 기능은 거의 항상 스마트 포인터를 구현할 때 사용되기 때문에 스마트 포인터 맥락에서 소개합니다. 예를 들어 Box<T>가 드롭되면, 박스가 가리키는 힙 공간을 할당 해제하죠.
출처: The Rust Book
자동 정리 (Automatic Cleanup)
어떤 언어에서는 일부 타입에 대해, 해당 타입의 인스턴스 사용이 끝날 때마다 프로그래머가 메모리나 리소스를 해제하는 코드를 직접 호출해야 해요. 파일 핸들, 소켓, 락 같은 것들이 그런 예시죠. 프로그래머가 잊어버리면 시스템이 과부하에 걸려 충돌할 수도 있습니다. Rust에서는 값이 스코프를 벗어날 때마다 실행될 특정 코드를 지정할 수 있고, 컴파일러가 그 코드를 자동으로 삽입해요. 그 결과 특정 타입의 인스턴스 사용이 끝난 프로그램의 모든 곳에 정리 코드를 직접 배치하느라 신경 쓸 필요가 없습니다. 그래도 리소스가 새지 않아요!
값이 스코프를 벗어날 때 실행될 코드는 Drop 트레이트로 지정해요. Drop 트레이트는 self에 대한 가변 참조를 받는 drop이라는 메서드 하나를 구현하도록 요구합니다. Rust가 drop을 언제 호출하는지 보려면, 지금은 println! 문으로 drop을 구현해 볼게요.
Listing 15-14는 CustomSmartPointer라는 구조체를 보여주는데, 유일한 커스텀 기능은 인스턴스가 스코프를 벗어날 때 Dropping CustomSmartPointer!를 출력해 Rust가 drop 메서드를 언제 실행하는지 보여주는 것이에요.
Filename: src/main.rs
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");
}
Listing 15-14: 정리 코드를 넣을 자리인 Drop 트레이트를 구현한 CustomSmartPointer 구조체
Drop 트레이트는 prelude에 포함되어 있어서 별도로 스코프로 가져올 필요가 없어요. CustomSmartPointer에 Drop 트레이트를 구현하고, println!을 호출하는 drop 메서드 구현을 제공했죠. drop 메서드의 본문은 타입의 인스턴스가 스코프를 벗어날 때 실행하고 싶은 어떤 로직이든 넣는 자리예요. 여기서는 Rust가 drop을 언제 호출하는지 눈으로 확인하려고 텍스트를 출력하고 있습니다.
main에서 CustomSmartPointer 인스턴스 두 개를 만들고 CustomSmartPointers created를 출력해요. main 끝에서 CustomSmartPointer 인스턴스가 스코프를 벗어나면, Rust가 drop 메서드에 넣은 코드를 호출해 마지막 메시지를 출력합니다. 참고로 우리는 drop 메서드를 명시적으로 호출할 필요가 없었어요.
이 프로그램을 실행하면 다음 출력을 보게 됩니다.
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.60s
Running `target/debug/drop-example`
CustomSmartPointers created
Dropping CustomSmartPointer with data `other stuff`!
Dropping CustomSmartPointer with data `my stuff`!
인스턴스가 스코프를 벗어날 때 Rust가 우리가 지정한 코드를 호출하며 drop을 자동으로 실행했어요. 변수는 생성된 순서의 역순으로 드롭되기 때문에 d가 c보다 먼저 드롭됩니다. 이 예시의 목적은 drop 메서드가 어떻게 동작하는지 시각적으로 보여주는 거예요. 보통은 출력 메시지 대신 타입이 실행해야 할 정리 코드를 지정하겠죠.
std::mem::drop로 조기 정리하기 (Early Cleanup with std::mem::drop)
안타깝게도 자동 drop 기능을 끄는 것은 간단하지 않아요. drop을 끄는 게 대개 필요한 경우는 없습니다. Drop 트레이트의 핵심은 자동으로 처리된다는 점이니까요. 하지만 때때로 값을 일찍 정리하고 싶을 수 있어요. 락을 관리하는 스마트 포인터를 사용하는 경우가 그 예시인데, 락을 해제하는 drop 메서드를 강제로 실행해서 같은 스코프의 다른 코드가 락을 얻을 수 있게 하고 싶을 수 있거든요. Rust는 Drop 트레이트의 drop 메서드를 직접 호출하게 해 주지 않아요. 대신 값이 스코프 끝보다 먼저 드롭되도록 강제하고 싶으면 표준 라이브러리가 제공하는 std::mem::drop 함수를 호출해야 합니다.
Listing 15-14의 main 함수를 수정해 Drop 트레이트의 drop 메서드를 직접 호출하려는 시도는, Listing 15-15처럼 동작하지 않아요.
Filename: src/main.rs
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");
}
Listing 15-15: 일찍 정리하려고 Drop 트레이트의 drop 메서드를 직접 호출하려는 시도
이 코드를 컴파일하려고 하면 다음 에러가 나요.
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
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`.
error: could not compile `drop-example` (bin "drop-example") due to 1 previous error
이 에러 메시지는 drop을 명시적으로 호출할 수 없다는 뜻입니다. 에러 메시지는 인스턴스를 정리하는 함수를 뜻하는 일반적인 프로그래밍 용어인 **소멸자(destructor)**라는 단어를 써요. 소멸자는 인스턴스를 생성하는 생성자(constructor)에 대응하는 개념이에요. Rust의 drop 함수는 특별한 소멸자 하나입니다.
Rust는 drop을 직접 호출하지 못하게 하는데, 그 이유는 Rust가 main 끝에서 그 값에 대해 여전히 자동으로 drop을 호출할 것이기 때문이에요. 그러면 같은 값을 두 번 정리하려는 셈이라 이중 해제(double free) 에러가 발생하게 됩니다.
값이 스코프를 벗어날 때 drop이 자동으로 삽입되는 것을 비활성화할 수 없고, drop 메서드를 직접 호출할 수도 없어요. 그래서 값을 일찍 정리하도록 강제해야 한다면 std::mem::drop 함수를 사용합니다.
std::mem::drop 함수는 Drop 트레이트의 drop 메서드와 달라요. 강제로 드롭하고 싶은 값을 인자로 전달해 호출합니다. 이 함수는 prelude에 있으므로, Listing 15-15의 main을 수정해 Listing 15-16처럼 drop 함수를 호출할 수 있어요.
Filename: src/main.rs
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");
drop(c);
println!("CustomSmartPointer dropped before the end of main");
}
Listing 15-16: 값이 스코프를 벗어나기 전에 std::mem::drop을 호출해 명시적으로 드롭하기
이 코드를 실행하면 다음이 출력됩니다.
$ cargo run
Compiling drop-example v0.1.0 (file:///projects/drop-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.73s
Running `target/debug/drop-example`
CustomSmartPointer created
Dropping CustomSmartPointer with data `some data`!
CustomSmartPointer dropped before the end of main
Dropping CustomSmartPointer with data some data!라는 텍스트가 CustomSmartPointer created와 CustomSmartPointer dropped before the end of main 사이에 출력되는데, 이는 drop 메서드 코드가 그 시점에 c를 드롭하기 위해 호출되었음을 보여줘요.
Drop 트레이트 구현에 지정한 코드는 정리를 편리하고 안전하게 만들기 위해 여러 방식으로 쓸 수 있어요. 예를 들어 자신만의 메모리 할당자를 만드는 데 쓸 수도 있죠! Drop 트레이트와 Rust의 소유권 시스템 덕분에, Rust가 자동으로 정리해 주므로 언제 청소해야 할지 기억할 필요가 없습니다.
또한 실수로 아직 사용 중인 값을 정리해서 생기는 문제를 걱정할 필요도 없어요. 참조가 항상 유효하도록 보장하는 소유권 시스템은 값이 더 이상 사용되지 않을 때 drop이 정확히 한 번만 호출되도록도 보장합니다.
지금까지 Box<T>와 스마트 포인터의 몇 가지 특징을 살펴봤으니, 표준 라이브러리에 정의된 다른 스마트 포인터 몇 개를 이어서 볼게요.
더 알아보기 (Learn more)
Drop트레이트 문서 —Drop트레이트와std::mem::drop함수의 전체 API를 확인해 보세요.- The Rustonomicon: Destructors — 소멸자와 메모리 정리에 대해 더 깊이 알고 싶다면 참고하세요.