더 나은 에러 보고

우리는 모두 에러가 발생할 것이라는 사실을 받아들일 수밖에 없습니다. 다른 언어들과 다르게, 러스트를 사용할 때는 이 현실을 무시하기가 쉽지 않습니다. 러스트에는 예외가 없으며, 모든 발생 가능한 에러 상태는 보통 함수의 반환 타입으로 표현됩니다.

Result

read_to_string과 같은 함수는 문자열을 반환하지 않습니다. 대신, String이나 에러 타입 중 하나를 담은 Result를 반환합니다. (여기서 에러 타입은 std::io::Error)

어떤 타입이 들어있는지 어떻게 알 수 있을까요? Resultenum이기 때문에, match를 이용해 확인할 수 있습니다.

#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
match result {
    Ok(content) => { println!("File content: {}", content); }
    Err(error) => { println!("Oh noes: {}", error); }
}
}

Unwrap

이제 우리는 파일 내용에 접근할 수 있지만, match 블록 이후로 실제 뭔가를 할 수는 없습니다. 이를 위해서는 에러 케이스를 처리해야 합니다. 이때 어려운 부분은 match 블록의 모든 분기가 같은 타입을 반환해야 한다는 점입니다. 하지만 간단한 트릭이 있습니다:

#![allow(unused)]
fn main() {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { panic!("Can't deal with {}, just exit here", error); }
};
println!("file content: {}", content);
}

match 블록 이후에 content를 문자열로 사용할 수 있습니다. 만약 result가 에러라면 문자열은 존재하지 않게 되지만, result를 사용하기 전에 프로그램이 종료될 것이기 때문에 문제가 없습니다.

조금 과격해 보이지만, 매우 편리한 방법입니다. 만약 파일을 읽는 프로그램이 파일이 존재하지 않는 경우 아무것도 할 수 없다면, 프로그램 종료는 적합한 전략입니다. 여기에는 unwrap이라는 Result의 단축 메서드도 있습니다:

#![allow(unused)]
fn main() {
let content = std::fs::read_to_string("test.txt").unwrap();
}

패닉할 필요 없습니다

물론 프로그램 종료가 에러를 다루는 유일한 방법은 아닙니다. panic! 대신 단순히 return을 사용할 수 있습니다:

fn main() -> Result<(), Box<dyn std::error::Error>> {
let result = std::fs::read_to_string("test.txt");
let content = match result {
    Ok(content) => { content },
    Err(error) => { return Err(error.into()); }
};
Ok(())
}

그러나 이렇게 하려면 함수의 반환 타입을 변경해야 합니다. 지금까지의 모든 예시에 실제로는 숨겨진 부분이 있었습니다. 바로 이 코드가 속해 있는 함수 시그니처입니다. return이 있는 앞 예시에서 이것이 매우 중요해집니다. 여기 전체 예시가 있습니다:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let result = std::fs::read_to_string("test.txt");
    let content = match result {
        Ok(content) => { content },
        Err(error) => { return Err(error.into()); }
    };
    println!("file content: {}", content);
    Ok(())
}

우리의 반환 타입은 Result입니다! 덕분에 두 번째 match 분기에서 return Err(error);을 사용할 수 있습니다. 맨 마지막에 Ok(())가 보이시나요? 이는 함수의 기본 반환 값이며, “결과가 정상이고, 내용은 없다“라는 의미입니다.

물음표

.unwrap()을 호출하는 것은 match의 에러 분기에서 panic!을 사용하는 것과 동일한 일종의 단축어입니다. 또 다른 단축어로는 에러 분기의 return을 위한 ?가 있습니다.

맞아요, 물음표입니다. Result 타입의 값에 이 연산자를 붙일 수 있고, 러스트는 내부적으로 이 연산자를 우리가 작성한 match와 매우 비슷한 것으로 확장해 줍니다.

한번 해보세요:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let content = std::fs::read_to_string("test.txt")?;
    println!("file content: {}", content);
    Ok(())
}

정말 간결하죠!

맥락 제공하기

main 함수에서 ?를 사용하여 에러를 받는 것은 괜찮지만, 최선의 방법은 아닙니다. 예를 들어: std::fs::read_to_string("test.txt")?를 실행할 때 test.txt가 존재하지 않는다면, 아래와 같은 출력을 보게 될 것입니다:

Error: Os { code: 2, kind: NotFound, message: "No such file or directory" }

코드가 파일 이름을 포함하지 않는다면, 어떤 파일이 NotFound인지 말해주기가 상당히 어렵습니다. 이를 해결하는 여러 방법이 있습니다.

예를 들어, 우리만의 에러 타입을 만들 수 있습니다. 그리고 커스텀 에러 메시지를 만들면 됩니다:

#[derive(Debug)]
struct CustomError(String);

fn main() -> Result<(), CustomError> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .map_err(|err| CustomError(format!("Error reading `{}`: {}", path, err)))?;
    println!("file content: {}", content);
    Ok(())
}

이제, 프로그램을 실행하면 우리가 만든 커스텀 에러 메시지가 출력됩니다:

Error: CustomError("Error reading `test.txt`: No such file or directory (os error 2)")

그다지 예쁘지는 않지만, 나중에 디버그 출력을 우리 타입에 맞게 적용할 수 있습니다.

실제로 이러한 패턴은 매우 일반적입니다. 그러나 원본 에러가 아닌 문자열 표현만 저장한다는 문제가 있습니다. 이러한 문제를 해결하기 위해 주로 anyhow 라이브러리를 사용합니다. 이를 통해 CustomError 타입처럼 Context 트레잇을 이용해 설명을 추가할 수 있습니다. 더불어, 원본 에러를 저장함으로써 에러의 근본 원인을 알 수 있도록 해주는 에러 메시지 “체인“을 제공합니다.

먼저 Cargo.toml 파일의 [dependencies] 섹션에 anyhow = "1.0"을 추가하여 anyhow 크레이트를 가져옵니다.

전체 예시는 아래와 같습니다:

use anyhow::{Context, Result};

fn main() -> Result<()> {
    let path = "test.txt";
    let content = std::fs::read_to_string(path)
        .with_context(|| format!("could not read file `{}`", path))?;
    println!("file content: {}", content);
    Ok(())
}

실행하면 아래와 같이 에러가 출력됩니다:

Error: could not read file `test.txt`

Caused by:
    No such file or directory (os error 2)