Rust drop 순서: 지역 변수, 필드, 임시값, 그리고 let _가 바로 drop되는 이유

Rust에서 지역 변수는 선언의 역순, 구조체 필드는 선언 순서로 drop됩니다. 임시값은 보통 문장 끝에서 사라지고 let _는 값을 묶지 않습니다. Rust 2024 에디션의 if let·꼬리 식 변경까지 레퍼런스로 정리합니다.

rustdropraiirust-2024language-spec

Rust에서 뮤텍스 잠금, 파일 핸들, 트랜잭션처럼 “스코프가 끝날 때 정리되는” 값은 Drop이 언제 실행되는지에 동작이 달려 있습니다. 대부분은 직관대로 움직이지만, let _ = mutex.lock().unwrap();처럼 잠금을 잡았다고 생각한 줄에서 바로 잠금이 풀리거나, if let의 else 블록에서 같은 잠금을 다시 잡으려다 교착 상태에 빠지는 경우가 있습니다.

이 글은 The Rust Reference의 destructors 장과 Rust 2024 에디션 가이드를 기준으로 drop 순서 규칙을 정리합니다. 난이도는 중급입니다. 소유권과 Drop 트레이트를 알고 있다고 가정합니다.

기본 규칙

레퍼런스의 규칙을 요약하면 다음과 같습니다.

  • 모든 변수와 임시값은 drop 스코프에 속합니다. 제어 흐름이 그 스코프를 떠나면 그 스코프에 속한 변수는 선언의 역순으로, 임시값은 생성의 역순으로 drop됩니다.
  • let으로 선언한 지역 변수는 그 let을 담고 있는 블록의 스코프에 속합니다.
  • 함수 매개변수는 함수 본문 전체의 스코프에 속하므로 함수가 끝날 때 가장 나중에 drop됩니다.
  • 값 하나를 drop할 때는, 그 타입이 Drop을 구현했다면 먼저 Drop::drop을 호출하고 그다음 필드를 재귀적으로 drop합니다. 구조체 필드는 선언 순서대로, 튜플 요소는 순서대로, 배열이나 소유한 슬라이스의 원소는 첫 번째부터 마지막까지 drop됩니다.
  • 패턴 하나 안에서 묶인 변수는 패턴 안의 선언 역순으로 drop됩니다.

지역 변수는 역순, 필드는 정순입니다. 둘의 방향이 반대라는 점을 기억해 두면 나머지는 대부분 여기서 따라 나옵니다.

let _는 값을 묶지 않는다

레퍼런스의 패턴 장은 와일드카드 패턴 _에 대해 “식별자 패턴과 달리 매치한 값을 복사하거나, 이동하거나, 빌리지 않는다”고 적고 있습니다. 반면 _c처럼 밑줄로 시작하는 이름은 평범한 식별자 패턴이라서 실제로 변수에 묶입니다. 값이 Copy 타입이면 복사하고, 아니면 이동합니다. 아래 예제의 Noisy는 Copy가 아니므로 값이 이동합니다. 이름만 보면 비슷하지만 동작은 다릅니다.

여기에 임시값 규칙이 더해집니다. let 문의 초기화 식은 플레이스(place) 식 문맥이고, 함수 호출 같은 값 식이 이 자리에 오면 이름 없는 임시 저장소가 만들어집니다. 이 임시값의 drop 스코프는 보통 그 문장의 끝입니다. let x = &temp();처럼 참조로 묶는 경우에는 임시값의 수명이 블록 끝까지 연장되지만, _는 아무것도 묶지 않으므로 연장되지 않습니다.

아래 코드로 확인해 보겠습니다.

