← Rust 강의 목록으로
🦀
에러 처리
에러 처리 · 선수: 17강

18. 커스텀 에러 타입 (thiserror·From)

실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.

Rustthiserror에러 타입FromError trait
소요 시간
약 1.5시간
난이도
📊 중급
선수 조건
🎯 17강
결과물
실무 라이브러리·서비스에서는 Box<dyn Error> 보다 구체적인 에러 타입을 정의해 호출자가 종류별로 분기할 수 있게 합니다. 이 강의에서 enum 으로 자체 에러 타입을 만들고, From 트레잇·Display·Error 트레잇을 손으로 구현해 보고, thiserror 크레잇으로 이걸 한 줄로 줄이는 방법까지 다룹니다.

이 강의에서 배우는 것

  • 1enum 으로 도메인별 에러 variant 를 정의한다
  • 2Display·Error 트레잇을 직접 구현한다
  • 3From<T> 로 다른 에러 자동 변환을 설정한다
  • 4thiserror 매크로로 보일러플레이트를 줄인다
  • 5anyhow 와 thiserror 의 역할 차이를 안다

소개

라이브러리는 어떤 종류의 실패가 일어났는지 호출자에게 정확히 알려야 합니다 — `MyError::NotFound`, `MyError::PermissionDenied` 같이 variant 로 분류해야 호출자가 분기 처리 가능. 이 강의에서 그 패턴을 손으로 한 번 만들고, thiserror 로 짧게 줄이는 흐름을 봅니다.

핵심 개념

1) 손으로 작성하는 커스텀 에러

rust
use std::fmt;

#[derive(Debug)]
enum AppError {
    NotFound(String),
    Invalid(String),
    Io(std::io::Error),
}

impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            AppError::NotFound(s) => write!(f, "not found: {}", s),
            AppError::Invalid(s)  => write!(f, "invalid: {}", s),
            AppError::Io(e)       => write!(f, "io: {}", e),
        }
    }
}

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

impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

2) thiserror — 한 줄로 줄이기

Cargo.toml 에 의존성 추가:

toml
[dependencies]
thiserror = "1"
rust
use thiserror::Error;

#[derive(Debug, Error)]
enum AppError {
    #[error("not found: {0}")]
    NotFound(String),
    #[error("invalid: {0}")]
    Invalid(String),
    #[error("io: {0}")]
    Io(#[from] std::io::Error),  // From 자동 생성
}

3) thiserror vs anyhow

크레잇역할추천 사용처
thiserror라이브러리용 구체 에러 enum 매크로재사용 라이브러리 작성
anyhow애플리케이션용 Box<dyn Error> wrapper + 컨텍스트바이너리·서비스 코드

핵심 예제

thiserror 로 만든 에러로 ? 자동 변환:

rust
use thiserror::Error;
use std::fs;
use std::num::ParseIntError;

#[derive(Debug, Error)]
enum AppError {
    #[error("file: {0}")]
    Io(#[from] std::io::Error),
    #[error("parse: {0}")]
    Parse(#[from] ParseIntError),
    #[error("empty file")]
    Empty,
}

fn first_number(path: &str) -> Result<i32, AppError> {
    let s = fs::read_to_string(path)?;           // io::Error → AppError
    let line = s.lines().next().ok_or(AppError::Empty)?;
    Ok(line.trim().parse::<i32>()?)              // ParseIntError → AppError
}

fn main() -> Result<(), AppError> {
    println!("{}", first_number("data.txt")?);
    Ok(())
}

호출자가 variant 별로 분기:

rust
match first_number("data.txt") {
    Ok(n)                       => println!("ok: {}", n),
    Err(AppError::Empty)        => eprintln!("파일이 비었음"),
    Err(AppError::Io(e))        => eprintln!("IO 실패: {}", e),
    Err(AppError::Parse(e))     => eprintln!("숫자 아님: {}", e),
}

자주 하는 실수

Q. Box<dyn Error> 만 쓰면 안 되나요?

A. 빠른 프로토타이핑이나 main 함수엔 OK. 라이브러리에서 그렇게 하면 호출자가 종류별 분기를 못 합니다 — 항상 다운캐스팅해야 해서 사용성이 떨어집니다.

Q. thiserror 와 anyhow 동시에?

A. 흔히 함께 씁니다. 라이브러리 모듈은 thiserror 로 구체 에러를 정의하고, 그것들을 사용하는 바이너리/main 에서는 anyhow::Result 로 받아 컨텍스트(.context) 를 덧붙이는 식.

Q. impl Error 가 꼭 필요한가요?

A. 다른 라이브러리·프레임워크와 상호운용하려면 필수. Error 트레잇을 구현해야 `Box<dyn Error>` 같은 통합 타입에 들어갈 수 있고 source() 체인을 따라갈 수 있습니다.

정리

  • enum 으로 도메인 에러를 variant 로 정의 → 호출자가 분기 가능
  • Display·Error·From 트레잇을 직접 구현하거나 thiserror 매크로로 한 줄
  • 라이브러리 = thiserror, 애플리케이션 = anyhow (혼용 가능)
  • ?의 자동 변환은 From 트레잇에 의존

과제

  1. 파일 IO 와 정수 파싱을 함께 다루는 함수의 커스텀 에러를 손으로 작성 (Display·Error·From)
  2. 같은 에러를 thiserror 로 재작성하고 코드 줄 수 비교
  3. 에러 variant 에 #[from] 을 붙이고 ? 가 자동 변환되는지 확인
예제 코드 / 강의 자료

전체 강의 자료와 예제 코드는 GitHub에서 자유롭게 받아볼 수 있습니다.

GitHub에서 보기 ↗