본문 바로가기

panic!과 복구 불가능한 오류

복구할 수 없는 상태에서 panic을 발생시키고 스택 되감기와 중단 방식을 구분하며 백트레이스로 원인을 추적합니다.

소프트웨어에는 오류가 따르기 마련이므로, 러스트에는 무언가 잘못되었을 때를 다루는 기능이 여럿 있습니다. 러스트에서는 대부분 코드가 컴파일되기 전에 오류가 날 수 있다는 것을 알고 어떻게 할지 정해야 하므로, 오류를 찾아 처리했는지 확인한 뒤에야 코드를 배포하게 되어 프로그램이 더 튼튼해집니다. 다만 파일이나 입력에서 실제로 어떤 실패가 일어날지까지 컴파일러가 아는 것은 아닙니다.

러스트는 오류를 복구 가능한(recoverable) 오류와 복구 불가능한(unrecoverable) 오류 두 가지로 나눕니다. 파일을 찾을 수 없는 오류처럼 복구 가능한 오류라면 대개 사용자에게 문제를 알리고 다시 시도하게 하면 됩니다. 복구 불가능한 오류는 배열의 끝을 넘는 곳에 접근하는 것처럼 버그의 증상인 경우가 많으므로, 프로그램을 바로 멈추고 싶습니다.

대부분의 언어는 예외(exception) 같은 방법으로 두 종류의 오류를 구분하지 않고 같은 방식으로 다룹니다. 러스트에는 예외가 없습니다. 대신 복구 가능한 오류를 위한 Result<T, E> 타입과, 복구 불가능한 오류가 났을 때 실행을 멈추는 panic! 매크로가 있습니다. 이 글에서는 먼저 panic!을 부르는 방법을 보고, 다음 글에서 Result<T, E>를 돌려주는 방법을, 그다음 글에서 오류를 복구할지 멈출지 정하는 기준을 다룹니다.

패닉이 일어나는 방식

코드에서 나쁜 일이 일어나 손쓸 방법이 없을 때를 위해 러스트에는 panic! 매크로가 있습니다. 실제로 패닉이 일어나는 방법은 두 가지로, 배열의 끝을 넘는 접근처럼 패닉을 일으키는 동작을 하거나 panic!을 직접 부르는 것입니다. 어느 쪽이든 프로그램에 패닉이 납니다. 기본으로 패닉이 나면 실패 메시지를 출력하고, 현재 스레드의 스택을 되감으며(unwind) 정리한 뒤 끝납니다. 환경 변수를 주면 패닉이 날 때 호출 스택을 보여 주게 해 원인을 쉽게 찾을 수도 있습니다.

아래 그림은 실패가 복구할 수 있는 것인지에 따라 Result와 panic!이 나뉘고, 패닉이 나면 그 뒤의 정리 방식이 설정에 따라 달라지는 흐름을 보여 줍니다.

Rust Result와 panic 판단, 백트레이스 및 종료 전략

현재 스레드의 panic 뒤 unwind는 내려온 프레임의 Drop을 실행하고 abort는 프로세스를 중단하며 스택 Drop을 실행하지 않는 자원 변화다. 전략은 실행 중 선택하는 사용자 분기가 아닌 사전 설정된 두 별도 사례로 둔다.

Rust에서 복구 가능한 실패는 Result로 반환하고 계약 위반이나 버그는 panic으로 드러내며, 메시지와 위치에서 백트레이스의 첫 관련 사용자 프레임을 찾아 unwind와 abort의 정리 차이를 판단하는 흐름

RESULT · PANIC · BACKTRACE · STRATEGY

실패가 정상 입력이면 값으로 돌려주고, 계약이 깨졌다면 패닉을 조사한다

panic!은 범용 예외 처리 문법이 아닙니다. 호출자가 복구할 수 있는 실패와 계속 실행할 수 없는 내부 계약 위반을 먼저 나누고, 패닉이 발생하면 메시지에서 호출 경로와 깨진 전제로 조사 범위를 좁힙니다.

