본문 바로가기

Drop과 자원 정리

값이 스코프를 벗어날 때 Drop으로 자원을 자동 정리하고 std::mem::drop을 사용해 필요한 시점에 일찍 해제합니다.

스마트 포인터 패턴에서 중요한 두 번째 트레이트는 Drop인데, 이는 어떤 값이 스코프 밖으로 벗어나려고 할 때 무슨 일을 할지 커스터마이징하게끔 해줍니다.

어떠한 타입이든 Drop 트레이트를 구현할 수 있고, 이 코드가 파일이나 네트워크 연결 같은 자원 해제에 사용되게 할 수 있습니다.

스마트 포인터에 대한 맥락에서 Drop을 소개하는 이유는 Drop 트레이트의 기능이 스마트 포인터를 구현할 때 거의 항상 이용되기 때문입니다.

예를 들어 Box<T>가 버려질 때는 이 박스가 가리키고 있는 힙 공간의 할당을 해제할 것입니다.

몇몇 언어들에서는 어떤 타입의 인스턴스 사용을 끝낼 때마다 프로그래머가 직접 메모리 혹은 자원을 해제하는 코드를 호출해 줘야 합니다.

그 예에는 파일 핸들, 소켓, 또는 락이 포함됩니다.

해제를 잊어버리면 시스템은 과부하에 걸리고 멈출 수도 있습니다.

러스트에서는 값이 스코프 밖으로 벗어날 때마다 실행되는 특정 코드를 지정할 수 있고, 컴파일러가 이 코드를 자동으로 삽입해 줄 것입니다.

결과적으로, 프로그램 내에서 특정 타입의 인스턴스 사용이 끝나는 지점마다 메모리 정리 코드를 집어넣는 것을 걱정하지 않아도 됩니다.

정상 drop 경로의 자원 정리를 자동화할 수 있습니다. 다만 Rc 순환, mem::forget, 비정상 프로세스 종료 등에서는 누수나 정리 생략이 가능하며 Drop이 모든 누수를 막지는 않습니다.

Drop 트레이트를 구현하여 어떤 값이 스코프 밖으로 벗어났을 때 실행되는 코드를 지정합니다.

Drop 트레이트를 구현하려면 drop이라는 이름의 메서드 하나를 구현해야 하는데 이 메서드는 self에 대한 가변 참조자를 매개변수로 갖습니다.

러스트가 언제 drop을 호출하는지 알아보기 위해서, 지금은 println! 구문을 써서 drop을 구현해봅시다.

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

예제 14-14: 메모리 정리 코드를 집어넣게 될 Drop 트레이트를 구현한 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.");
}

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을 호출했고, 이것이 지정해 두었던 코드를 실행시켰습니다.

변수들은 만들어진 순서의 역순으로 버려지므로, d가 c보다 먼저 버려집니다.

이 예제의 Drop::drop은 정리 시점을 보여 주는 메시지만 출력합니다. 메서드가 끝나면 러스트가 data: String 필드도 자동으로 버려 문자열의 자원을 정리합니다. 필드의 정리를 직접 다시 호출할 필요는 없습니다. 실제 타입의 Drop 구현에는 그 타입이 별도로 맡고 있는 자원을 해제하는 동작을 넣습니다.


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

대부분의 값은 스코프가 끝날 때 자동으로 정리되도록 두면 됩니다. 하지만 가끔은 스코프의 나머지 코드를 실행하기 전에 어떤 값을 일찍 정리하고 싶을 때도 있습니다.

한 가지 예는 락을 관리하는 스마트 포인터를 이용할 때입니다.

예를 들어 락을 잡고 있는 가드 값을 먼저 버리면, 같은 스코프의 다른 코드에서 그 락을 다시 얻을 수 있습니다. 여기서 가드는 살아 있는 동안 자원을 붙잡고 정리될 때 이를 해제하는 값이며, 가드를 버리는 것이 락 자체를 삭제한다는 뜻은 아닙니다.

러스트는 수동으로 Drop 트레이트의 drop 메서드를 호출하게 해주지는 않는 대신, 표준 라이브러리가 제공하는 std::mem::drop 함수를 호출하여 스코프가 끝나기 전에 강제로 값을 버리도록 할 수 있습니다.

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

