panic!
panic!
매크로가 실행되면 프로그램은 실패 메세지를 출력하고, 스택을 되감고 청소한 후 종료됩니다. 흔히 어떤 종류의 버그가 발견되었고 프로그래머가 해당 에러를 어떻게 처리할 지 명확하지 않을때 발생합니다.
단순한 프로그램 내에서 panic!
호출을 시도해볼 수 있습니다.
fn main() {
panic!("crash and burn");
}
출력:
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.25 secs
Running `target/debug/panic`
thread 'main' panicked at 'crash and burn', src/main.rs:2
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
panic!
호출이 마지막 세 줄의 에러 메세지를 야기합니다. 패닉이 발생한 지점을 보여주며, 문제를 일으킨 코드 부분을 발견하기위해 panic!
호출이 발생된 함수에 대한 backtrace를 사용할 수 있습니다.
panic!
backtrace 사용직접 매크로 호출이 아닌 코드의 버그에 의해 panic!
호출이 라이브러리로부터 발생되는 경우를 봅시다. 아래의 코드는 벡터의 끝을 넘어선 요소에 대한 접근을 시도하는 코드입니다.
fn main() {
let v = vec![1, 2, 3];
v[99];
}
이러한 상황에서 C와 같은 다른 언어들은 원하는 것이 아닐지라도 요청한 것을 정확히 주려고 시도합니다. 설령 그 메모리 영역이 벡터 소유가 아닐지라도 벡터 내에 해당 요소와 상응하는 위치의 메모리에 들어 있는 무언가를 얻을 것입니다. 이를 buffer overread라고 부르며 보안 취약점을 발생시킬 수 있습니다.
Rust는 이러한 종류의 취약점으로부터 보호하기 위해 실행을 멈추고 계속하기를 거부합니다.
$ cargo run
Compiling panic v0.1.0 (file:///projects/panic)
Finished dev [unoptimized + debuginfo] target(s) in 0.27 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is
100', /stable-dist-rustc/build/src/libcollections/vec.rs:1362
note: Run with `RUST_BACKTRACE=1` for a backtrace.
error: Process didn't exit successfully: `target/debug/panic` (exit code: 101)
위 에러는 직접 작성한 파일이 아닌 libcollections/vec.rs를 가리키고 있습니다. 이는 표준 라이브러리 내에 있는 Vec<T>
의 구현 부분입니다. 벡터 v
에 []
를 사용할 때 실행되는 코드는 libcollections/vec.rs 안에 있으며, 그 곳이 바로 panic!
이 실제 발생한 곳입니다.
RUST_BACKTRACE
환경 변수를 설정해 에러의 원인이 된 것이 무엇인지 정확하게 backtrace할 수 있습니다. backtrace란 어떤 지점에 도달하기까지 호출해온 모든 함수의 리스트를 말합니다.
$ RUST_BACKTRACE=1 cargo run
Finished dev [unoptimized + debuginfo] target(s) in 0.0 secs
Running `target/debug/panic`
thread 'main' panicked at 'index out of bounds: the len is 3 but the index is 100', /stable-dist-rustc/build/src/libcollections/vec.rs:1392
stack backtrace:
1: 0x560ed90ec04c - std::sys::imp::backtrace::tracing::imp::write::hf33ae72d0baa11ed
at /stable-dist-rustc/build/src/libstd/sys/unix/backtrace/tracing/gcc_s.rs:42
.
.
.
13: 0x560ed90ee926 - std::rt::lang_start::hd7c880a37a646e81
at /stable-dist-rustc/build/src/libstd/panicking.rs:436
at /stable-dist-rustc/build/src/libstd/panic.rs:361
at /stable-dist-rustc/build/src/libstd/rt.rs:57
14: 0x560ed90e7302 - main
15: 0x7f0d53f16400 - __libc_start_main
16: 0x560ed90e6659 - _start
17: 0x0 - <unknown>
Result
enum Result<T, E> {
Ok(T),
Err(E),
}
T
와 E
는 제네릭 타입 파라미터입니다. T
는 성공한 경우에 OK
variant 내에 반환될 값의 타입을 나타내며 E
는 실패한 경우에 Err
variant 내에 반환될 에러의 타입을 나타냅니다. Result
가 이러한 제네릭 타입 파라미터를 갖기 때문에, 반환하고자 하는 성공적인 값과 에러 값이 다를 수 있는 다양한 상황 내에서 표준 라이브러리에 정의된 Result
타입과 함수들을 사용할 수 있습니다.
use std::fs::File;
fn main() {
let f: u32 = File::open("hello.txt");
}
위 코드를 컴파일 시도했을 때 다음 메세지가 출력됩니다.
error[E0308]: mismatched types
--> src/main.rs:4:18
|
4 | let f: u32 = File::open("hello.txt");
| ^^^^^^^^^^^^^^^^^^^^^^^ expected u32, found enum
`std::result::Result`
|
= note: expected type `u32`
= note: found type `std::result::Result<std::fs::File, std::io::Error>`
위 메세지는 File::open
함수의 반환 타입이 Result<T, E>
라는 것을 알려줍니다. 여기서 제네릭 파라미터 T
는 성공값의 타입인 std::fs::File
로 채워져 있는데, 이는 파일 핸들입니다. 에러에 사용되는 E
의 타입은 std::io::Error
입니다.
해당 반환 타입은 File::open
을 호출하는 것이 성공하여 우리가 읽거나 쓸 수 있는 파일 핸들을 반환해줄 수도 있고 파일이 존재하지 않거나 접근 권한이 없어 실패할 수도 있다는 뜻입니다. 이러한 정보를 Result
열거형으로 전달합니다.
위 코드를 수정하고 반환 값에 따라 다른 행동을 취할 수 있도록 코드를 추가해봅시다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(error) => {
panic!("There was a problem opening the file: {:?}", error)
},
};
}
Ok
일 때에는 내부의 file
값을 반환하고, Err
의 경우 panic!
매크로를 호출하는 방법을 택했습니다.
위 코드는 어떤 에러이든 간에 panic!
매크로를 호출하였지만, 실패 이유에 따라 다른 행동을 취하도록 할 수 있습니다. 파일이 없는 경우라면 새로운 파일을 만들어 반환하고, 권한이 없는 등의 그 밖의 이유는 panic!
을 일으키도록 코드를 수정해봅시다.
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file,
Err(ref error) if error.kind() == ErrorKind::NotFound => {
match File::create("hello.txt") {
Ok(fc) => fc,
Err(e) => {
panic!(
"Tried to create file but there was a problem: {:?}",
e
)
},
}
},
Err(error) => {
panic!(
"There was a problem opening the file: {:?}",
error
)
},
};
}
Err
variant 내에 있는 File::open
이 반환하는 값의 타입은 io::Error
이며 이는 표준 라이브러리에서 제공하는 구조체입니다. 이 구조체가 제공하는 kind
메소드를 호출하여 io::ErrorKind
값을 얻을 수 있습니다. 우리가 사용하고자 하는 variant는 파일이 존재하지 않는 경우인 ErrorKind::NotFound
입니다. 파일 생성을 시도할 때에도 실패할 수 있기 때문에 match
구문을 추가해줍니다.
unwrap
, expect
match
는 충분히 잘 동작하지만, 살짝 장황하기도 하고 의도를 항상 잘 전달하는 것은 아닙니다. Result<T, E>
타입은 다양한 작업을 하기 위해 정의된 수많은 헬퍼 메소드를 가지고 있습니다. 그 중 unwrap
메소드는 위 코드의 match
구문과 비슷한 구현을 한 숏컷 메소드입니다. Ok
이라면 Ok
내의 값을 반환하고, Err
라면 panic!
매크로를 호출합니다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt").unwrap();
}
또 다른 메소드인 expect
는 unwrap
과 유사한데, panic!
에러 메세지를 선택할 수 있게 해줍니다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt").expect("Failed to open hello.txt");
}
에러 발생 가능성이 있는 무언가를 호출하는 함수를 작성할 때, 함수 내에서 에러를 처리하는 대신 에러를 호출하는 코드 쪽으로 반환하여 에러 전파 를 할 수 있습니다. 코드 내용 내에서 이용 가능한 것들보다 더 많은 정보와 로직을 가지고 있을 수도 있는 호출하는 코드 쪽에 더 많은 제어권을 줍니다.
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let f = File::open("hello.txt");
let mut f = match f {
Ok(file) => file,
Err(e) => return Err(e),
};
let mut s = String::new();
match f.read_to_string(&mut s) {
Ok(_) => Ok(s),
Err(e) => Err(e),
}
}
위 코드에서는 함수의 반환 타입이 Result<String, io::Error>
입니다. 만일 이 함수가 어떤 문제 없이 성공하면 함수를 호출한 코드는 String
을 담은 값(함수가 파일로부터 읽어들인 사용자 이름)을 반환할 것입니다. 만일 문제가 발생한다면 문제에 대한 더 많은 정보를 담고있는 io::Error
의 인스턴스를 담은 Err
값을 반환할 것입니다.
이처럼 에러를 전파하는 패턴은 매우 흔하기 때문에 Rust에서는 이를 더 쉽게 해주는 물음표 연산자 ?
를 제공합니다.
?
위의 코드를 물음표 연산자를 이용해 간결하게 바꾸어 봅시다.
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut f = File::open("hello.txt")?;
let mut s = String::new();
f.read_to_string(&mut s)?;
Ok(s)
}
Result
값 뒤의 ?
는 위에서 정의했던 match
표현식과 거의 같은 방식으로 동작하게끔 정의되어 있습니다. 만일 Result
의 값이 Ok
라면, Ok
내의 값이 이 표현식으로부터 얻어지고 프로그램이 계속됩니다. 만일 값이 Err
라면, return
키워드를 사용하여 에러 값을 호출하는 코드에게 전파하는 것과 같이 전체 함수로부터 Err
내의 값이 반환될 것입니다.
match
표현식과의 한 가지 차이점은 에러 값들이 표준 라이브러리 내에 있는 From
트레잇에 정의된 from
함수를 친다는 것입니다. from
함수의 호출이 ?
가 얻게 되는 에러 타입을 ?
가 사용되고 있는 현재 함수의 반환 타입에 정의된 에러 타입으로 변환합니다.
?
는 많은 수의 boilerplate를 제거해주고 함수의 구현을 더 단순하게 만들어 줍니다. 아래와 같이 ?
뒤에 바로 메소드 호출을 연결하는 식(chaning)으로 코드를 더 줄일 수도 있습니다.
use std::io;
use std::io::Read;
use std::fs::File;
fn read_username_from_file() -> Result<String, io::Error> {
let mut s = String::new();
File::open("hello.txt")?.read_to_string(&mut s)?;
Ok(s)
}
또한, ?
는 match
표현식과 동일한 방식으로 동작하도록 정의되어 있기 때문에 Result
타입을 반환하는 함수에서만 사용 가능합니다.
panic!
과 Result
사용 결정하기panic!
예제 코드 내에서는 panic!
을 일으킬 수 있는 unwrap
같은 메소드를 호출하는 것이 어플리케이션이 에러를 처리하고자 하는 방법에 대한 플레이스홀더로서의 의미를 갖습니다.
비슷한 상황에서 에러를 어떻게 처리할지 결정하기 전에, unwrap
과 expect
메소드가 프로토타이핑을 할 때 매우 편리합니다.
만일 테스트 내에서 메소드 호출이 실패한다면, 해당 메소드가 테스트 중인 기능이 아니더라도 전체 테스트를 실패시키도록 합니다. panic!
은 테스트가 어떻게 실패하는지 표시해주기 때문에 unwrap
이나 expect
를 사용합니다.
Result
use std::net::IpAddr;
let home = "127.0.0.1".parse::<IpAddr>().unwrap();
위 코드에서는 하드코딩된 String을 파싱하여 IpAddr
인스턴스를 만듭니다. 127.0.0.1
이 유효한 주소임을 볼 수 있으므로 unwrap
사용이 허용됩니다. 하지만 이 사실이 parse
메소드의 반환 타입을 변경해주지는 않습니다. 여전히 Result
값을 갖게 되고, 이는 String이 항상 유효한 IP 주소라는 것을 알 수 있을 만큼 컴파일러가 똑똑하진 않기 때문입니다.
어떤 가정, 보장, 계약, 불변성이 깨질 가능성이 있는 때에는 panic!
을 사용하는 것이 바람직합니다. 예를 들면 유효하지 않은 값이나 모순되는 값, 찾을 수 없는 값이 코드를 통과하는 경우가 있습니다.
앞선 상황과 같은 상태에 도달했지만 어떤 코드이던 간에 일어날 것으로 예상될 때라면 panic!
을 호출하는 것보다 Result
를 반환하는 것이 더 적합합니다. 예를 들면 기형적인 데이터가 주어지는 parser나, 속도 제한에 달했음을 나타내는 상태를 반환하는 HTTP 요청 등이 있습니다. 이러한 경우 Result
를 반환하면 호출자가 문제 처리 방식을 결정하여 상태를 위로 전파할 수 있습니다.
loop {
// snip
let guess: i32 = match guess.trim().parse() {
Ok(num) => num,
Err(_) => continue,
};
if guess < 1 || guess > 100 {
println!("The secret number will be between 1 and 100.");
continue;
}
match guess.cmp(&secret_number) {
// snip
}
위 코드와 같은 경우는 이상적인 유효성 보장 해결책이 아닙니다. 오직 1과 100 사이의 값에서만 동작하는 것이 전적으로 중요하고 많은 함수가 이러한 요구사항을 가진다면 잠재적으로 성능에 영향을 줄 것입니다.
이 대신 새로운 타입을 만들어, 모든 곳에서 유효성 확인을 하는 대신 해당 타입의 인스턴스를 생성하는 함수 내에 유효성 확인을 넣을 수 있습니다.
pub struct Guess {
value: u32,
}
impl Guess {
pub fn new(value: u32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess {
value
}
}
pub fn value(&self) -> u32 {
self.value
}
}