패닉 발생 뒤 전략에 따라 달라지는 정리패닉은 현재 스레드에서 시작한다. unwind는 내려온 스택 프레임을 정리하며 Drop을 실행하고 처리 경계에 따라 최종 결과가 달라진다. abort는 프로세스를 중단하고 스택의 Drop을 실행하지 않는다. OS의 자원 회수와 Rust의 Drop은 별개다.unwindabortpanic 발생현재 스레드에서 시작선택된panic 전략unwind내려온 프레임의 Drop 실행처리 경계가 최종 결과 결정abort프로세스 중단스택 Drop 실행 안 함OS 자원 회수와 별개
패닉 발생 뒤 전략에 따라 달라지는 정리패닉은 현재 스레드에서 시작한다. unwind는 내려온 스택 프레임을 정리하며 Drop을 실행하고 처리 경계에 따라 최종 결과가 달라진다. abort는 프로세스를 중단하고 스택의 Drop을 실행하지 않는다. OS의 자원 회수와 Rust의 Drop은 별개다.unwindabortpanic 발생현재 스레드에서 시작선택된panic 전략unwind내려온 프레임 정리Drop 실행최종 결과는처리 경계에 따름abort프로세스 중단스택 Drop 실행 안 함OS 자원 회수와 별개

보조 설명 · 아래 원래 코드와 실패 조건도 함께 확인합니다.

recoverable · Result<T, E>

실패가 예상 가능한 입력·환경 상태다

파일 없음, 잘못된 사용자 입력, 네트워크 실패처럼 호출자가 재시도·대체·보고할 수 있으면 Result로 반환합니다. 실패는 정상 제어 흐름의 값이며 호출자가 정책을 선택합니다.

contract / bug · panic!

내부 불변식이나 반드시 참인 전제가 깨졌다

범위 밖 인덱싱이나 검증된 상태의 불가능한 분기처럼 정상 결과를 만들 수 없으면 패닉이 버그를 드러낼 수 있습니다. 현재 스레드가 패닉을 시작하며, 전체 프로세스의 결과는 스레드 경계와 패닉 전략에 달려 있습니다.

message → backtrace → user frame

출력 한 줄에서 깨진 계약까지 세 단계로 거슬러 간다

  1. 패닉 메시지와 보고 위치를 읽는다

    메시지는 실패한 검사, 위치는 패닉이 발생한 지점을 보여줍니다. 라이브러리 내부 위치라면 그 자체를 근본 원인으로 단정하지 않습니다.

  2. RUST_BACKTRACE=1로 호출 경로를 연다

    디버그 심볼이 있는 빌드에서 백트레이스를 다시 얻습니다. 더 자세한 출력이 필요하면 환경과 비용을 확인한 뒤 RUST_BACKTRACE=full을 사용합니다.

  3. 첫 관련 사용자 코드 프레임과 직전 상태를 조사한다

    사용자 파일이 처음 보인다는 이유만으로 끝내지 않고, 패닉 메시지와 관련된 프레임의 입력·인덱스·상태 변경이 어떤 계약을 깨뜨렸는지 확인합니다.

정상 실패와 두 패닉 전략의 제어·정리 차이
경로제어 흐름Drop과 자원
Result::Err호출자에게 오류 값을 정상 반환합니다.정상 범위 종료와 소유권 규칙에 따라 Drop이 실행됩니다.
unwind현재 스레드의 스택 프레임을 거꾸로 빠져나옵니다.되감는 범위의 값에 Drop을 실행합니다. 패닉이 프로세스 전체를 끝내는지는 스레드 경계와 처리 방식에 달려 있습니다.
abort스택을 되감지 않고 프로세스를 중단합니다.스택에 남은 값의 Drop은 실행되지 않습니다. 운영 체제는 프로세스 자원을 회수하지만 애플리케이션 수준 정리를 대신하지는 않습니다.
Result::Err

오류 값을 정상 반환

호출자가 처리하며 정상 범위 종료와 소유권 규칙에 따라 Drop이 실행됩니다.

unwind

현재 스레드의 스택을 되감음

되감는 범위의 값에 Drop을 실행합니다. 전체 프로세스 결과는 스레드 경계와 처리 방식에 달려 있습니다.

abort

되감지 않고 프로세스 중단

스택 값의 Drop은 실행되지 않습니다. 운영 체제의 자원 회수는 애플리케이션 정리를 대신하지 않습니다.

