Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

panic! 으로 복구 불가능한 에러 다루기

가끔은 코드 안에서 좋지 않은 일이 벌어지고, 그것에 대해 할 수 있는 일이 전혀 없을 때가 있습니다. 이런 경우 러스트는 panic! 매크로를 제공합니다. 실제로 패닉을 일으키는 방법은 두 가지입니다. 코드가 패닉을 일으키는 동작을 하게 만드는 것(예를 들어 배열 끝을 넘어서 접근하기), 혹은 panic! 매크로를 직접 호출하는 것입니다. 두 경우 모두 프로그램에서 패닉이 발생합니다. 기본적으로 이런 패닉은 실패 메시지를 출력하고, 스택을 언와인드하며 정리한 뒤 종료합니다. 또한 환경 변수를 통해 패닉이 발생했을 때 호출 스택까지 함께 표시하도록 할 수 있어, 패닉 원인을 추적하기가 더 쉬워집니다.

패닉 발생 시 스택 언와인드와 중단(abort)

기본적으로 패닉이 발생하면 프로그램은 언와인드(unwinding) 를 시작합니다. 즉, 러스트가 스택을 거슬러 올라가며 만나는 각 함수의 데이터를 정리한다는 뜻입니다. 하지만 이렇게 되돌아가며 정리하는 작업에는 비용이 많이 듭니다. 그래서 러스트는 즉시 중단(aborting) 하는 대안도 제공합니다. 중단은 아무 정리도 하지 않고 프로그램을 곧바로 끝냅니다.

그때 프로그램이 사용하던 메모리는 운영체제가 정리하게 됩니다. 만약 프로젝트에서 최종 바이너리 크기를 최대한 줄여야 한다면, Cargo.toml 의 적절한 [profile] 섹션에 panic = 'abort' 를 추가해 패닉 시 언와인드 대신 중단하도록 바꿀 수 있습니다. 예를 들어 릴리스 모드에서 패닉 시 중단하고 싶다면 다음을 추가합니다.

[profile.release]
panic = 'abort'

이제 간단한 프로그램에서 panic! 을 직접 호출해 봅시다.

Filename: src/main.rs
fn main() {
    panic!("crash and burn");
}

프로그램을 실행하면 다음과 비슷한 출력이 보일 것입니다.

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
     Running `target/debug/panic`

thread 'main' panicked at src/main.rs:2:5:
crash and burn
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

panic! 호출은 마지막 두 줄에 들어 있는 에러 메시지를 발생시킵니다. 첫 번째 줄은 우리가 지정한 패닉 메시지와 패닉이 발생한 소스 코드 위치를 보여 줍니다. src/main.rs:2:5src/main.rs 파일의 두 번째 줄, 다섯 번째 문자라는 뜻입니다.

이 경우 표시된 줄은 우리 코드의 일부이며, 그 줄로 가 보면 실제 panic! 호출이 있다는 것을 볼 수 있습니다. 하지만 다른 경우에는 panic! 호출이 우리 코드가 직접 호출한 다른 코드 안에 있을 수도 있습니다. 그러면 오류 메시지가 가리키는 파일명과 줄 번호는, 우리 코드가 아니라 panic! 매크로가 실제로 호출된 그쪽 코드 위치가 될 것입니다.

panic! 호출이 어디서 왔는지에 대한 함수 백트레이스를 사용하면, 문제를 일으키는 우리 코드 부분을 알아낼 수 있습니다. panic! 백트레이스를 어떻게 읽는지 이해하기 위해, 이번에는 우리가 직접 매크로를 호출하는 대신 우리 코드의 버그로 인해 라이브러리 안에서 panic! 이 호출되는 예를 보겠습니다. 목록 9-1은 벡터에서 유효한 범위를 벗어난 인덱스에 접근하려는 코드입니다.

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

    v[99];
}
Listing 9-1: 벡터 끝을 넘는 요소에 접근하려고 하여 panic! 호출을 일으키는 코드

여기서는 벡터의 100번째 요소에 접근하려고 합니다(인덱스는 0부터 시작하므로 실제 인덱스 값은 99입니다). 하지만 이 벡터에는 요소가 세 개뿐입니다. 이런 상황에서 러스트는 패닉을 일으킵니다. [] 는 어떤 요소를 반환해야 하는 문법인데, 유효하지 않은 인덱스를 넘기면 러스트가 돌려줄 수 있는 올바른 요소가 존재하지 않기 때문입니다.

