# 커스텀 오류 타입 정의

러스트에서 오류 처리를 더욱 견고하게 하려면 표준 라이브러리에서 제공하는 `Result<T, E>` 패턴뿐만 아니라 프로젝트 상황에 맞는 커스텀 오류 타입을 정의하는 방식을 고려해볼 수 있다. 커스텀 오류 타입을 정의하면 발생할 수 있는 오류를 의미적으로 더 명확히 표현하고, 오류 발생 원인을 호출자에게 구체적으로 전달할 수 있으며, 오류를 좀 더 유연하게 핸들링할 수 있다는 장점이 있다.

가장 단순한 형태의 커스텀 오류 타입은 열거형(enum)으로 정의하고, `Debug`, `Display`, 그리고 `std::error::Error` 트레이트를 구현하는 것이다. 예를 들어 입출력과 정수 파싱 중에 생기는 오류를 하나의 타입으로 다루고 싶다면 다음처럼 작성할 수 있다.

```
#[derive(Debug)]
enum MyError {
    Io(std::io::Error),
    ParseInt(std::num::ParseIntError),
    InvalidData(String),
}

impl std::fmt::Display for MyError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            MyError::Io(e) => write!(f, "IO 오류 발생: {}", e),
            MyError::ParseInt(e) => write!(f, "정수 파싱 오류 발생: {}", e),
            MyError::InvalidData(msg) => write!(f, "잘못된 데이터: {}", msg),
        }
    }
}

impl std::error::Error for MyError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            MyError::Io(e) => Some(e),
            MyError::ParseInt(e) => Some(e),
            MyError::InvalidData(_) => None,
        }
    }
}
```

위 코드에서 `MyError`는 입출력 오류, 정수 파싱 오류, 그 밖의 임의의 데이터 유효성 오류를 하나의 타입으로 표현한다. `Debug`는 `#[derive(Debug)]`로 자동 구현하고, `Display`는 구체적인 오류 메시지를 커스터마이징하기 위해 수동으로 구현한다. `std::error::Error` 트레이트를 구현할 때는 `source` 메서드를 통해 내부에 포함된 다른 오류를 반환하도록 할 수 있다. 이를 통해 오류 체인(chaining)이 가능해지고, 외부에서 오류를 디버깅하거나 로그를 분석할 때 추가 정보를 얻기 쉽다.

러스트 표준 라이브러리의 `From` 트레이트를 구현하면 기존의 오류 타입을 `MyError` 타입으로 쉽게 전환할 수 있다. 예를 들어 입출력 오류를 `MyError`로 손쉽게 변환하고 싶다면 다음과 같이 구현한다.

```
impl From<std::io::Error> for MyError {
    fn from(error: std::io::Error) -> Self {
        MyError::Io(error)
    }
}

impl From<std::num::ParseIntError> for MyError {
    fn from(error: std::num::ParseIntError) -> Self {
        MyError::ParseInt(error)
    }
}
```

이런 식으로 `From` 트레이트가 구현되어 있으면 `?` 연산자를 사용할 때 기존 오류 타입이 자동으로 `MyError`로 변환된다. 예를 들어 파일 읽기와 정수 파싱을 동시에 수행하는 함수를 작성한다고 해보자.

```
fn read_and_parse_int(path: &str) -> Result<i32, MyError> {
    let contents = std::fs::read_to_string(path)?; 
    let parsed = contents.trim().parse::<i32>()?;
    Ok(parsed)
}
```

이 함수는 내부적으로 표준 라이브러리의 `std::fs::read_to_string`이 `std::io::Error`를 반환하더라도, `From<std::io::Error>`가 구현되어 있으므로 `?` 연산자가 이를 자동으로 `MyError::Io` 형태로 변환한다. 정수 파싱에 실패할 경우에도 같은 방식으로 `ParseIntError`가 `MyError::ParseInt`가 되어 반환된다.

커스텀 오류 타입을 좀 더 편리하게 정의하기 위해서는 `thiserror` 크레이트를 사용할 수도 있다. 이를 이용하면 열거형을 간결하게 작성하고, `Display`와 `Error` 구현부도 자동으로 제공받을 수 있다. 예시는 다음과 같다.

```
use thiserror::Error;

#[derive(Error, Debug)]
enum MyError {
    #[error("IO 오류 발생: {0}")]
    Io(#[from] std::io::Error),

    #[error("정수 파싱 오류 발생: {0}")]
    ParseInt(#[from] std::num::ParseIntError),

    #[error("잘못된 데이터: {0}")]
    InvalidData(String),
}
```

`thiserror::Error` 매크로를 사용하면 `Display` 구현을 위해 필요한 문자열을 `#[error(...)]` 어트리뷰트로 작성하기만 하면 된다. 이때 `#[from]` 어트리뷰트를 적절히 활용하면 `From` 트레이트를 자동으로 구현할 수 있어 `?` 연산자와 함께 사용하기 매우 편리하다.

커스텀 오류 타입을 정의하는 이유는 프로젝트의 오류 상황을 더욱 명확히 표현하고, 에러 체인 등을 통해 추적 가능성을 높이기 위함이다. 단순히 문자열만 반환하는 대신 열거형으로 오류를 구체화하면 호출자 입장에서도 오류를 세밀하게 구분해 핸들링할 수 있다. 예를 들어 `MyError::InvalidData` 변 variant를 만나면 사용자는 입력값이 잘못되었음을 즉시 파악해 입력 재시도를 유도할 수 있고, `MyError::Io`가 발생하면 외부 시스템 문제를 의심해볼 수 있다.

또한 커스텀 오류 타입을 사용할 때는 가능하면 소스 오류를 그대로 보존해주어야 한다. 예제 코드에서 보았듯이 `source` 메서드나 `From` 트레이트 구현을 통해 원본 오류를 담아주면 나중에 문제 분석이 쉬워진다. 예를 들어 로그를 남길 때 구체적인 IO 오류 유형(예: 권한 부족, 파일 없음 등)을 함께 기록할 수 있게 되어 유지보수가 좋아진다.

러스트 생태계에서 오류 처리는 여러 방법이 공존한다. 표준 라이브러리의 `std::error::Error`만 잘 활용해도 충분하지만, 코드 양이 많아지면 `thiserror`와 같은 서드파티 크레이트가 큰 도움이 된다. 반면 `anyhow`는 주로 애플리케이션 레벨에서 오류 전파를 단순화하기 위해 사용되고, 구체적인 오류 원인을 처리해야 할 때는 적합하지 않을 수 있다. 커스텀 오류 타입을 정의해두면 로직 계층에서 별도의 분기를 쉽게 처리할 수 있고, `anyhow`를 사용하는 상위 계층에서는 이 커스텀 오류를 포장하여 최종 사용자에게는 단순한 메시지만 노출하도록 하는 혼합 전략도 많이 쓰인다.

결론적으로, 커스텀 오류 타입 정의는 러스트 애플리케이션에서 오류 상황을 체계적으로 관리하기 위한 필수적인 기법이다. 간단한 프로토타입에서는 문자열 기반의 오류 처리만으로도 충분할 수 있지만, 코드가 복잡해지고 모듈 경계가 명확해질수록 커스텀 오류 타입이 주는 이점은 커진다. 열거형에 다양한 오류 항목을 정의하고, `Display`와 `std::error::Error`를 올바르게 구현한 뒤, `From` 트레이트나 `thiserror`로 전환 로직을 단순화하면, 이후 오류를 추적하고 수정하는 과정이 훨씬 수월해진다. 이러한 접근은 코드의 가독성과 유지보수성을 모두 높일 수 있는 좋은 방법이다.