예제 14-15: 메모리를 일찍 정리하기 위한 Drop 트레이트의 drop 메서드의 수동 호출 시도하기
src/main.rs
fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    c.drop();
    println!("CustomSmartPointer dropped before the end of main.");
}

이 코드의 컴파일을 시도하면 다음과 같은 에러를 얻게 됩니다.

$ 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

이 에러 메시지는 c.drop()처럼 Drop::drop 메서드를 직접 호출하는 것이 허용되지 않음을 기술하고 있습니다. 뒤에서 사용할 일반 함수 drop(c)까지 금지한다는 뜻은 아닙니다.

에러 메시지에서 소멸자(destructor)라는 용어가 사용되었는데, 이는 인스턴스를 정리하는 함수에 대한 일반적인 프로그래밍 용어입니다.

소멸자는 인스턴스를 생성하는 생성자(constructor)와 유사한 용어입니다.

러스트에서는 Drop::drop 메서드가 타입이 정리될 때 실행할 소멸자 동작을 정의합니다.

Drop::drop은 &mut self로 값을 잠깐 빌리므로, 직접 호출을 허용하더라도 호출자에게서 값의 소유권이 이동하지는 않을 것입니다. 그러면 값이 스코프를 벗어날 때 컴파일러가 삽입한 정리 과정도 다시 실행될 수 있습니다.

이는 러스트가 동일한 값에 대해 두 번 메모리 정리를 시도할 것이므로 중복 해제(double free) 에러가 될 수 있습니다.

보통의 사용에서는 스코프 종료 시 drop이 자동 실행되며 Drop::drop 메서드를 직접 호출할 수 없습니다. mem::forget이나 ManuallyDrop 같은 정리 생략 API는 별도 계약입니다.

따라서, 어떤 값에 대한 메모리 정리를 강제로 일찍 하고 싶을 때는 std::mem::drop 함수를 이용합니다.

std::mem::drop 함수는 Drop 트레이트에 있는 drop 메서드와는 다릅니다.

이 함수에 일찍 버리려고 하는 값을 인수로 넘겨 호출합니다.

std::mem::drop의 형태는 fn drop<T>(_x: T) {}입니다. 함수 본문에서 특별한 해제 명령을 실행하는 대신 값을 인수로 받아, 함수가 끝나며 그 인수가 정상적으로 정리되게 합니다. 따라서 CustomSmartPointer처럼 Copy가 아닌 값의 소유권을 넘기면 호출 뒤에는 원래 변수를 다시 사용할 수 없습니다. 공유 참조 &T나 정수처럼 Copy인 값을 넘기는 경우에는 복사본만 전달되므로 원래 소유 값을 일찍 정리하는 효과가 없습니다. 값을 인수로 받는 drop의 계약을 확인하세요.

이 함수는 프렐루드에 포함되어 있어서, 예제 14-14의 main을 예제 14-16처럼 수정할 수 있습니다.

아래 다이어그램은 값이 만들어진 뒤 자동 정리되거나, std::mem::drop으로 조금 더 이른 시점에 정리되는 흐름을 한 번에 보여줍니다.

Drop 스코프와 조기 해제 순서

생성→소유 이동→std mem drop 소비→Drop 구현→선언 순서 필드 정리의 실제 자원 생애이다. 이동 시점 정리와 c.drop 직접 호출을 구분한다.

SEQUENCE · OWNERSHIP RELEASE

drop(c)는 값을 소비하여 스코프 끝보다 일찍 정리한다

std::mem::drop(value)는 값을 소비하는 일반 함수다. 정상 정리에서는 Drop::drop이 먼저 실행되고 이어 필드가 선언 순서대로 정리되며, 같은 스코프의 지역 변수는 보통 선언의 역순으로 끝난다.

