# anyhow, thiserror 등 에러 라이브러리

Rust에서 에러 처리를 조금 더 편리하고 유연하게 하기 위해서는 타사가 제공하는 라이브러리를 활용하는 방법이 유용하다. 특히 크게 두 가지 접근이 널리 쓰인다. 하나는 에러를 구체적으로 정의하기보다는 좀 더 범용적으로 다루기 위해서 `anyhow` 라이브러리를 사용하는 것이고, 다른 하나는 사용자가 직접 에러 타입을 작성하면서도 매크로를 통해 반복적인 코드를 줄여주는 `thiserror` 라이브러리를 사용하는 방식이다. 이 두 라이브러리를 적절히 이해하고 선택해 활용하면 에러 처리의 생산성과 가독성을 크게 높일 수 있다.

`anyhow`는 애플리케이션 전반에서 에러를 간단히 모아서 다루고자 할 때 적합하다. 구체적인 에러 타입보다는 다양한 에러가 모두 `anyhow::Error` 안에 담길 수 있도록 설계되었다. 이런 설계는 리턴 타입을 단순화하며, 실제 로직에서 발생할 수 있는 여러 가지 에러를 일일이 열거하기 어려울 때 유용하다. 또한 `?` 연산자로 발생한 에러를 손쉽게 전파할 수 있고, 에러를 생성하는 헬퍼 매크로인 `anyhow::bail!`이나 `anyhow::ensure!` 같은 유틸리티를 제공하기 때문에 편의성이 매우 높다. 따라서 간단한 프로젝트나 빠른 프로토타이핑, 도구성 애플리케이션 등을 작성할 때는 `anyhow`를 사용하는 것이 일반적이다.

예시를 살펴보면 다음과 같다.

```
[dependencies]
anyhow = "1.0"
use anyhow::{Result, Context};

fn read_file_contents(path: &str) -> Result<String> {
    let contents = std::fs::read_to_string(path)
        .with_context(|| format!("파일을 읽어오는 도중 에러가 발생하였다: {}", path))?;
    Ok(contents)
}

fn main() -> Result<()> {
    let file_data = read_file_contents("example.txt")?;
    println!("{}", file_data);
    Ok(())
}
```

위 예시에서는 `Result<T, anyhow::Error>`를 리턴 타입으로 사용한다. `with_context` 메서드를 통해 에러가 발생했을 때 맥락(Context)을 함께 제공할 수 있어 에러 메시지가 좀 더 풍부해진다. 또한 `main` 함수 역시 `-> Result<()>` 형태로 작성해주면, 발생하는 에러를 `?` 연산자를 사용해 간단히 전파하고 종료 시점에 적절히 처리하도록 할 수 있다.

만약 보다 구체적이고 의미 있는 에러 타입을 설계해야 하는 상황이라면 `thiserror`가 적절하다. `thiserror`는 에러 타입을 직접 정의하되, 각 에러 변형(variant)에 대해 에러 메시지와 표시 형식을 간단히 설정할 수 있는 매크로를 제공한다. 특히 라이브러리를 작성할 때나, 다양한 범주의 에러를 체계적으로 정의해두고 싶을 때 많이 쓰인다. `thiserror`를 통해 도출된 사용자 정의 에러 타입은 패턴 매칭으로 구체적인 에러 종류를 구분하기 좋고, 각 상황에 대한 핸들링 로직을 명확히 작성할 수 있다.

예시를 살펴보면 다음과 같다.

```
[dependencies]
thiserror = "1.0"
use thiserror::Error;

#[derive(Error, Debug)]
pub enum MyError {
    #[error("파일을 찾을 수 없다: {0}")]
    FileNotFound(String),

    #[error("네트워크 통신 오류가 발생하였다: {0}")]
    NetworkError(String),

    #[error("알 수 없는 에러가 발생하였다")]
    Unknown,
}

fn do_something(path: &str) -> Result<String, MyError> {
    if !std::path::Path::new(path).exists() {
        return Err(MyError::FileNotFound(path.to_string()));
    }

    // 실제 파일을 처리하는 로직 등...
    Ok("성공적으로 작업을 마쳤다".to_string())
}

fn main() {
    match do_something("not_exist.txt") {
        Ok(msg) => println!("{}", msg),
        Err(e) => eprintln!("오류 발생: {}", e),
    }
}
```

`#[derive(Error, Debug)]` 애트리뷰트로부터 `thiserror`의 기능을 활용하게 되며, 각 변형(variant)마다 `#[error("...")]` 애트리뷰트를 통해 에러 메시지를 작성할 수 있다. 이렇게 작성한 에러 타입은 매번 `String` 기반의 메시지를 생성하는 방식보다 훨씬 체계적으로 관리할 수 있고, 코드가 길어질수록 중요성이 더 커진다. 또한 에러 자체를 구분하기가 쉬워지고, `match` 등을 통해 패턴 매칭을 할 때도 각 변형마다 다른 로직을 적용하기 수월해진다.

종합하면 `anyhow`와 `thiserror`는 각각 쓰임새가 다르지만, 목적에 따라 매우 효과적인 에러 처리 방식을 제공한다. 빠른 프로토타이핑이나 단순한 애플리케이션에서는 `anyhow`의 범용적인 에러 타입을 사용하여 빠르게 개발을 진행할 수 있고, 보다 체계적으로 에러 타입을 정의해야 하거나 라이브러리를 작성하는 과정에서는 `thiserror`를 통해 에러를 명확히 선언하고 유지보수성을 높일 수 있다. 상황에 맞춰 이 두 라이브러리를 적절히 활용하는 것이 올바른 에러 처리 전략으로 이어진다.
