Rust의 Result 타입으로 에러 핸들링 개선하기
배경
사이드 프로젝트로 CLI 도구를 만들면서 Rust를 본격적으로 사용하게 됐다. Node.js에서는 try-catch나 Promise의 reject로 에러를 처리했는데, Rust는 접근 방식이 달랐다.
Result 타입의 기본
Rust는 에러를 Result<T, E> 타입으로 표현한다. 성공 시 Ok(T), 실패 시 Err(E)를 반환하는 구조다.
use std::fs::File;
use std::io::Read;
fn read_config(path: &str) -> Result<String, std::io::Error> {
let mut file = File::open(path)?;
let mut contents = String::new();
file.read_to_string(&mut contents)?;
Ok(contents)
}
? 연산자가 핵심이다. 에러 발생 시 즉시 반환하고, 성공 시 값을 unwrap한다. try-catch보다 깔끔하다.
패턴 매칭으로 분기 처리
호출부에서는 match를 사용해 에러를 처리했다.
match read_config("config.json") {
Ok(content) => println!("Config loaded: {}", content),
Err(e) => eprintln!("Failed to read config: {}", e),
}
커스텀 에러 타입
프로젝트가 커지면서 여러 에러 타입을 통합할 필요가 생겼다. thiserror 크레이트를 활용했다.
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Parse error: {0}")]
Parse(String),
}
소감
컴파일 타임에 에러 처리를 강제하니 런타임 오류가 현저히 줄었다. 초반에는 번거로웠지만, 코드 신뢰도가 높아지는 게 체감됐다. Node.js 프로젝트에서도 이런 명시성이 그립다.