Rust 에러 처리 패턴 — anyhow vs thiserror, 실전에서 뭘 써야 하나

게시일: 2025년 5월 6일 · 14분 읽기

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);
        }
    }
}

장점:

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 타입을 믿고 사용하자.

iL
ian.lab

실무 개발자입니다. 현장에서 겪은 문제와 해결 과정을 기록합니다. 오류 제보는 연락처로 보내주세요.