Drop 트레이트로 메모리 정리 코드 실행하기

스마트 포인터 패턴에서 중요한 트레이트 그 두 번째는 Drop인데, 이는 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할지 커스터마이징하게끔 해줍니다. 어떠한 타입이든 Drop 트레이트를 구현할 수 있고, 이 코드가 파일이나 네트워크 연결 같은 자원 해제에 사용되게 할 수 있습니다.

스마트 포인터에 대한 맥락에서 Drop을 소개하는 이유는 Drop 트레이트의 기능이 스마트 포인터를 구현할 때 거의 항상 이용되기 때문입니다. 예를 들어 Box<T>가 버려질 때는 이 박스가 가리키고 있는 힙 공간의 할당을 해제할 것입니다.

몇몇 언어들에서는 어떤 타입의 인스턴스 사용을 끝낼 때마다 프로그래머가 직접 메모리 혹은 자원을 해제하는 코드를 호출해 줘야 합니다. 그 예에는 파일 핸들, 소켓, 또는 락이 포함됩니다. 해제를 잊어버리면 시스템은 과부하에 걸리고 멈출 수도 있습니다. 러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되는 특정 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 삽입해 줄 것입니다. 결과적으로, 프로그램 내에서 특정 타입의 인스턴스 사용이 끝나는 지점마다 메모리 정리 코드를 집어넣는 것에 관한 걱정하지 않아도 됩니다. 여전히 자원 누수는 발생하지 않을 테니까요!

Drop 트레이트를 구현하여 어떤 값이 스코프 밖으로 벗어났을 때 실행되는 코드를 지정합니다. Drop 트레이트는 drop이라는 이름의 메서드 하나를 구현해야 하는데 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖습니다. 러스트가 언제 drop을 호출하는지 알아보기 위해서, 지금은 println! 구문을 써서 drop을 구현해 봅시다.

예제 15-14는 러스트가 drop 함수를 호출하는 시점을 보여주기 위해서, 인스턴스가 스코프 밖으로 벗어났을 때 Dropping CustomSmartPointer!를 출력하는 커스텀 기능만을 갖춘 CustomSmartPointer 구조체를 보여줍니다.

파일명: 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.");
}

예제 15-14: 메모리 정리 코드를 집어넣게 될 Drop 트레이트를 구현한 CustomSmartPointer 구조체

Drop 트레이트는 프렐루드에 포함되어 있으므로, 이를 스코프로 가져올 필요는 없습니다. CustomSmartPointer에는 Drop 트레이트가 구현되어 여기에 println!을 호출하는 drop 메서드 구현체를 제공하였습니다. drop 함수의 본문 부분에는 해당 타입의 인스턴스가 스코프 밖으로 벗어났을 때 실행시키고 싶은 어떠한 로직이라도 집어넣을 수 있습니다. 여기서는 러스트가 drop을 호출하게 될 때를 보여주기 위해서 어떤 텍스트를 출력하는 중입니다.

main에서는 두 개의 CustomSmartPointer 인스턴스를 만든 다음 CustomSmartPointers created.를 출력합니다. main의 끝부분에서, CustomSmartPointer 인스턴스들은 스코프 밖으로 벗어날 것이고, 러스트는 drop 메서드에 집어넣은 코드를 호출할 것이고, 이는 마지막 메시지를 출력합니다. drop 메서드를 명시적으로 호출할 필요가 없다는 점을 주목하세요.

