panic!으로 복구 불가능한 에러 처리하기
panic!으로 복구 불가능한 에러 처리하기
가끔은 코드에 나쁜 일이 일어나는데 어쩔 수가 없을 때가 있어요. 그럴 때 Rust에는 panic! 매크로가 있어요. 실제로 패닉을 일으키는 방법은 두 가지예요. 배열 끝을 넘어선 인덱스에 접근하는 것처럼 코드가 패닉을 일으키게 하는 행동을 하는 경우, 그리고 panic! 매크로를 명시적으로 직접 호출하는 경우죠. 두 경우 모두 우리 프로그램에 패닉이 발생해요. 기본적으로 이런 패닉은 실패 메시지를 출력하고, 언와인드(unwind)하며 스택을 정리한 다음 프로그램을 종료해요. 환경 변수를 통하면 패닉이 발생했을 때 호출 스택(call stack)을 표시하게 만들어 패닉의 원인을 추적하기 쉽게 할 수도 있어요.
출처: The Rust Book
패닉에 대한 반응: 스택 언와인딩 vs 중단
기본적으로 패닉이 발생하면 프로그램은 **언와인딩(unwinding)**을 시작해요. Rust가 스택을 거슬러 올라가며 만나는 각 함수의 데이터를 정리해 나가는 방식이죠. 하지만 거슬러 올라가며 정리하는 건 꽤 많은 작업이에요. 그래서 Rust는 정리 없이 프로그램을 끝내는 **중단(aborting)**이라는 대안을 선택할 수 있게 해줘요.
프로그램이 쓰던 메모리는 그러면 운영 체제가 정리해야 해요. 프로젝트에서 결과 바이너리를 최대한 작게 만들어야 한다면, Cargo.toml 파일의 해당 [profile] 섹션에 panic = 'abort'를 추가해서 패닉 시 언와인딩 대신 중단으로 바꿀 수 있어요. 예를 들어 출시(release) 모드에서 패닉 시 중단하고 싶다면 이렇게 추가하면 되죠.
[profile.release]
panic = 'abort'
간단한 프로그램에서 panic!을 호출해 볼게요.
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:5는 src/main.rs 파일의 두 번째 줄, 다섯 번째 문자라는 뜻이죠.
이 경우 표시된 줄은 우리 코드의 일부이고, 그 줄로 가면 panic! 매크로 호출이 보여요. 다른 경우에는 panic! 호출이 우리 코드가 호출하는 코드 안에 있을 수 있어요. 그러면 에러 메시지가 알려주는 파일 이름과 줄 번호는 결국 패닉을 부른 우리 코드의 줄이 아니라, panic! 매크로가 호출된 다른 사람의 코드를 가리키죠.
panic! 호출이 나온 함수들의 백트레이스(backtrace)를 사용해 문제를 일으키는 우리 코드 부분을 찾아낼 수 있어요. panic! 백트레이스를 어떻게 쓰는지 이해하려면, 패닉이 우리 코드가 매크로를 직접 호출해서가 아니라 우리 코드의 버그 때문에 라이브러리에서 발생한 경우가 어떤 모습인지 다른 예제를 통해 봐볼게요. 리스팅 9-1은 벡터에서 유효한 인덱스 범위를 넘어선 인덱스에 접근하려는 코드예요.
fn main() {
let v = vec![1, 2, 3];
v[99];
}
여기서 우리는 벡터의 100번째 요소(인덱싱이 0부터 시작하므로 인덱스 99)에 접근하려고 하는데, 벡터에는 요소가 세 개밖에 없어요. 이런 상황에서 Rust는 패닉을 일으켜요. []는 요소를 반환하기 위해 쓰이지만, 잘못된 인덱스를 넘기면 Rust가 돌려줄 올바른 요소가 없거든요.
C 언어에서는 데이터 구조의 끝을 넘어 읽는 것이 정의되지 않은 동작(undefined behavior)이에요. 데이터 구조에 해당하지 않는 메모리라도, 그 구조의 해당 요소에 대응하는 메모리 위치에 있는 값을 얻게 될 수 있죠. 이걸 **버퍼 오버리드(buffer overread)**라고 하는데, 공격자가 인덱스를 조작해 데이터 구조 뒤에 저장된, 읽으면 안 되는 데이터를 읽게 만들 수 있다면 보안 취약점으로 이어질 수 있어요.
이런 종류의 취약점에서 프로그램을 보호하기 위해, Rust는 존재하지 않는 인덱스의 요소를 읽으려 하면 실행을 멈추고 더 진행하지 않아요. 직접 시도해 볼게요.
$ 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
이 에러는 main.rs의 4번째 줄, 즉 v의 벡터에서 인덱스 99에 접근하려는 지점을 가리켜요.
note: 줄은 RUST_BACKTRACE 환경 변수를 설정하면 정확히 무엇 때문에 에러가 발생했는지에 대한 백트레이스를 얻을 수 있다고 알려줘요. 백트레이스는 이 지점까지 도달하기 위해 호출된 모든 함수의 목록이에요. Rust의 백트레이스는 다른 언어와 똑같이 동작해요. 읽는 핵심은 맨 위에서부터 시작해 우리가 직접 작성한 파일이 보일 때까지 읽는 거예요. 그 위치가 바로 문제가 시작된 지점이죠. 그 지점 위에 있는 줄들은 우리 코드가 호출한 코드이고, 아래에 있는 줄들은 우리 코드를 호출한 코드예요. 이 앞뒤의 줄들에는 Rust 핵심 코드, 표준 라이브러리 코드, 또는 우리가 쓰는 크레이트가 포함될 수 있어요. 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.
출력이 꽤 많죠. 실제로 보이는 출력은 운영 체제와 Rust 버전에 따라 다를 수 있어요. 이 정보로 백트레이스를 얻으려면 디버그 심볼(debug symbol)이 활성화되어 있어야 해요. --release 플래그 없이 cargo build나 cargo run을 쓰면 여기처럼 디버그 심볼은 기본적으로 활성화돼요.
리스팅 9-2의 출력에서 백트레이스 6번 줄은 문제를 일으키는 우리 프로젝트의 줄, 즉 src/main.rs의 4번째 줄을 가리켜요. 프로그램이 패닉에 빠지지 않길 원한다면, 우리가 작성한 파일을 언급하는 첫 번째 줄이 가리키는 위치부터 조사를 시작해야 해요. 리스팅 9-1에서는 의도적으로 패닉이 일어날 코드를 작성했으니, 벡터 인덱스 범위를 넘어서는 요소를 요청하지 않는 방식으로 패닉을 고치면 돼요. 앞으로 우리 코드가 패닉을 일으킨다면, 어떤 값으로 어떤 행동을 취하다가 패닉이 발생했는지, 코드가 대신 무엇을 해야 하는지를 알아내야 해요.
panic!과, 에러 상황을 처리할 때 panic!을 써야 하는 경우와 쓰지 말아야 하는 경우는 이 장 뒷부분의 "To panic! or Not to panic!" 절에서 다시 다룰게요. 다음으로는 Result를 써서 에러에서 복구하는 방법을 살펴볼 거예요.