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가 더 빠른 편이라 상황에 맞게 선택해야 할 것 같다.