/ PROGRAMMING, RUST

Rust - Error Handling

Rust에서는 에러를 크게 두 가지로 구분합니다. 하나는 프로그램을 정지시키지 않고 처리 가능한 recoverable한 에러이고, 다른 하나는 프로그램의 실행 중지가 필요한 unrecoverable한 에러입니다.

Rust 맛보기

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은 OkErr 두 가지 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::Errorkind() 메소드를 갖고 있는데 이를 통해 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)
            }
        },
    };
}

unwrapexpect

이전에도 가볍게 다뤘었지만, unwrapResult 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 메소드

ResultOption 타입을 상호 변환하는 메소드입니다.

  • 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());
}

참고 문헌

Rust 맛보기