🦀
에러 처리
에러 처리 · 선수: 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 트레잇에 의존
과제
- 파일 IO 와 정수 파싱을 함께 다루는 함수의 커스텀 에러를 손으로 작성 (Display·Error·From)
- 같은 에러를 thiserror 로 재작성하고 코드 줄 수 비교
- 에러 variant 에 #[from] 을 붙이고 ? 가 자동 변환되는지 확인