struct Noisy(&'static str);

impl Drop for Noisy {
    fn drop(&mut self) {
        println!("drop {}", self.0);
    }
}

struct Pair {
    first: Noisy,
    second: Noisy,
}

fn main() {
    let a = Noisy("a");
    let _ = Noisy("b");
    let _c = Noisy("c");
    let pair = Pair { first: Noisy("pair.first"), second: Noisy("pair.second") };
    let _ = a;
    println!("end of main");
}

아래 출력은 레퍼런스 규칙으로 도출한 것이며, 이 글을 쓴 환경에서 실제로 실행한 결과는 아닙니다.

drop b
end of main
drop pair.first
drop pair.second
drop c
drop a
  • let _ = Noisy("b");의 Noisy("b")는 임시값이고 _에 묶이지 않으므로 그 let 문이 끝나는 즉시 drop됩니다. end of main보다 먼저 찍히는 이유입니다.
  • let _ = a;는 a를 이동시키지 않습니다. 이 줄에서는 아무 일도 일어나지 않고, a는 그대로 남아 있다가 main이 끝날 때 drop됩니다.
  • main 끝에서 변수는 선언의 역순인 pair, _c, a 순서로 drop됩니다.
  • pair를 drop할 때는 필드가 선언 순서대로 first, second 순서로 drop됩니다.

잠금에서 문제가 되는 경우

rustc의 lint 문서에 실린 예는 다음과 같습니다.

let _ = data.lock().unwrap();
println!("doing some work");

data.lock().unwrap()이 돌려준 MutexGuard는 임시값이고 _에 묶이지 않으므로 이 문장이 끝나는 즉시 drop됩니다. 즉 잠금이 바로 풀리고, 다음 줄은 잠금 없이 실행됩니다. rustc는 이 패턴을 let_underscore_lock lint로 잡으며, 2026-10-14에 확인한 lint 목록에서는 기본값이 deny라서 컴파일 오류가 됩니다.

다만 이 lint는 동기화 잠금만 대상으로 합니다. 직접 만든 가드 타입처럼 Drop을 구현한 다른 값에 쓰이는 let_underscore_drop lint는 기본값이 allow이므로, 따로 켜지 않으면 경고 없이 지나갑니다. lint 문서가 권하는 방법은 두 가지입니다.

  • 스코프 끝까지 유지하려면 let _guard = ...처럼 밑줄로 시작하는 이름에 묶습니다.
  • 정말 바로 버리려는 의도라면 drop(...)을 호출해 의도를 드러냅니다.

std::mem::drop은 특별한 기능이 아니라 pub fn drop<T>(_x: T) {}로 정의된 평범한 함수입니다. 값을 인자로 받아 이동시키고, 함수가 끝날 때 그 값이 drop됩니다. 그래서 Copy 타입에 쓰면 복사본이 넘어가므로 원래 값에는 아무 영향이 없습니다.

임시값의 스코프

임시값의 drop 스코프는 수명 연장이 없을 때, 그 식을 감싸는 다음 스코프 중 가장 작은 것입니다. 레퍼런스가 나열한 것 가운데 자주 만나는 것만 추리면 이렇습니다.

  • 문장 하나
  • if, while, loop의 본문, if의 else 블록
  • if나 while의 일반 조건식(패턴 매칭이 아닌 것)
  • &&, ||의 각 피연산자
  • match 갈래의 본문
  • 블록의 꼬리 식 전체 (2024 에디션)
  • if let의 조건과 then 블록 (2024 에디션)

match의 검사 대상 식(scrutinee)은 이 목록에 없습니다. 레퍼런스는 이 점을 명시하며, 그 결과 scrutinee에서 만든 임시값은 match 식이 끝난 뒤, 보통 문장 끝까지 살아 있습니다. 예를 들어 match *value.read().unwrap() { ... }라고 쓰면 읽기 잠금은 모든 갈래가 실행되는 동안 유지됩니다. 이것은 에디션과 관계없이 같습니다.

Rust 2024 에디션에서 바뀐 두 가지

에디션 가이드에 따르면 Rust 2024 에디션은 Rust 1.85.0에서 안정화되었고, 임시값 스코프를 좁히는 규칙 두 가지가 추가되었습니다. 에디션은 크레이트 단위 설정이며 Cargo에서는 Cargo.toml의 [package]에 있는 edition 키로 정합니다. Cargo 문서는 이 키가 없으면 2015 에디션으로 간주한다고 적고 있습니다.

if let의 임시값은 else 전에 drop된다

에디션 가이드의 예를 옮기면 다음과 같습니다.

use std::sync::RwLock;

fn f(value: &RwLock<Option<bool>>) {
    if let Some(x) = *value.read().unwrap() {
        println!("value is {x}");
    } else {
        let mut v = value.write().unwrap();
        if v.is_none() {
            *v = Some(true);
        }
    }
}

2021 에디션까지는 value.read()가 만든 읽기 잠금 임시값이 if let 식 전체가 끝날 때까지, 즉 else 블록이 끝난 뒤까지 살아 있었습니다. 그래서 else로 들어가 쓰기 잠금을 잡으려 하면 교착 상태가 됩니다. 2024 에디션에서는 then 블록이 끝나거나 제어가 else로 넘어가는 시점에 임시값이 drop되므로 이 코드가 의도대로 동작합니다.

반대로 예전처럼 임시값을 else 너머까지 유지해야 하는 코드도 있을 수 있습니다. 에디션 가이드는 if let을 match로 바꾸는 것이 항상 안전하다고 설명합니다. match의 scrutinee 임시값은 앞에서 본 것처럼 match 식이 끝난 뒤까지 살아 있으므로 2021 에디션의 if let과 같은 동작이 됩니다. cargo fix --edition은 if_let_rescope lint를 통해 이런 match 재작성을 제안하는데, 가이드는 적용 후 각 변경이 실제로 필요한지 검토하라고 권합니다. 위 예에서 match로 바꾸면 교착 상태가 다시 생깁니다.

꼬리 식의 임시값은 지역 변수보다 먼저 drop된다

블록이나 함수 본문의 마지막 식(꼬리 식)에서 만든 임시값은 2021 에디션까지 블록의 지역 변수보다 늦게 drop되었습니다. 그래서 다음 코드는 2021 에디션에서 c가 충분히 오래 살지 않는다는 E0597 오류가 납니다.

use std::cell::RefCell;

fn f() -> usize {
    let c = RefCell::new("..");
    c.borrow().len()
}

2024 에디션에서는 c.borrow()가 만든 임시값이 먼저 drop되고 그다음 c가 drop되므로 컴파일됩니다.

이 변경 때문에 반대로 2024 에디션에서 컴파일되지 않는 코드도 있습니다. 에디션 가이드의 예인 let x = { &String::from("1234") }.len();은 2021 에디션에서는 임시 String이 문장 끝까지 연장되었지만, 2024 에디션에서는 블록 끝에서 drop되므로 빌린 동안 drop된다는 오류가 납니다. 가이드가 제시한 해결책은 블록 결과를 let s = { &String::from("1234") };처럼 지역 변수에 먼저 묶어 수명 연장을 받는 것입니다.

꼬리 식 변경에는 의미를 보존하는 자동 재작성이 없습니다. tail_expr_drop_order lint는 영향을 받을 수 있는 값을 알려 줄 뿐 코드를 고치지 않으므로, 경고를 직접 보고 판단해야 합니다.

정리하면

  • 같은 블록의 지역 변수는 선언의 역순, 구조체 필드는 선언 순서로 drop됩니다. 다른 언어에서 비슷한 규칙을 본 적이 있다면, Go의 defer가 등록 역순으로 실행되고 Java의 try-with-resources가 자원을 선언 역순으로 닫는 것과 같은 방향입니다.
  • let _ = expr;은 값을 묶지 않습니다. expr이 임시값이면 문장 끝에서 바로 drop되고, 기존 변수라면 이동시키지 않습니다. 스코프 끝까지 유지하려면 _guard 같은 이름을 씁니다.
  • match scrutinee의 임시값은 match 뒤까지 살아 있습니다.
  • 2024 에디션에서는 if let 임시값이 else 전에, 꼬리 식 임시값이 지역 변수보다 먼저 drop됩니다. 에디션을 올릴 때 잠금이나 RefCell 빌림이 걸린 코드는 이 두 lint의 경고를 확인하는 것이 좋습니다.

범위와 출처

이 글은 2026-10-14에 확인한 The Rust Reference와 Rust Edition Guide의 내용을 기준으로 합니다. 확인한 레퍼런스 페이지에는 버전 표기가 없었고, 페이지 안의 테스트 링크가 rust-lang/rust의 1.99.0 태그를 가리키고 있었습니다. 레퍼런스는 임시값 수명 연장의 정확한 규칙이 바뀔 수 있으며 현재 동작을 설명한 것이라고 밝히고 있습니다. lint의 기본 수준(deny/allow)도 rustc 문서의 확인 시점 기준입니다.

패닉 중 언와인딩에서의 drop, 클로저가 이동으로 캡처한 변수의 drop 순서(레퍼런스상 명시되지 않음), ManuallyDrop과 mem::forget, async와 Drop의 관계는 다루지 않습니다. 예제 출력은 규칙으로 도출한 것이며 실행 결과가 아닙니다.

아래 링크는 2026-10-14에 확인했습니다.

블로그로