v[index]

존재를 전제하는 인덱싱

범위를 벗어나면 패닉합니다. 이미 검증된 인덱스처럼 “반드시 존재한다”는 계약이 있을 때 쓰고, 패닉이 나면 그 전제가 깨진 경로를 고칩니다.

v.get(index)

부재를 Option<&T>으로 처리

외부 입력으로 인덱스가 달라질 수 있다면 Some과 None을 분기합니다. 정상적인 부재를 패닉으로 바꾸지 않고 API 타입에 드러냅니다.

패닉 출력의 정확한 문구, 위치 형식과 백트레이스 프레임은 러스트 버전, 대상 플랫폼, 빌드 프로필에 따라 달라집니다. 형태를 암기하기보다 메시지·위치 → 백트레이스 → 첫 관련 사용자 프레임 → 깨진 계약 순서를 유지합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
panic 발생

현재 스레드에서 실패가 시작한다.

  • 현재: panic
  • 전략: 사전 설정 unwind 또는 abort
  • 이 전략은 panic 뒤 사용자 입력으로 선택하는 분기가 아니다.
전략별 정리

두 독립 설정의 결과를 비교한다.

  • unwind: 벗어난 프레임 Drop 실행
  • abort: 스택 Drop 실행 안 함
  • OS 자원 회수와 Rust Drop 실행은 같은 작업이 아니다.
종료 경계

설정과 처리 경계의 결과를 남긴다.

  • unwind: 처리 경계에 따른 결과
  • abort: 프로세스 중단
  • panic을 항상 즉시 전체 프로세스 종료로 일반화하지 않는다.

Rust Result와 panic 판단, 백트레이스 및 종료 전략

현재 스레드의 panic 뒤 unwind는 내려온 프레임의 Drop을 실행하고 abort는 프로세스를 중단하며 스택 Drop을 실행하지 않는 자원 변화다. 전략은 실행 중 선택하는 사용자 분기가 아닌 사전 설정된 두 별도 사례로 둔다.

Rust에서 복구 가능한 실패는 Result로 반환하고 계약 위반이나 버그는 panic으로 드러내며, 메시지와 위치에서 백트레이스의 첫 관련 사용자 프레임을 찾아 unwind와 abort의 정리 차이를 판단하는 흐름

RESULT · PANIC · BACKTRACE · STRATEGY

실패가 정상 입력이면 값으로 돌려주고, 계약이 깨졌다면 패닉을 조사한다

panic!은 범용 예외 처리 문법이 아닙니다. 호출자가 복구할 수 있는 실패와 계속 실행할 수 없는 내부 계약 위반을 먼저 나누고, 패닉이 발생하면 메시지에서 호출 경로와 깨진 전제로 조사 범위를 좁힙니다.

패닉 발생 뒤 전략에 따라 달라지는 정리패닉은 현재 스레드에서 시작한다. unwind는 내려온 스택 프레임을 정리하며 Drop을 실행하고 처리 경계에 따라 최종 결과가 달라진다. abort는 프로세스를 중단하고 스택의 Drop을 실행하지 않는다. OS의 자원 회수와 Rust의 Drop은 별개다.unwindabortpanic 발생현재 스레드에서 시작선택된panic 전략unwind내려온 프레임의 Drop 실행처리 경계가 최종 결과 결정abort프로세스 중단스택 Drop 실행 안 함OS 자원 회수와 별개
패닉 발생 뒤 전략에 따라 달라지는 정리패닉은 현재 스레드에서 시작한다. unwind는 내려온 스택 프레임을 정리하며 Drop을 실행하고 처리 경계에 따라 최종 결과가 달라진다. abort는 프로세스를 중단하고 스택의 Drop을 실행하지 않는다. OS의 자원 회수와 Rust의 Drop은 별개다.unwindabortpanic 발생현재 스레드에서 시작선택된panic 전략unwind내려온 프레임 정리Drop 실행최종 결과는처리 경계에 따름abort프로세스 중단스택 Drop 실행 안 함OS 자원 회수와 별개

보조 설명 · 아래 원래 코드와 실패 조건도 함께 확인합니다.

recoverable · Result<T, E>