이 프로그램을 실행시키면 다음과 같은 출력을 보게 될 것입니다:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [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`!

러스트는 인스턴스가 스코프 밖으로 벗어났을 때 drop을 호출했고, 이것이 지정해 두었던 코드를 실행시켰습니다. 변수들은 만들어진 순서의 역순으로 버려지므로, dc보다 먼저 버려집니다. 이 예제의 목적은 여러분에게 drop 메서드가 어떻게 동작하는지에 대한 시각적인 가이드를 제공하는 것입니다; 보통은 메시지 출력이 아니라 여러분의 타입에 대해 실행해야 하는 메모리 정리 코드를 지정하게 될 것입니다.

std::mem::drop으로 값을 일찍 버리기

불행하게도 자동적인 drop 기능을 비활성화하는 일은 직관적이지 않습니다. drop 비활성화는 보통 필요가 없습니다; Drop 트레이트의 요점은 이것이 자동으로 이루어진다는 것이니까요. 하지만 가끔은 어떤 값을 일찍 정리하고 싶을 때도 있습니다. 한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다: 강제로 drop 메서드를 실행하여 락을 해제해서 같은 스코프의 다른 코드에서 해당 락을 얻도록 하고 싶을 수도 있지요. 러스트는 수동으로 Drop 트레이트의 drop 메서드를 호출하게 해주지는 않는 대신, 표준 라이브러리가 제공하는 std::mem::drop 함수를 호출하여 스코프가 끝나기 전에 강제로 값을 버리도록 할 수 있습니다.

예제 15-14의 main 함수를 예제 15-15처럼 수정하여 Drop 트레이트의 drop 메서드를 수동으로 호출하려고 하면 컴파일 에러가 납니다:

파일명: 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.");
}

예제 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: `drop(c)`

For more information about this error, try `rustc --explain E0040`.
error: could not compile `drop-example` due to previous error

이 에러 메시지는 drop을 명시적으로 호출하는 것이 허용되지 않음을 기술하고 있습니다. 에러 메시지에서 소멸자 (destructor) 라는 용어가 사용되었는데, 이는 인스턴스를 정리하는 함수에 대한 일반적인 프로그래밍 용어입니다. 소멸자는 인스턴스를 생성하는 생성자 (constructor) 와 유사한 용어입니다. 러스트의 drop 함수는 특정한 형태의 소멸자입니다.

러스트는 drop을 명시적으로 호출하도록 해주지 않는데 이는 러스트가 여전히 main의 끝부분에서 그 값에 대한 drop 호출을 자동으로 할 것이기 때문입니다. 이는 러스트가 동일한 값에 대해 두 번 메모리 정리를 시도할 것이므로 중복 해제 (double free) 에러가 될 수 있습니다.

어떤 값이 스코프 밖으로 벗어났을 때의 자동적인 drop 호출을 비활성화할 수 없고, drop 메서드를 명시적으로 호출할 수도 없습니다. 따라서, 어떤 값에 대한 메모리 정리를 강제로 일찍 하기 원할 때는 std::mem::drop 함수를 이용합니다.

std::mem::drop 함수는 Drop 트레이트에 있는 drop 메서드와는 다릅니다. 이 함수에 일찍 버리려고 하는 값을 인수로 넘겨 호출합니다. 이 함수는 프렐루드에 포함되어 있어서, 예제 15-14의 main을 예제 15-16처럼 수정할 수 있습니다:

파일명: 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.");
}

예제 15-16: std::mem::drop을 호출하여 값이 스코프를 벗어나기 전에 명시적으로 버리기

이 코드를 실행하면 아래와 같이 출력할 것입니다:

$ cargo run
   Compiling drop-example v0.1.0 (file:///projects/drop-example)
    Finished dev [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. 사이에 출력되는데, 이는 c를 버리는 drop 메서드가 그 지점에서 호출되었음을 보여줍니다.

Drop 트레이트 구현체에 지정되는 코드를 다양한 방식으로 사용하여 메모리 정리를 편리하고 안전하게 할 수 있습니다: 예를 들면, 이것을 사용하여 여러분만의 고유한 메모리 할당자를 만들 수 있습니다! Drop 트레이트와 러스트의 소유권 시스템을 이용하면 러스트가 메모리 정리를 자동으로 수행하기 때문에 메모리 정리를 기억해 두지 않아도 됩니다.

또한 아직 사용 중인 값이 뜻하지 않게 정리되면서 발생하는 문제도 걱정할 필요 없습니다: 참조자가 항상 유효하도록 보장해 주는 소유권 시스템은 그 값이 더 이상 사용되지 않을 때 drop이 한 번만 호출되는 것도 보장합니다.

지금까지 Box<T>와 스마트 포인터의 몇 가지 특성을 시험해 보았으니, 표준 라이브러리에 정의되어 있는 몇 가지 다른 스마트 포인터를 살펴봅시다.