Rust - Error Handling
Rust에서는 에러를 크게 두 가지로 구분합니다. 하나는 프로그램을 정지시키지 않고 처리 가능한 recoverable한 에러이고, 다른 하나는 프로그램의 실행 중지가 필요한 unrecoverable한 에러입니다.
Rust 맛보기
- 1. Rust - 입문하기
- 2. Rust - 타입 모음
- 3. Rust - Memory Ownership
- 4. Rust - Control Flow
- 5. Rust - Structured Data Types
- 6. Rust - Project Organization
- 7. Rust - Error Handling
- 8. Rust - Collections
- 9. Rust - Generics
- 10. Rust - Test Automation
- 11. Rust - Functional Programming
- 12. Rust - Memory Management
Recoverable error의 예로는 File Not Found Error 등이 있고, unrecoverable error의 예로는 배열의 out of bounds 등이 있습니다.
Rust는 이러한 서로 다른 유형의 에러에 따라 다른 처리 방법을 제공하고 있습니다.
Recoverable error는 Result
enum을 사용하여 처리하고, unrecoverable error는 panic!
매크로를 사용하여 처리합니다.
panic!
매크로
예를 들어 배열의 overread같은 경우 C는 이를 감지하지 못하지만, Rust에서는 이를 자동으로 감지하여 panic을 발생시킵니다. panic이 발생하면 프로그램은 안전하게 종료됩니다. panic!
매크로를 사용하여 유저가 직접 명시적으로 panic을 발생시킬 수도 있습니다.
Backtrace
RUST_BACKTRACE=1 cargo run
debug 심볼이 활성화된 상태에서 위와 같이 실행하면 어떤 코드가 panic을 발생시켰는지 알 수 있습니다.
Handling Recoverable Errors
프로그램을 정지시키지 않고 에러를 해석 및 핸들링하기 위해 Result
enum을 사용합니다. Result
enum은 Ok
와 Err
두 가지 variant를 가지고 있습니다.
예를들어 File::open
함수는 Result
enum을 반환합니다.
use std::fs::File;
fn main() {
let f = File::open("hello.txt");
let f = match f {
Ok(file) => file, // file: std::fs::File (파일 핸들)
Err(error) => { // error: std::io::Error
panic!("Problem opening the file: {:?}", error)
// :?는 디버깅을 위한 출력 포맷
},
};
}
Error 타입에 대한 패턴 매칭
io::Error
는 kind()
메소드를 갖고 있는데 이를 통해 io::ErrorKind
enum을 반환합니다. 이를 통해 에러의 종류를 판별할 수 있습니다.
예를들어 ErrorKind::NotFound
는 파일이 존재하지 않는 경우를 나타냅니다. (파이썬으로 치면 FileNotFoundError
)
use std::fs::File;
use std::io::ErrorKind;
fn main() {
let greeting_file_result = File::open("hello.txt");
let greeting_file = match greeting_file_result {
Ok(file) => file,
Err(error) => match 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), // {e:?} 도 가능
},
other_error => {
panic!("There was a problem opening the file: {:?}", other_error)
}
},
};
}
unwrap
과 expect
이전에도 가볍게 다뤘었지만, unwrap
은 Result
enum의 Ok
variant를 반환하고, Err
variant가 발생하면 panic!
을 호출합니다.
expect
는 여기에 더해 Err
가 발생했을 때에 인자로 에러 메시지를 지정 가능한 함수입니다.
Propagating Errors
함수가 Result
enum을 반환하도록 하여 함수 내부에서 발생한 에러를 바깥에서 처리할 수 있도록 하는 것을 error propagation이라고 합니다.
use std::fs::File;
use std::io;
fn read_username_from_file() -> Result<String, io::Error> {
// Ok(String) 또는 Err(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),
}
}
?
: Shortcut for Propagation
?
연산자는 Result
enum을 반환하는 함수에서 사용할 수 있는 shortcut입니다. Result
enum을 반환하는 함수 내에서 ?
를 사용하면 Ok
variant가 반환되면 Ok
의 값을 반환하고, Err
variant가 반환되면 Err
의 값을 반환합니다.
use std::fs::File;
use std::io;
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, 실패시 Err 반환
// 단 Ok는 함수 내부에서 반환되므로 함수가 종료되지 않고, Err는 외부로 반환되며 함수가 종료됨
Ok(s) // read_to_string의 Ok variant는 필요없으니 여기서 Ok(s)로 반환
}
?
연산자와 match
표현식의 차이는, ?
연산자는 내부적으로 from()
함수를 호출한다는 점입니다. ?
연산자가 from()
함수를 호출하게 될 경우, 에러의 타입을 현재 함수가 리턴하는 타입으로 변환하게 됩니다. 이러한 특성은 함수 내부에서 발생 가능한 다양한 kind를 갖는 에러를 하나의 에러 타입으로 변환하여 처리할 수 있게 해줍니다.
?
연산자는 Result
또는 Option
enum (또는 FromResidual
타입)을 반환하는 함수 내에서만 사용할 수 있다는 점에 주의해야 합니다.
Option
타입에 ?
연산자를 사용하게 되면, Result
일 때와 비슷하게 값이 None
variant일 때에는 그 시점에서 함수가 None
을 반환하고, Some
variant일 때에는 Some
의 값을 해당 expression scope에서 반환 후 함수가 계속됩니다.
ok
메소드와 ok_or
메소드
Result
와 Option
타입을 상호 변환하는 메소드입니다.
-
ok()
:Result<T, E>
타입을Option<T>
로 변환합니다.Ok
variant는Some
으로,Err
variant는None
으로 변환됩니다. -
ok_or()
:Option<T>
타입을Result<T, E>
로 변환합니다.Some
variant는Ok
로,None
variant는Err
로 변환됩니다.
참고 함수: std::fs::read_to_string
use std::fs;
fn read_username_from_file() -> Result<String, io::Error> {
fs::read_to_string("hello.txt")
}
이 함수는 std::io::Read
trait에 정의된 동명의 메소드와는 다른 것으로, 새 스트링을 만들고, 파일을 열어 컨텐츠를 스트링에 저장한 후 Result
enum에 담아 반환하는 일련의 작업을 한 번에 수행하는 함수입니다.
main
함수의 리턴 타입 명시
main
함수는 유닛 타입 ()
또는 Result<(), E>
만 반환할 수 있는 제약이 있습니다.
main
함수의 리턴 타입을 Result<(), Box<dyn Error>>
로 명시하면 main
함수에서 에러 발생 시 C에서처럼 에러 값을 반환하며 프로그램이 종료되도록 할 수 있습니다.
Box<dyn Error>
는 모든 에러 타입을 포괄하는 Trait 객체입니다.
use std::error::Error;
fn main() -> Result<(), Box<dyn Error>> {
let _result = read_username_from_file()?;
Ok(())
}
프로그램이 정상적으로 실행됐을 때 Ok(())
를 리턴하도록 하면 0
을 반환하며 프로그램이 종료됩니다.
Validation을 위해 Custom Type 생성하기
Guessing Game 예제를 떠올려 봅시다. 사용자가 입력한 값이 1과 100 사이의 값인지 확인하려면 어떻게 하는 것이 좋을까요? if-else
문을 사용하여 확인할 수도 있지만, 이는 같은 코드의 반복적인 사용을 유도해서 코드의 가독성을 떨어뜨릴 수 있습니다.
대신, 새로운 타입을 만든 후 해당 타입의 인스턴스를 생성할 때 유효성 검사를 수행하도록 하는 것이 좋습니다.
pub struct Guess(i32);
impl Guess {
pub fn new(value: i32) -> Guess {
if value < 1 || value > 100 {
panic!("Guess value must be between 1 and 100, got {}.", value);
}
Guess(value)
}
pub fn value(&self) -> i32 {
self.0
}
}
fn main() {
let guess = Guess::new(50);
println!("Guess value: {}", guess.value());
}
참고 문헌
-
고려대학교 컴퓨터학과 오상은 교수님의 시스템 프로그래밍(COSE322) 과목 강의자료
Rust 맛보기