실패가 예상 가능한 입력·환경 상태다

파일 없음, 잘못된 사용자 입력, 네트워크 실패처럼 호출자가 재시도·대체·보고할 수 있으면 Result로 반환합니다. 실패는 정상 제어 흐름의 값이며 호출자가 정책을 선택합니다.

contract / bug · panic!

내부 불변식이나 반드시 참인 전제가 깨졌다

범위 밖 인덱싱이나 검증된 상태의 불가능한 분기처럼 정상 결과를 만들 수 없으면 패닉이 버그를 드러낼 수 있습니다. 현재 스레드가 패닉을 시작하며, 전체 프로세스의 결과는 스레드 경계와 패닉 전략에 달려 있습니다.

message → backtrace → user frame

출력 한 줄에서 깨진 계약까지 세 단계로 거슬러 간다

  1. 패닉 메시지와 보고 위치를 읽는다

    메시지는 실패한 검사, 위치는 패닉이 발생한 지점을 보여줍니다. 라이브러리 내부 위치라면 그 자체를 근본 원인으로 단정하지 않습니다.

  2. RUST_BACKTRACE=1로 호출 경로를 연다

    디버그 심볼이 있는 빌드에서 백트레이스를 다시 얻습니다. 더 자세한 출력이 필요하면 환경과 비용을 확인한 뒤 RUST_BACKTRACE=full을 사용합니다.

  3. 첫 관련 사용자 코드 프레임과 직전 상태를 조사한다

    사용자 파일이 처음 보인다는 이유만으로 끝내지 않고, 패닉 메시지와 관련된 프레임의 입력·인덱스·상태 변경이 어떤 계약을 깨뜨렸는지 확인합니다.

정상 실패와 두 패닉 전략의 제어·정리 차이
경로제어 흐름Drop과 자원
Result::Err호출자에게 오류 값을 정상 반환합니다.정상 범위 종료와 소유권 규칙에 따라 Drop이 실행됩니다.
unwind현재 스레드의 스택 프레임을 거꾸로 빠져나옵니다.되감는 범위의 값에 Drop을 실행합니다. 패닉이 프로세스 전체를 끝내는지는 스레드 경계와 처리 방식에 달려 있습니다.
abort스택을 되감지 않고 프로세스를 중단합니다.스택에 남은 값의 Drop은 실행되지 않습니다. 운영 체제는 프로세스 자원을 회수하지만 애플리케이션 수준 정리를 대신하지는 않습니다.
Result::Err

오류 값을 정상 반환

호출자가 처리하며 정상 범위 종료와 소유권 규칙에 따라 Drop이 실행됩니다.

unwind

현재 스레드의 스택을 되감음

되감는 범위의 값에 Drop을 실행합니다. 전체 프로세스 결과는 스레드 경계와 처리 방식에 달려 있습니다.

abort

되감지 않고 프로세스 중단

스택 값의 Drop은 실행되지 않습니다. 운영 체제의 자원 회수는 애플리케이션 정리를 대신하지 않습니다.

v[index]

존재를 전제하는 인덱싱

범위를 벗어나면 패닉합니다. 이미 검증된 인덱스처럼 “반드시 존재한다”는 계약이 있을 때 쓰고, 패닉이 나면 그 전제가 깨진 경로를 고칩니다.

v.get(index)

부재를 Option<&T>으로 처리

외부 입력으로 인덱스가 달라질 수 있다면 Some과 None을 분기합니다. 정상적인 부재를 패닉으로 바꾸지 않고 API 타입에 드러냅니다.

패닉 출력의 정확한 문구, 위치 형식과 백트레이스 프레임은 러스트 버전, 대상 플랫폼, 빌드 프로필에 따라 달라집니다. 형태를 암기하기보다 메시지·위치 → 백트레이스 → 첫 관련 사용자 프레임 → 깨진 계약 순서를 유지합니다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
예의 값과 변화
관찰·상황값·보충 정보변화·이유
panic 발생

현재 스레드에서 실패가 시작한다.

  • 현재: panic
  • 전략: 사전 설정 unwind 또는 abort
  • 이 전략은 panic 뒤 사용자 입력으로 선택하는 분기가 아니다.
전략별 정리

