Rust로 CLI 도구 만들면서 배운 에러 처리 패턴

배경

팀에서 사용하던 Node.js 기반 배포 CLI가 의존성 관리와 실행 속도 문제로 불편함이 있었다. 최근 Rust를 학습하면서 이 도구를 재작성해보기로 했다.

에러 처리 구조

초기에는 모든 함수에서 Result<T, Box<dyn Error>>를 반환했는데, 에러 컨텍스트 추가가 번거로웠다. anyhow 크레이트를 도입하니 훨씬 간결해졌다.

use anyhow::{Context, Result};
use std::fs;

fn read_config(path: &str) -> Result<Config> {
    let content = fs::read_to_string(path)
        .context(format!("Failed to read config: {}", path))?;
    
    let config: Config = serde_json::from_str(&content)
        .context("Invalid JSON format")?;
    
    Ok(config)
}

context()로 에러에 맥락을 추가할 수 있어서 디버깅이 편했다. 특히 파일 경로나 API 엔드포인트 같은 정보를 에러 메시지에 포함시키기 좋았다.

커스텀 에러 타입

도메인별로 명확한 에러 구분이 필요한 경우는 thiserror를 사용했다.

use thiserror::Error;

#[derive(Error, Debug)]
pub enum DeployError {
    #[error("Environment {0} not found")]
    EnvNotFound(String),
    
    #[error("Build failed: {0}")]
    BuildFailed(String),
    
    #[error("Network error")]
    Network(#[from] reqwest::Error),
}

이렇게 하니 호출부에서 에러 종류별로 다른 처리를 할 수 있었다.

결과

Node.js 버전은 cold start에 200ms 정도 걸렸는데, Rust 버전은 10ms 이하로 줄었다. 단일 바이너리로 배포되니 node_modules 관리도 필요 없어졌다.

타입 시스템과 컴파일러 덕분에 런타임 에러도 확실히 줄었다. 다만 개발 속도는 아직 Node.js가 더 빠른 편이라 상황에 맞게 선택해야 할 것 같다.