Drop 스코프와 조기 해제 순서 값 생성과 이동, std::mem::drop에 의한 조기 소유권 종료, Drop 구현 호출, 필드 정리 순서를 시간축으로 보여 주는 시퀀스 create value move ownership std::mem::drop(value) Drop::drop(&mut self) 필드 · 선언 순서 cleanup complete SCOPE 외부 스코프 OWNER 현재 소유자 DROP Drop::drop FIELDS 필드 정리 Drop 구현 다음 필드 정리 · 직접 메서드 호출은 금지 · 정상 정리 경로의 모델
정리 시퀀스의 여섯 메시지원 desktop의4참여자·6메시지를모바일에서는6독립송신/수신쌍으로반복표현한다. 번호는시간순서,화살표는각실제메시지방향이며그룹사이새간선은없다. 마지막쌍은필드자원→스코프로돌아오는완료통지로원반환방향을보존한다. Drop구현이먼저실행되고이어필드정리한다. mem::drop은일반값소비함수이며참조만버리면참조대상이소멸하는것이아니다.1 · 메시지스코프소유자값 생성2 · 메시지스코프소유자소유권 이동3 · 메시지스코프소유자drop(value)4 · 메시지소유자Drop 구현Drop::drop5 · 메시지Drop 구현필드 · 자원필드 정리6 · 메시지필드 · 자원스코프정리 완료
  1. 생성과 이동

    값을 만들고 다른 바인딩으로 이동하면 새 소유자가 나중 정리를 맡습니다.

  2. 조기 종료

    std::mem::drop(value)는 값을 소비해 소유권을 즉시 끝냅니다.

  3. Destructor

    정상 경로에서 Drop::drop이 먼저 호출됩니다.

  4. 필드 정리

    그 뒤 구조체 필드가 선언 순서대로 정리됩니다.

  5. 증거 경계

    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은 일반적인 값 소비 함수이며 참조만 버리면 참조 대상이 소멸하는 것이 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
소유 값 생성·이동

소유 값을 만든 뒤 새 이름에 이동한다.

  • 이전 이름: moved
  • 새 이름: 정리 책임 보유
  • 이동 시점은 자원 정리 시점이 아니다.
조기 소비

std::mem::drop이 소유 값을 소비한다.

  • 호출: std::mem::drop(value)
  • 소유 값: 소비됨
  • 직접 c.drop 호출 대신 값을 소비하는 함수를 사용한다.
정리 순서

Drop 구현 뒤 필드가 선언 순서로 정리된다.

  • 첫 정리: Drop::drop 본문
  • 다음: 필드 선언 순서
  • 사용자 Drop과 필드 정리를 같은 한 호출로 오인하지 않는다.

Drop 스코프와 조기 해제 순서

생성→소유 이동→std mem drop 소비→Drop 구현→선언 순서 필드 정리의 실제 자원 생애이다. 이동 시점 정리와 c.drop 직접 호출을 구분한다.

SEQUENCE · OWNERSHIP RELEASE

drop(c)는 값을 소비하여 스코프 끝보다 일찍 정리한다

std::mem::drop(value)는 값을 소비하는 일반 함수다. 정상 정리에서는 Drop::drop이 먼저 실행되고 이어 필드가 선언 순서대로 정리되며, 같은 스코프의 지역 변수는 보통 선언의 역순으로 끝난다.

Drop 스코프와 조기 해제 순서 값 생성과 이동, std::mem::drop에 의한 조기 소유권 종료, Drop 구현 호출, 필드 정리 순서를 시간축으로 보여 주는 시퀀스 create value move ownership std::mem::drop(value) Drop::drop(&mut self) 필드 · 선언 순서 cleanup complete SCOPE 외부 스코프 OWNER 현재 소유자 DROP Drop::drop FIELDS 필드 정리 Drop 구현 다음 필드 정리 · 직접 메서드 호출은 금지 · 정상 정리 경로의 모델
정리 시퀀스의 여섯 메시지원 desktop의4참여자·6메시지를모바일에서는6독립송신/수신쌍으로반복표현한다. 번호는시간순서,화살표는각실제메시지방향이며그룹사이새간선은없다. 마지막쌍은필드자원→스코프로돌아오는완료통지로원반환방향을보존한다. Drop구현이먼저실행되고이어필드정리한다. mem::drop은일반값소비함수이며참조만버리면참조대상이소멸하는것이아니다.1 · 메시지스코프소유자값 생성2 · 메시지스코프소유자소유권 이동3 · 메시지스코프소유자drop(value)4 · 메시지소유자Drop 구현Drop::drop5 · 메시지Drop 구현필드 · 자원필드 정리6 · 메시지필드 · 자원스코프정리 완료
  1. 생성과 이동

    값을 만들고 다른 바인딩으로 이동하면 새 소유자가 나중 정리를 맡습니다.

  2. 조기 종료

    std::mem::drop(value)는 값을 소비해 소유권을 즉시 끝냅니다.

  3. Destructor

    정상 경로에서 Drop::drop이 먼저 호출됩니다.

  4. 필드 정리

    그 뒤 구조체 필드가 선언 순서대로 정리됩니다.

  5. 증거 경계

    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은 일반적인 값 소비 함수이며 참조만 버리면 참조 대상이 소멸하는 것이 아니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