두 독립 설정의 결과를 비교한다.

  • unwind: 벗어난 프레임 Drop 실행
  • abort: 스택 Drop 실행 안 함
  • OS 자원 회수와 Rust Drop 실행은 같은 작업이 아니다.
종료 경계

설정과 처리 경계의 결과를 남긴다.

  • unwind: 처리 경계에 따른 결과
  • abort: 프로세스 중단
  • panic을 항상 즉시 전체 프로세스 종료로 일반화하지 않는다.

패닉은 현재 스레드에서 시작합니다. 기본 전략인 되감기(unwinding)는 스택을 거꾸로 거슬러 올라가며 함수마다 남은 값의 Drop을 실행해 정리합니다. 이 되감기는 일이 많으므로, 정리하지 않고 바로 끝내는 그만두기(aborting)를 고를 수도 있습니다. 그만두기를 고르면 스택을 되감지 않고 프로세스를 끝내므로, 스택에 남은 값의 Drop은 실행되지 않습니다. 프로세스가 끝나면 운영체제가 메모리를 거두어 가지만, Drop이 하던 파일 기록 마무리 같은 프로그램 수준의 정리는 대신 일어나지 않습니다.

Cargo.toml의 알맞은 [profile] 섹션에 panic = 'abort'를 더하면 되감기 대신 그만두기를 씁니다. 예를 들어 릴리스 빌드에서 패닉이 나면 그만두게 하려면 다음과 같이 적습니다. 이렇게 하면 실행 파일이 작아질 수 있지만, 늘 작아진다는 보장은 없습니다.

[profile.release]
panic = 'abort'

간단한 프로그램에서 panic!을 불러 봅니다. 아래 main은 다른 일 없이 곧바로 panic!에 메시지를 넘기므로, 프로그램은 시작하자마자 멈춥니다. 출력의 세 줄이 각각 무엇을 알려 주는지가 이 예제에서 볼 부분입니다.

src/main.rs
fn main() {
    panic!("crash and burn");
}
thread 'main' (6018279) panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic!을 부르면 이 메시지가 나옵니다. 첫 줄의 괄호 안 숫자는 스레드의 ID로 실행할 때마다 다르며, src/main.rs:2:5는 src/main.rs의 2번째 줄 5번째 글자에서 패닉이 났다는 뜻입니다. 둘째 줄은 panic!에 넘긴 메시지입니다. 이 예에서는 그 줄이 우리가 쓴 코드여서 panic! 호출을 바로 볼 수 있습니다. 하지만 우리 코드가 부른 다른 코드 안에서 panic!이 불릴 수도 있으며, 그때 오류 메시지의 파일 이름과 줄 번호는 다른 사람의 코드를 가리키고, 결국 패닉으로 이어진 것은 우리 코드의 어느 줄입니다. 그 줄을 찾으려면 panic!이 일어난 함수들의 백트레이스(backtrace)를 봅니다.

더 깊이: 되감기는 어디서 멈출까

되감기는 프로세스 전체가 아니라 패닉이 난 스레드의 스택을 거슬러 올라가며, Reference의 패닉 설명대로 회복할 수 있는 지점에 이르면 멈춥니다. 표준 라이브러리가 마련한 그런 지점은 두 가지입니다. 하나는 thread::spawn으로, 새로 만든 스레드에 패닉 회복을 미리 설정해 두므로 그 스레드가 패닉을 내도 다른 스레드는 계속 실행됩니다. 다른 하나는 같은 스레드 안에서 되감기를 멈추는 std::panic::catch_unwind입니다.

다만 catch_unwind 문서는 이 함수를 일반적인 try/catch로 쓰지 말고, 자주 실패할 수 있는 함수에는 Result를 쓰라고 권합니다. 이 함수는 되감는 패닉만 잡으므로, panic = 'abort'로 빌드해 프로세스를 끝내는 패닉은 잡지 못합니다. 또 사용자 정의 패닉 훅(panic hook)이 있다면, 패닉이 잡히기 전, 되감기를 시작하기 전에 그 훅이 먼저 실행됩니다.

