Rust 오류 자료형: thiserror 라이브러리 사용법
Rust는 명시적이고 안전한 오류 처리를 중시하는 프로그래밍 언어입니다.
대표적으로 Result<T, E>
타입을 통해 다양한 에러 상황을 타입 시스템으로 포착할 수 있죠.
하지만 실무에서 직접 오류 자료형을 정의하고 Error
트레이트를 구현하다 보면, 반복적인 보일러플레이트 코드 작성에 지치는 경우가 많습니다.
이럴 때 thiserror 라이브러리가 여러분의 구세주가 될 수 있습니다.
Error 트레이트
우선 표준 라이브러리의 Error
트레이트를 직접 구현하는데 필요한 최소한의 코드를 보여드리겠습니다.
아래 ValidationError
열거형은 3개의 변형(variant)로 이루어져있습니다.
우선 Debug
트레이트를 파생시키고, Display
트레이트를 구현하고, 마지막으로 Error
트레이트를 구현해야합니다.
use std::error::Error;
use std::fmt;
#[derive(Debug)]
enum ValidationError {
InvalidEmail,
RequiredField(&'static str),
OutOfRange { min: i32, max: i32, value: i32 },
}
impl fmt::Display for ValidationError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ValidationError::InvalidEmail => write!(f, "Invalid email format"),
ValidationError::RequiredField(field) => write!(f, "Field '{}' is required", field),
ValidationError::OutOfRange { min, max, value } => {
write!(
f,
"Value must be between {} and {}, got {}",
min, max, value
)
}
}
}
}
impl Error for ValidationError {}
위 예제는 최소한의 구현이고 실제로는 Error
트레이트의 메서드까지 구현하게 되는데요.
매번 새로운 오류 자료형을 정의할 때 마다 이렇게 코드를 많이 작성하려면 개발 생산성이 떨어질 수 있겠죠?
thiserror란?
thiserror는 개발자가 오류 자료형을 쉽게 정의할 수 있도록 도와주는 Rust 라이브러리입니다.
Error
트레이트(trait) 구현을 자동화하기 위해서 #[derive(Error)]
매크로를 제공하는데요.
덕분에 오류 메시지, 변환, 체인 구성 등을 매우 간단하게 처리할 수 있습니다.
thiserror 크레이트(crate)는 터미널에서 cargo add
명령어를 사용하거나 Cargo.toml
를 편집하여 설치할 수 있습니다.
$ cargo add thiserror
[dependencies]
thiserror = "1.0"
오류 정의
자 그럼, 지금부터 thiserror을 사용하여 동일한 오류 자료형을 정의해보겠습니다.
use thiserror::Error;
#[derive(Error, Debug)]
enum ValidationError {
#[error("Invalid email format")]
InvalidEmail,
#[error("Field '{0}' is required")]
RequiredField(&'static str),
#[error("Value must be between {min} and {max}, got {value}")]
OutOfRange { min: i32, max: i32, value: i32 },
}
어떤가요? 코드 양이 확 줄고 읽기도 편해졌죠?
변경 사항을 간단히 설명을 드리면,
std::error::Error
대신에thiserror::Error
를 불러옵니다.- 오류 자료형 위에
#[derive(Error)]
매크로를 붙여줍니다. - 열거형의 각 변형 위에
#[error("...")]
속성으로 오류 메시지를 지정합니다. - 필드 값을
{}
또는{0}
,{message}
같은 형식으로 메시지에 삽입할 수 있습니다.
오류 메세지
위와 같이 오류 자료형을 정의하면 Display
트레이트가 자동으로 구현된 효과가 납니다.
fn main() {
let invalid_email = ValidationError::InvalidEmail;
let required_field = ValidationError::RequiredField("password");
let out_of_range = ValidationError::OutOfRange {
min: 1,
max: 100,
value: 101,
};
println!("{}", invalid_email);
println!("{}", required_field);
println!("{}", out_of_range);
}
필드 값을 메시지 안에 자연스럽게 녹여 사용자에게 명확한 피드백을 제공할 수 있게 되었습니다.
Invalid email format
Field 'password' is required
Value must be between 1 and 100, got 101
오류 변환
다른 에러 타입을 감싸는 코드를 직접 작성하면 번거롭지만, #[from]
을 사용하면 자동으로 From
트레이트가 구현되어 손쉽게 변환할 수 있습니다.
use std::fs::File;
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
enum FileError {
#[error("File not found: {filename}")]
NotFound { filename: String },
#[error("Permission denied")]
PermissionDenied,
#[error("IO error")]
Io(#[from] io::Error),
}
fn read_config_file(filename: &str) -> Result<String, FileError> {
use std::io::Read;
match File::open(filename) {
Ok(mut file) => {
let mut contents = String::new();
file.read_to_string(&mut contents)?; // io::Error → FileError::Io로 자동 변환됨
Ok(contents)
}
Err(e) => match e.kind() {
io::ErrorKind::NotFound => Err(FileError::NotFound {
filename: filename.to_string(),
}),
io::ErrorKind::PermissionDenied => Err(FileError::PermissionDenied),
_ => Err(FileError::Io(e)),
},
}
}
오류 체인
#[source]
메크로를 통해서 에러의 원인을 명시할 수 있습니다.
Error::source()
메서드를 통해 체인을 순차적으로 추적할 수 있습니다.
use std::io;
use thiserror::Error;
#[derive(Error, Debug)]
enum DatabaseError {
#[error("Connection failed")]
ConnectionFailed(#[source] io::Error),
#[error("Query failed: {query}")]
QueryFailed {
query: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
}
마치며
지금까지 Rust에서 에러 자료형을 깔끔하게 정의할 수 있게 해주는 강력한 라이브러리인 thiserror에 대해서 알아보았습니다. thiserror를 잘 활용하셔서 보일러플레이트 코드를 최소화하시고 더 생산성있게 에러 처리를 하실 수 있으셨으면 좋겠습니다.