[Rust]

[Rust] 에러 처리

극꼼 2023. 8. 16. 13:13
반응형


<에러 처리>

러스트는 에러를 복구 가능한 에러, 복구 불가능한 에러 두 범주로 나눕니다. 대부분의 언어는 예외처리 메커니즘을 쓰는데 러스트에는 이 기능이 없는 대신, 복구 가능한 에러를 위한 Result<T, E> 타입이 있고 복구 불가능한 에러가 발생하면 프로그램을 종료하는 panic! 매크로가 있습니다.

 

1) panic! 으로 복구 불가능한 에러 처리

패닉을 일으키는 방법에는 패닉을 일으킬 동작을 하는 것과 panic! 매크로를 명시적으로 호출하는 방법이 있습니다.

패닉은 실패 메시지를 출력하고, 되감고(unwind), 스택을 청소하고, 종료합니다. 패닉이 발생하면 근원을 쉽게 추적하기 위해 환경 변수를 통해 러스트가 호출 스택을 보여주도록 할 수 있습니다.

 

* unwinding : panic!이 발생했을 때 러스트가 패닉을 발생시킨 각 함수로부터 스택을 거꾸로 훑어가면서 데이터를 청소하는 것입니다.
* aborting : 데이터 정리 작업 없이 즉각 종료합니다. 프로그램이 사용하고 있던 메모리를 운영체제가 청소해 주어야 하는데, 프로젝트 내의 결과 바이너리를 가능한 한 작게 만들고 싶을 때 Cargo.toml [profile] 섹션에 panic = 'abort'를 추가할 수 있습니다.
[profile.release]
panic = 'abort'​

 

panic! 호출을 다음과 같이 사용할 수 있습니다. 에러 메시지로는 thread 'main' panicked at 'crash and burn' 가 출력됩니다.

fn main() {
    panic!("crash and burn");
}

 

 

* 백트레이스(backtrace) : 어떤 지점에 도달하기까지 호출한 모든 함수의 목록

벡터의 유효한 범위를 넘어서는 인덱스로 접근을 시도하는 것은 러스트에서 발생하는 대표적인 패닉 예시입니다. 이를 버퍼 초과 읽기(buffer overread)라 하며, 어떤 공격자가 배열 뒤에 저장된 데이터를 읽어 낼 요량으로 인덱스를 다루면 보안 취약점이 될 수 있기 때문에 이를 보호하기 위해 러스트는 실행을 멈추게 됩니다.

let v = vec![1, 2, 3];
v[99];
thread 'main' panicked at '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

stack backtrace: 0: rust_begin_unwind at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/std/src/panicking.rs:593:5 1: core::panicking::panic_fmt at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/panicking.rs:67:14 2: core::panicking::panic_bounds_check at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/panicking.rs:162:5 3: <usize as core::slice::index::SliceIndex<[T]>>::index at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/slice/index.rs:258:10 4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/slice/index.rs:18:9 5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/alloc/src/vec/mod.rs:2690:9 6: playground::main at ./src/main.rs:3:1 7: core::ops::function::FnOnce::call_once at /rustc/eb26296b556cef10fb713a38f3d16b9886080f26/library/core/src/ops/function.rs:250:5

에러 메시지에서 RUST_BACKTRACE 환경 변수를 설정해서 에러의 원인이 뭔지 백트레이스하고 있습니다.

 

백트레이스 활성화를 위해서는 디버그 심볼이 활성화되어 있어야 하는데, cargo build, cargo run을 --release 플래그 없이 실행했을 때 기본적으로 활성화됩니다.

 

2) Result로 복구 가능한 에러 처리

: Result 열거형은 Ok, Err 두 개의 variant를 가지도록 정의되어 있습니다.

* T, E : 제네릭 타입 매개변수. E는 실패한 경우 반환될 에러의 타입

enum Result<T, E> {
    Ok(T),
    Err(E),
}

 

다음은 사용자 입력을 받고, 숫자 타입이 아니면 0값을 가지도록 하는 코드입니다. Option 열거형과 Result 열거형은 같은 Prelude mod에서 가져왔기 때문에 match 갈래의 Ok, Err 앞에 Result::라고 지정하지 않아도 됩니다.

use std::io;

fn main() {
    let mut guess = String::new();
    io::stdin()
        .read_line(&mut guess)
        .expect("Failed to read line");

    let guess: u32 = match guess.trim().parse() {
        Ok(num) => num,
        Err(_) => {
            println!("Not number");
            0
        },
    };
}

 

- 서로 다른 에러 처리

: 아래 코드와 같이 Err(error)를 다시 match로 받아서 내부 매칭을 통해 NotFound 타입인지, 그 외 타입인지로 나눕니다. NotFound variant일 경우 create로 파일을 생성하되, create가 생성할 경우에 대한 에러 매칭도 진행시킵니다.

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!("Problem creating the file: {:?}", e),
            },
            other_error => {
                panic!("Problem opening the file: {:?}", other_error);
            }
        },
    };
}

 

- unwrap, expect : 에러 발생 시 패닉을 위한 숏컷 메서드입니다.

  • unwrap 메서드 : match 구문과 비슷한 구현을 한 숏컷 메서드로, Result가 Err variant일 경우 panic! 매크로를 호출해줍니다.
let greeting_file = File::open("hello.txt").unwrap();