다른 언어와 맞닿은 곳에서는 규칙이 더 엄격합니다. Rust 1.81 발표에 따르면, 이 버전부터 extern "C"처럼 되감기를 허용하지 않는 ABI의 함수에서 패닉이 바깥으로 나가려 하면 되감기를 이어 가지 않고 프로세스를 끝냅니다. 이것으로 오래된 건전성(soundness) 문제가 해결되었고, 다른 언어의 함수를 거쳐서도 되감기가 이어져야 하는 코드는 Rust 1.71에 들어온 "C-unwind" 같은 ABI를 씁니다.

panic! 백트레이스 이용하기

panic!을 직접 부르지 않고 우리 코드의 버그 때문에 라이브러리 안에서 패닉이 나는 경우를 봅니다. 다음 코드는 벡터의 범위를 벗어난 인덱스로 원소에 접근하려 합니다.

src/main.rs
fn main() {
    let v = vec![1, 2, 3];

    v[99];
}

인덱스는 0부터 시작하므로 v[99]는 100번째 원소를 요청하지만, 이 벡터에는 원소가 세 개뿐입니다. []는 원소를 돌려주기로 되어 있는데, 올바르지 않은 인덱스에는 돌려줄 원소가 없으므로 러스트는 패닉을 냅니다. C에서 배열의 끝을 넘는 접근은 정의되지 않은 동작(undefined behavior)이어서, 그 자리에 있는 아무 값이나 읽을 수 있습니다. 이를 버퍼 초과 읽기(buffer overread)라고 하며, 공격자가 인덱스를 조작해 배열 뒤에 저장된 읽으면 안 되는 데이터를 읽어 내는 보안 취약점이 됩니다.

러스트는 이런 취약점을 막으려고, 없는 인덱스의 원소를 읽으려 하면 실행을 멈추고 더 나아가지 않습니다. 실행해 보면 다음과 같습니다.

thread 'main' (6017887) panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

이 오류는 우리 코드인 main.rs의 4번째 줄, 인덱스 99로 접근하려 한 곳을 가리킵니다. 그다음 줄은 RUST_BACKTRACE 환경 변수를 주면 오류로 이어진 경로를 볼 수 있다고 알려 줍니다. 백트레이스는 그 지점에 오기까지 불린 모든 함수의 목록이며, 다른 언어의 백트레이스와 같은 방식으로 읽습니다. 맨 위에서부터 읽어 내려가다가 우리가 쓴 파일이 처음 나오는 곳을 찾으면, 그곳이 문제가 시작된 지점입니다. 그보다 위의 줄은 우리 코드가 부른 코드이고, 아래의 줄은 우리 코드를 부른 코드이며, 그 앞뒤에는 러스트 핵심 코드, 표준 라이브러리, 우리가 쓰는 크레이트가 섞여 있을 수 있습니다.

RUST_BACKTRACE를 0이 아닌 값으로 주고 실행해 백트레이스를 봅니다. 다음은 그 출력의 주요 부분입니다.

RUST_BACKTRACE=1 cargo run
thread 'main' (6017887) panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: core::panicking::panic_bounds_check
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.

실제 출력에는 프레임마다 표준 라이브러리 소스의 경로가 함께 나오며, 그 경로와 프레임의 이름, 번호는 운영체제와 러스트 버전에 따라 다릅니다. Windows의 PowerShell에서는 $env:RUST_BACKTRACE=1; cargo run처럼 환경 변수를 줍니다. 이런 정보가 담긴 백트레이스를 보려면 디버그 심벌이 있어야 하는데, --release 없이 cargo build나 cargo run을 쓰면 기본으로 들어갑니다.

위의 출력에서 6번 줄이 우리 프로젝트 src/main.rs의 4번째 줄을 가리킵니다. 패닉이 나지 않게 하려면 우리가 쓴 파일이 처음 나오는 이 줄부터 살펴봅니다. 이 예처럼 일부러 패닉을 내는 코드라면, 범위를 벗어난 인덱스로 원소를 요청하지 않는 것이 고치는 방법입니다. 앞으로 패닉이 나면 어떤 값으로 무슨 일을 하다가 패닉이 났는지, 그리고 원래 무엇을 해야 했는지 찾아야 합니다. 다만 우리 파일이 처음 나오는 줄이 언제나 근본 원인인 것은 아니므로, 그 줄에 들어온 값과 그 바로 앞의 상태 변화를 함께 확인합니다.

