Rust - chapter 9 error handling


Rust 문서 챕터 9 에러처리를 읽고 정리한 글이다.(링크)

에러 처리

러스트에서는 에러를 두가지 범주로 묶는다고 한다. 복구 가능한 에러, 복구 불가능한 에러
다수의 프로그래밍 언어는 예외처리 기능을 가지고 있지만 러스트는 없다고 한다. 대신 복구 가능한 에러를 위한 Result<T, E> 와 복구 불가능한 에러에 대응할 수 있는 panic! 매크로가 있다.

복구 불가능한 에러

복구 불가능한 에러의 경우는 panic! 매크로를 사용하여 실패 메시지를 출력하고 스택을 되감아 청소한 뒤 프로그램을 종료할 수 있다.

💿   panic! 매크로 스택 되감기 설정
기본적으로 이 매크로는 실행되면 프로그램이 되감기를 시작하는데 이는 스택을 거꾸로 훓어가면서 데이터를 제거하는 동작이다. 하지만 이 작업은 비용이 크므로 데이터 없이 프로그램을 종료할 수 있는 방법으로 Cargo.toml 파일에 [profile] 섹션에 panic = 'abort' 를 설정이 있다.

[profile.release]
panic = 'abort'

복구 불가능한 에러로는 벡터에 존재하지 않는 인덱스를 호출하는 것 같이 있다.
panic! 매크로는 간단하게 사용할 수 있는데 아래와 같다.
또한 각 구현체에 구현이 되어 있기에 따로 작성하지 않더라도 대부분의 오류 케이스에 러스트는 panic! 매크로를 호출 할 것이다.

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

발생하는 오류를 자세하 확인하고 싶다면 실행할 때 RUST_BACKTRACE 환경변수를 넣어주도록 하자.

RUST_BACKTRACE=1 cargo run

복구 가능한 에러

Result<T, E> 타입을 사용하는 경우이다. T 는 제너릭, E 는 에러 타입이다.
간단한 예제를 살펴보자

use std::fs::File;

fn main() {
  let f: u32 = File::open("hello.txt");
}

위 예제는 오류를 발생한다. File::open() 함수는 반환 타입이 Resut<T, E> 이기 때문이다. Result<T, E> 타입을 처리하는 경우는 많다.

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) => {
          // 1
          panic!("Tried to create file but there was a problem: {:?}", e);
        }
      }
    },
    Err(error) => {
      // 2
      panic!("Problem opening the file: {:?}", error);
    }
  };
}

위에서는 match 를 통해 파일이 없을 경우는 파일을 생성하고 생성애도 실패할 경우 패닉 매크로를 호출하고 다른 에러라면 2번 매크로가 동작하게 된다. 작성한 순서대로 확인을 한다는 점과 참조자를 얻기 위해서 & 이 아닌 ref 를 사용한 점을 유의하자. (이는 18장에서 설명한다고 한다. 9장인데…)

위 형태를 mat`ch 를 대신하여 아래와 같이도 작성할 수 있으니 참조하자.

use std::fs::File;
use std::io::ErrorKind;

fn main() {
  let f = File::open("hello.txt").unwrap_or_else(|error| {
    if error.kind() == ErrorKind::NotFound {
      File::create("hello.txt").unwrap_or_else(|error| {
        panic!("Problem creating the file: {:?}", error);
      })
    } else {
      panic!("Problem opening the file: {:?}", error);
    }
  });
}

panic! 단축기능 - unwrap, expect

매번 저렇게 패턴을 작성하는 것은 매우 비효율적이다. 그래서 간단히 작성 할 수 있는 방법이 있다.

use std::fs::File;

fn main() {
  let f = File::open("hello.txt").unwrap();
  let f = File::open("hello.txt").expect("Failed to open hello.txt");
}

모두 파일을 여는데 실패한다면 패닉 매크로를 호출하게 된다. 문서에는 expect 를 사용하는 것을 권장하는데 이는 에러에 대해 예제처럼 명확하게 작성할 수 있기 때문이다. upwarp 메소드는 에러 메시지를 사용자가 작성할 수 없다.

에러 전파하기, 단축기능 - ?

보통 에러가 발생할 수 있는 함수일 경우 함수 내에서 에러를 처리하기보다는 호출하는 쪽에 코드를 반환하여 호출하는 곳에서 어떻게 처리할지 결정하도록 한다. 이를 에러 전파하기(Propagating Errors) 라 한다.

use std::io;
use std::io::Re
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),
  }
}

먼저 파일을 열고 파일이 없을 경우 먼저 에러를 반환하고 파일에서 문자열을 읽고 여기서도 실패하면 에러를 반환하는 코드이다. 이 코드를 ? 를 사용하여 단축하면 아래와 같이 된다.

use std::io;
use std::io::Re
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(s)?;

  Ok(s);
}

panic! 사용 시점

언제 panic! 을 사용하고 Result 를 반환할지 결정하는 것도 어려운 일이 될 것으로 보인다. 문서에서는 아래의 경우가 아니라면 기본적으로 Result를 반환하는 것을 권장하고 있다.

  • 예제, 프로토타입 코드, 테스트 코드
  • 사용자가 컴파일러보다 많은 정보를 가지고 확신할 수 있는 경우
  • 타인이 내 코드를 호출하거나 하는 등 코드에 대한 제어권이 없을 경우
  • 커스텀 타입을 생성하고 해당 타입의 유효성을 위해서 사용하는 경우

개인이 참고하고자 작성한 글이며, 잘못된 정보가 있을 수 있습니다. 잘못된 정보는 메일로 보내주시면 감사하겠습니다. 🙏