위 코드에 대한 에러 메시지는 다음과 같습니다.

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value: Os { code: 2, kind: NotFound, message: "No such file or directory" }', src/main.rs:4:49
  • expect 메서드 : 기본 에러 메시지를 호출하는 unwrap 메서드와 달리, 매개변수로 전달한 에러 메시지를 출력합니다.
let greeting_file = File::open("hello.txt")
    .expect("hello.txt should be included in this project");

 

- 에러 전파하기(propagating) : 에러를 처리할 때 함수를 호출하는 쪽으로 에러를 반환에서 그쪽에서 처리할 수 있게 제어권을 주는 것입니다.

다음 예시 코드에서는 match를 사용해 file 변수를 호출한 코드(file.read_to_string(&mut username))로 에러를 넘겼습니다.

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file1(file_name : String) -> Result<String, io::Error> {
    let file_result = File::open(file_name);
    let mut file = match file_result {
        Ok(file) => file,
        Err(e) => return Err(e),
    };
    
    let mut username = String::new();
    match file.read_to_string(&mut username){
        Ok(_) => Ok(username),
        Err(e) => Err(e)
    }
}

fn main() {
    let file_name = String::from("happy cat.txt");
    let username = read_username_from_file(file_name);
    println!("{:?}",username);
}

 

- 물음표 연산자 : 에러 전파 패턴을 더 간단하게 구현할 수 있게됩니다.

위에서 예시로 들었던 read_username_from_file() 함수를 다시 예시로 들어 물음표 연산자를 사용해보겠습니다. 에러가 발생할 경우 ?는 함수로부터 빠져나와서 호출하는 코드에게 Err값을 줍니다.

fn read_username_from_file(file_name : String) -> Result<String, io::Error> {
    let mut username_file = File::open("hello.txt")?;
    let mut username = String::new();
    username_file.read_to_string(&mut username)?;
    Ok(username)
}

match 표현식과 ? 연산자의 차이점 : ? 연산자를 사용할 때 에러 값은 from 함수를 거칩니다. ? 연산자가 from을 호출하면, ? 연산자가 사용된 현재 함수의 반환 타입에 정의된 에러 타입으로 에러를 변환합니다. 이는 모든 에러를 하나의 에러 타입으로 반환할 때 유용합니다.

 

* read_username_from_file() 함수를 더 간단히 구현할 수 있습니다. fs::read_to_string() 함수는 
1) 파일을 열고
2) 새 String을 생성하고
3) 파일 내용을 읽고
4) 내용을 String에 넣어 반환합니다.
use std::fs;
use std::io;

fn read_username_from_file(file_name : String) -> Result<String, io::Error> {
    fs::read_to_string(file_name)
}

 

? 연산자는 ?가 사용된 값과 호환 가능한 반환 타입의 함수에서만 사용할 수 있습니다. 위에서 보았던 ? 연산자 예시 코드에서 read_username_from_file() 함수는 Result<String, io::Error> 값을 반환하고 있습니다. 만약 호환되지 않는 타입의 함수일 경우 컴파일할 때 다음과 같은 에러가 뜰 것입니다.

error[E0277]: the `?` operator can only be used in a function that returns `Result` or `Option` (or another type that implements `FromResidual`)

 

* main() 함수도 Result<(), E>를 반환할 수 있습니다. 실행 파일은 main이 Ok(())를 반환할 경우 0 값으로 종료되고, Err 값을 반환하면 0이 아닌 값으로 종료됩니다.

 

Result를 반환하는 함수에서도 Result에 ? 연산자를 사용할 수 있고, Option을 반환하는 함수에서도 그렇지만 이 둘을 한 함수에서 섞어서 사용할 수는 없습니다.


3) panic!을 써야할까, Result를 써야할까?

기본적으로 Result를 반환하는 것이 좋은 선택이지만, 예제, 프로토타입, 테스트 상황에서는 패닉을 일으키는 코드가 더 적절합니다. 

  • 실패가 예상될 경우 -> Result
  • 실패가 예상되지 않을 경우 -> panic!
  • 코드에 유효하지 않은 값이 호출되었을 경우 -> panic!

 

- 유효성 검사를 위한 커스텀 타입

Guess 구조체의 value필드에 1~100 사이의 숫자만 입력되어야 할 때 다음과 같이 유효성 검사를 할 수 있습니다.

#[derive(Debug)]
pub struct Guess{
    value: 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.value
    }
}

Guess를 사용하면 결과는 다음과 같습니다.

fn main() {
    let num1 = Guess::new(10);
    println!("{:?}", num1); 
    // Guess { value: 10 } 출력
    
    let num2 = Guess::new(101); 
    // panic!
    // thread 'main' panicked at 'Guess value must be between 1 and 100, got 101.' 출력
}

출처 : https://rust-kr.github.io/doc.rust-kr.org/ch09-00-error-handling.html

반응형

'[Rust]' 카테고리의 다른 글

[Rust] 트레이트(trait)  (0) 2023.08.18
[Rust] 제네릭 타입  (0) 2023.08.17
[Rust] match 연산자  (0) 2023.08.15
[Rust] 컬렉션(Collection)  (0) 2023.08.14
[Rust] 패키지, 크레이트, 모듈로 프로젝트 관리  (0) 2023.08.11