Rust 에러 처리 패턴 — anyhow vs thiserror, 실전에서 뭘 써야 하나
unwrap() 남발하다가 프로덕션에서 panic 터졌다. Rust의 에러 처리를 제대로 배웠다.
anyhow vs thiserror
두 라이브러리의 목적은 다르다:
anyhow: 애플리케이션에서 사용 (빠른 개발)
thiserror: 라이브러리에서 사용 (명확한 에러 타입)
anyhow: 빠른 에러 처리
애플리케이션이라면 anyhow:
use anyhow::Result;
async fn process_audio(file_path: &str) -> Result<()> {
let content = std::fs::read(file_path)
.context("Failed to read audio file")?;
let decoded = decode_audio(&content)
.context("Failed to decode audio")?;
save_processed(&decoded)
.context("Failed to save processed audio")?;
Ok(())
}
// main
#[tokio::main]
async fn main() {
match process_audio("voice.wav").await {
Ok(_) => println!("Success"),
Err(e) => {
eprintln!("Error: {}", e); // ← 전체 error chain 출력
std::process::exit(1);
}
}
}
장점:
- 쉽고 빠르다
- 에러 체인 자동 생성 (.context())
- 에러 타입을 강제하지 않음
thiserror: 명확한 에러 타입
라이브러리라면 thiserror (또는 커스텀 enum):
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AudioError {
#[error("Failed to read file: {0}")]
FileRead(#[from] std::io::Error),
#[error("Invalid audio format: {0}")]
InvalidFormat(String),
#[error("Decoding failed: {0}")]
DecodingError(String),
#[error("Unsupported sample rate: {0}")]
UnsupportedSampleRate(u32),
}
pub fn decode_audio(data: &[u8]) -> Result<Vec<f32>, AudioError> {
if data.len() < 4 {
return Err(AudioError::InvalidFormat(
"Data too short".to_string()
));
}
// ...
Ok(samples)
}
// 사용처에서 특정 에러를 처리 가능
match decode_audio(data) {
Ok(samples) => { /* ... */ }
Err(AudioError::InvalidFormat(msg)) => {
println!("Invalid format: {}", msg);
}
Err(AudioError::UnsupportedSampleRate(rate)) => {
println!("Unsupported rate: {}", rate);
}
Err(e) => {
eprintln!("Other error: {}", e);
}
}
장점:
- 호출자가 특정 에러를 처리 가능
- 타입 안전
- 라이브러리 사용자에게 명확
실전 패턴
패턴 1: ? 연산자
fn process() -> Result<()> {
let file = std::fs::read("data.txt")?; // ← Err 시 즉시 반환
let data = parse(&file)?;
save(&data)?;
Ok(())
}
패턴 2: map_err로 에러 변환
fn parse_number(s: &str) -> Result<i32, AudioError> {
s.parse()
.map_err(|_| AudioError::InvalidFormat(
format!("Cannot parse: {}", s)
))
}
패턴 3: custom_context (anyhow)
fn process_file(path: &str) -> anyhow::Result<()> {
let content = std::fs::read(path)
.with_context(|| format!("Failed to read: {}", path))?;
Ok(())
}
// 에러 메시지
// Error: Failed to read: voice.wav
//
// Caused by:
// No such file or directory (os error 2)
panic vs Result
나쁜 패턴:
// ❌ 절대 금지
let data = std::fs::read("voice.wav").unwrap(); // panic!
let sample_rate: u32 = data[0..4].iter().sum(); // panic!
좋은 패턴:
// ✅ 좋음
let data = std::fs::read("voice.wav")
.context("Failed to read voice file")?;
let sample_rate = u32::from_le_bytes(
data[0..4].try_into()
.context("Invalid header length")?
);
에러 복구
예시: 네트워크 요청 재시도
async fn fetch_with_retry(url: &str) -> anyhow::Result<String> {
const MAX_RETRIES: u32 = 3;
for attempt in 1..=MAX_RETRIES {
match fetch(url).await {
Ok(data) => return Ok(data),
Err(e) if attempt < MAX_RETRIES => {
eprintln!("Attempt {} failed: {}, retrying...", attempt, e);
tokio::time::sleep(Duration::from_secs(2_u64.pow(attempt))).await;
}
Err(e) => return Err(e).context("Failed after all retries"),
}
}
unreachable!()
}
결론
Rust 에러 처리는 엄격하지만, 그 덕분에 런타임 에러가 줄어든다.
애플리케이션: anyhow 사용
라이브러리: thiserror로 명확한 타입 정의
복구 가능한 에러: Result<T, E>
복구 불가능한 에러: panic! (드물게)
프로덕션에서 panic이 터지는 건 최악이다. Rust의 Result 타입을 믿고 사용하자.