C에서는 자료구조 끝을 넘어 읽는 것이 정의되지 않은 동작입니다. 때에 따라서는 그 자료구조에 속하지 않는 메모리이더라도, 마치 그 요소 자리에 해당하는 위치의 값을 그냥 읽어 올 수도 있습니다. 이를 버퍼 오버리드(buffer overread) 라고 하며, 공격자가 인덱스를 조작해 읽어서는 안 되는 데이터를 읽을 수 있게 되면 보안 취약점으로 이어질 수 있습니다.

러스트는 이런 취약점으로부터 프로그램을 보호하기 위해, 존재하지 않는 인덱스의 요소를 읽으려 하면 실행을 중단하고 더 이상 진행하지 않습니다. 실제로 실행해 봅시다.

$ cargo run
   Compiling panic v0.1.0 (file:///projects/panic)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/panic`

thread 'main' 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

이 에러는 우리가 벡터 v 의 인덱스 99에 접근하려는 main.rs 4번째 줄을 가리킵니다.

note: 줄은 에러를 일으킨 정확한 경로를 백트레이스로 보고 싶다면 RUST_BACKTRACE 환경 변수를 설정할 수 있다고 알려 줍니다. 백트레이스(backtrace) 는 이 지점에 도달하기까지 호출된 모든 함수의 목록입니다. 러스트의 백트레이스도 다른 언어와 마찬가지로 읽습니다. 맨 위에서부터 내려가다가 여러분이 작성한 파일이 나오는 지점을 찾으면 됩니다. 그 지점이 문제의 출발점입니다. 그 위의 줄들은 여러분 코드가 호출한 코드들이고, 그 아래 줄들은 여러분 코드를 호출한 코드입니다. 여기에는 핵심 러스트 코드, 표준 라이브러리 코드, 또는 사용하는 외부 크레이트 코드가 포함될 수도 있습니다. RUST_BACKTRACE 환경 변수를 0 이 아닌 값으로 설정해 백트레이스를 확인해 봅시다. 목록 9-2는 여러분이 보게 될 출력과 비슷한 예를 보여 줍니다.

$ RUST_BACKTRACE=1 cargo run
thread 'main' 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
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/std/src/panicking.rs:692:5
   1: core::panicking::panic_fmt
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:75:14
   2: core::panicking::panic_bounds_check
             at /rustc/4d91de4e48198da2e33413efdcd9cd2cc0c46688/library/core/src/panicking.rs:273:5
   3: <usize as core::slice::index::SliceIndex<[T]>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:274:10
   4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/slice/index.rs:16:9
   5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/alloc/src/vec/mod.rs:3361:9
   6: panic::main
             at ./src/main.rs:4:6
   7: core::ops::function::FnOnce::call_once
             at file:///home/.rustup/toolchains/1.85/lib/rustlib/src/rust/library/core/src/ops/function.rs:250:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
Listing 9-2: RUST_BACKTRACE 환경 변수를 설정했을 때 panic! 호출이 생성하는 백트레이스

출력이 꽤 길지요! 실제로 보게 되는 정확한 출력은 운영체제와 러스트 버전에 따라 다를 수 있습니다. 이 정도 정보가 담긴 백트레이스를 얻으려면 디버그 심벌이 켜져 있어야 합니다. 우리가 지금처럼 cargo buildcargo run--release 없이 실행할 때는 디버그 심벌이 기본적으로 활성화됩니다.

목록 9-2 출력에서 백트레이스의 6번째 줄이 문제를 일으키는 우리 프로젝트 코드, 즉 src/main.rs 4번째 줄을 가리킵니다. 프로그램이 패닉하지 않게 하고 싶다면, 우리가 작성한 파일을 처음 언급하는 줄의 위치부터 조사를 시작하면 됩니다. 목록 9-1처럼 의도적으로 패닉을 일으키는 코드를 쓴 경우, 해결책은 벡터 인덱스 범위를 벗어난 요소를 요청하지 않는 것입니다. 앞으로 코드가 패닉을 일으킬 때는, 어떤 연산이 어떤 값과 함께 실행되어 패닉이 발생했는지, 그리고 그 대신 코드가 무엇을 해야 하는지를 판단해야 합니다.

이 장의 뒤쪽 [“panic! 을 써야 할 때와 쓰지 말아야 할 때”] to-panic-or-not-to-panic 절에서, 언제 panic! 을 사용해야 하고 언제 사용하지 말아야 하는지 다시 돌아와 이야기할 것입니다. 이제는 Result 로 에러에서 어떻게 복구하는지 살펴봅시다.