소유 값 생성·이동

소유 값을 만든 뒤 새 이름에 이동한다.

  • 이전 이름: moved
  • 새 이름: 정리 책임 보유
  • 이동 시점은 자원 정리 시점이 아니다.
조기 소비

std::mem::drop이 소유 값을 소비한다.

  • 호출: std::mem::drop(value)
  • 소유 값: 소비됨
  • 직접 c.drop 호출 대신 값을 소비하는 함수를 사용한다.
정리 순서

Drop 구현 뒤 필드가 선언 순서로 정리된다.

  • 첫 정리: Drop::drop 본문
  • 다음: 필드 선언 순서
  • 사용자 Drop과 필드 정리를 같은 한 호출로 오인하지 않는다.
예제 14-16: std::mem::drop을 호출하여 값이 스코프를 벗어나기 전에 명시적으로 버리기
src/main.rs
fn main() {
    let c = CustomSmartPointer {
        data: String::from("some data"),
    };
    println!("CustomSmartPointer created.");
    drop(c);
    println!("CustomSmartPointer dropped before the end of main.");
}

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

$ 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 스코프가 끝나거나 값이 drop(c)로 소비될 때 한 번 정리됩니다. 마지막 사용 지점과 drop 스코프 종료는 같지 않을 수 있습니다.

예시

이미 정의한 CustomSmartPointer를 유지하고 main을 다음으로 바꾸어 참조와 소유 값을 구별해 봅시다.

src/main.rs의 main 교체
fn main() {
    let p = CustomSmartPointer { data: String::from("kept") };
    drop(&p);
    println!("after reference");
    drop(p);
    println!("after owner");
}

예상 표준 출력의 순서는 after reference, Dropping CustomSmartPointer with data `kept`!, after owner입니다. drop(&p)는 참조를 인수로 받으므로 p의 소유 값을 소비하지 않으며, 컴파일러도 참조를 버리는 호출에 경고할 수 있습니다. 그 경고와 실행 실패를 혼동하지 마세요.

스스로 정리 순서 설명하기

위 main에서 drop(p) 한 줄만 제거하면 소멸자 출력은 어느 두 출력의 사이 또는 뒤에 오나요? 또 drop(p)를 그대로 둔 채 그 뒤에 println!("{}", p.data)를 더하면 정상 출력이 한 줄 추가되는지, 그 전에 컴파일이 거부되는지 설명하세요. panic·forget·프로세스 강제 종료는 이 문제에서 제외합니다.

정리 시점 확인

소유 값을 일찍 버리는 호출을 제거하면 두 일반 출력이 먼저 나오고 main의 drop 스코프 종료에서 소멸자 메시지가 뒤에 옵니다. 참조를 잠깐 사용했다는 이유로 p가 그때 자동 정리되지 않습니다. drop(p) 뒤 p.data를 다시 읽는 시도는 이미 소유 값이 이동되었으므로 컴파일이 거부됩니다. 정상 경로에서 같은 값을 두 번 정리하는 출력이 추가되는 것이 아닙니다.

mem::drop의 값 소비와 drop 스코프·필드 정리 순서를 나누어 확인하세요.

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

이 페이지 목차

이 페이지 목차