벡터의 범위 밖 인덱스가 정상적으로 들어올 수 있는 값이라면, 패닉 대신 get으로 받아 처리합니다. 언제 panic!을 쓰고 언제 쓰지 말아야 하는지는 panic! 사용 기준에서 다룹니다.

src/main.rs
fn main() {
    let v = vec![1, 2, 3];

    match v.get(99) {
        Some(value) => println!("{value}"),
        None => println!("no element at that index"),
    }
}
no element at that index

get(99)는 원소가 없으므로 None을 돌려주고, 프로그램은 메시지를 출력한 뒤 정상적으로 끝납니다.

정리

  • 러스트는 오류를 Result<T, E>로 다루는 복구 가능한 오류와 panic!으로 실행을 멈추는 복구 불가능한 오류로 나누며, 예외는 없습니다.
  • 패닉은 panic!을 직접 부르거나 범위 밖 인덱스처럼 패닉을 일으키는 동작을 할 때 일어납니다.
  • 기본 전략은 스택을 되감으며 Drop으로 정리하는 것이고, panic = 'abort'로 바꾸면 정리 없이 바로 끝냅니다.
  • 패닉 메시지는 스레드의 ID와 패닉이 난 파일, 줄, 열, 그리고 메시지를 보여 줍니다.
  • RUST_BACKTRACE=1로 실행하면 백트레이스를 볼 수 있으며, 우리가 쓴 파일이 처음 나오는 줄부터 살펴봅니다.

연습 문제

1. 마지막 인덱스

벡터를 vec![10, 20]으로 바꾸었습니다. v.get(1)과 v.get(2)는 각각 무엇을 돌려주나요? v.get(2) 대신 v[2]를 쓰고 바로 다음 줄에 println!을 두면 그 줄이 실행되나요?

해설 보기

v.get(1)은 값 20의 참조를 담은 Some(&20)을, v.get(2)는 마지막 인덱스 1을 넘으므로 None을 돌려줍니다. v[2]는 범위를 벗어나 그 줄에서 패닉이 나므로, 다음 줄의 println!은 실행되지 않습니다. 기본 전략이라면 스택을 되감으며 남은 값을 정리하고, panic = 'abort'라면 정리 없이 바로 끝납니다.

2. 패닉 메시지 읽기

어떤 프로그램이 thread 'main' (12345) panicked at src/report.rs:18:21:과 attempt to divide by zero를 출력했습니다. 무엇을 알 수 있고, 다음에 무엇을 해야 하나요?

해설 보기

main 스레드에서 src/report.rs의 18번째 줄 21번째 글자의 나눗셈이 0으로 나누려 해서 패닉이 났다는 것을 알 수 있으며, 괄호 안의 숫자는 스레드의 ID입니다. 그 줄의 나누는 수가 왜 0이 되었는지 그 값이 만들어진 곳을 따라가 보고, 필요하면 RUST_BACKTRACE=1로 다시 실행해 그 함수가 어떤 경로로 불렸는지 확인합니다. 0이 정상적으로 들어올 수 있는 값이라면 나누기 전에 확인하거나 checked_div로 처리합니다.

3. 정리가 필요한 프로그램

패닉이 나도 열어 둔 임시 파일을 지우는 Drop 구현이 꼭 실행되어야 하는 프로그램이라면, panic = 'abort'를 써도 될까요?

해설 보기

쓰지 않는 편이 맞습니다. 그만두기 전략은 스택을 되감지 않으므로 스택에 남은 값의 Drop이 실행되지 않아 임시 파일이 남습니다. 되감기 전략에서는 패닉이 나도 범위를 벗어나는 값의 Drop이 실행됩니다. 다만 프로세스가 강제로 종료되는 경우처럼 Drop이 실행되지 않는 상황도 있으므로, 꼭 필요한 정리는 그런 경우까지 생각해 설계합니다.

다음 글 Result와 복구 가능한 오류에서는 실패할 수 있는 연산이 Result를 돌려주게 하고, 그 오류를 처리하거나 부른 쪽으로 넘기는 방법을 살펴봅니다.