Thiserror: Rust의 효율적 오류 관리
Grace Collins
Solutions Engineer · Leapcell

오류 처리
프로그래밍에서 오류 처리는 매우 중요한 부분입니다. Rust에서는 오류 처리에 Result 및 Option 타입을 자주 사용합니다. 그러나 때로는 사용자 정의 오류 타입을 생성해야 합니다. 이때 thiserror 크레이트가 유용하며, 코드를 크게 단순화합니다. 이 글의 마지막 부분에는 thiserror를 사용하는 경우와 사용하지 않는 경우의 비교가 있습니다.
thiserror 크레이트 개요
thiserror 크레이트의 주요 목표는 Rust에서 사용자 정의 오류의 생성 및 처리를 단순화하는 것입니다. 프로젝트에서 thiserror를 사용하려면 먼저 Cargo.toml에 추가하십시오.
[dependencies] thiserror = "1.0"
사용자 정의 오류 생성
thiserror 크레이트는 Rust의 derive 매크로와 사용자 정의 속성을 결합하여 개발자에게 사용자 정의 오류 유형을 빠르게 생성할 수 있는 기능을 제공합니다.
예시:
use thiserror::Error; // 사용자 정의 오류 타입 정의 #[derive(Error, Debug)] pub enum MyError { // DataNotFound 오류에 대한 설명 #[error("데이터를 찾을 수 없습니다")] DataNotFound, // InvalidInput 오류에 대한 설명 #[error("잘못된 입력입니다")] InvalidInput, } // 사용자 정의 오류 사용 방법을 보여주는 예시 함수 fn search_data(query: &str) -> Result<(), MyError> { if query.is_empty() { // 쿼리가 비어 있을 때 InvalidInput 오류 반환 return Err(MyError::InvalidInput); } // 실제 데이터 쿼리 로직은 여기에서 생략됩니다. // ... // 데이터를 찾을 수 없을 때 DataNotFound 오류 반환 Err(MyError::DataNotFound) }
여기서 MyError는 우리가 정의한 사용자 정의 오류 enum입니다. 각 변수는 오류가 발생할 때 표시될 메시지를 제공하는 #[error("...")] 속성으로 주석 처리됩니다.
중첩된 오류
오류 체이닝을 통해 기본 라이브러리 또는 함수에서 전파된 오류를 캡처하고 이에 응답할 수 있습니다. thiserror는 오류가 다른 오류로 인해 발생했음을 지정하는 방법을 제공합니다.
예시:
use std::io; use thiserror::Error; // 사용자 정의 오류 타입 정의 #[derive(Error, Debug)] pub enum MyError { // IoError에 대한 설명, 중첩된 io::Error 포함 #[error("I/O 오류가 발생했습니다")] IoError(#[from] io::Error), } // 중첩된 오류 사용 방법을 보여주는 예시 함수 fn read_file(file_path: &str) -> Result<String, MyError> { // fs::read_to_string이 오류를 반환하면 MyError::from을 사용하여 MyError::IoError로 변환합니다. std::fs::read_to_string(file_path).map_err(MyError::from) }
#[from] 속성은 io::Error를 자동으로 MyError::IoError로 변환할 수 있음을 나타냅니다.
동적 오류 메시지
동적 오류 메시지를 사용하면 런타임 데이터를 기반으로 오류 메시지를 생성할 수 있습니다.
예시:
use thiserror::Error; // 사용자 정의 오류 타입 정의 #[derive(Error, Debug)] pub enum MyError { // FailedWithCode에 대한 설명, 여기서 {0}은 실제 코드 값으로 동적으로 대체됩니다. #[error("코드 {0}으로 실패했습니다")] FailedWithCode(i32), } // 동적 오류 메시지 사용 방법을 보여주는 예시 함수 fn process_data(data: &str) -> Result<(), MyError> { let error_code = 404; // 일부 계산된 오류 코드 // 동적 error_code를 사용하여 FailedWithCode 오류 생성 Err(MyError::FailedWithCode(error_code)) }
라이브러리 간 및 모듈 간 오류 처리
thiserror는 다른 오류 타입에서의 자동 변환도 지원합니다. 이는 모듈 또는 라이브러리 간 오류 처리에 특히 유용합니다.
예시:
use thiserror::Error; // 다른 라이브러리에서 가져온 시뮬레이션된 오류 타입 #[derive(Debug, Clone)] pub struct OtherLibError; // 사용자 정의 오류 타입 정의 #[derive(Error, Debug)] pub enum MyError { // OtherError에 대한 설명, 내부 오류 타입에서 직접 상속 #[error(transparent)] OtherError(#[from] OtherLibError), } // 다른 오류 타입에서 변환하는 방법을 보여주는 예시 함수 fn interface_with_other_lib() -> Result<(), MyError> { // 다른 라이브러리의 함수 호출... // 해당 함수가 오류를 반환하면 MyError::from을 사용하여 MyError::OtherError로 변환합니다. Err(MyError::from(OtherLibError)) }
#[error(transparent)] 속성은 이 오류가 단순히 다른 오류에 대한 컨테이너 역할을 하며, 해당 오류 메시지가 "소스" 오류에서 직접 상속됨을 의미합니다.
다른 오류 처리 크레이트와의 비교
thiserror는 매우 유용하지만 사용 가능한 유일한 오류 처리 크레이트는 아닙니다. 예를 들어 anyhow는 빠른 프로토타입 제작 및 애플리케이션 개발에 사용되는 또 다른 인기 있는 크레이트입니다. 그러나 thiserror는 보다 유연한 오류 정의 및 패턴 매칭 기능을 제공합니다.
실제 사례
파일을 읽고 파싱하는 것과 관련된 작업을 고려하십시오. 잠재적인 I/O 오류 및 파싱 오류를 처리해야 합니다.
예시:
use std::fs; use thiserror::Error; // 다른 부분에서 가져온 시뮬레이션된 파싱 오류 타입 #[derive(Debug, Clone)] pub struct ParseDataError; // 사용자 정의 오류 타입 정의 #[derive(Error, Debug)] pub enum MyError { // IoError에 대한 설명, 중첩된 io::Error 포함 #[error("I/O 오류가 발생했습니다")] IoError(#[from] io::Error), // ParseError에 대한 설명, 중첩된 ParseDataError 포함 #[error("데이터를 파싱하지 못했습니다")] ParseError(#[from] ParseDataError), } // 파일을 읽고 해당 내용 파싱 시도 fn read_and_parse(filename: &str) -> Result<String, MyError> { // 파일 내용 읽기, I/O 오류 발생 가능 let content = fs::read_to_string(filename)?; // 내용 파싱 시도, 파싱 오류 발생 가능 parse_data(&content).map_err(MyError::from) } // 시뮬레이션된 데이터 파싱 함수, 여기서 항상 오류를 반환합니다. fn parse_data(content: &str) -> Result<String, ParseDataError> { Err(ParseDataError) } // 위의 오류 처리 로직 사용 방법을 보여주는 메인 함수 fn main() { match read_and_parse("data.txt") { Ok(data) => println!("Data: {}", data), Err(e) => eprintln!("Error: {}", e), } }
비교: thiserror 사용 vs thiserror 미사용
여러 소스에서 발생하는 여러 가지 가능한 오류와 관련된 더 복잡한 예를 고려해 보겠습니다.
원격 API에서 데이터를 가져온 다음 해당 데이터를 데이터베이스에 저장해야 하는 애플리케이션을 작성한다고 가정해 보겠습니다. 각 단계는 실패하고 다른 유형의 오류를 반환할 수 있습니다.
thiserror를 사용하지 않는 코드:
use std::fmt; #[derive(Debug)] enum DataFetchError { HttpError(u16), Timeout, InvalidPayload, } impl fmt::Display for DataFetchError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::HttpError(code) => write!(f, "HTTP error with code: {}", code), Self::Timeout => write!(f, "Data fetching timed out"), Self::InvalidPayload => write!(f, "Invalid payload received"), } } } impl std::error::Error for DataFetchError {} #[derive(Debug)] enum DatabaseError { ConnectionFailed, WriteFailed(String), } impl fmt::Display for DatabaseError { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::ConnectionFailed => write!(f, "Failed to connect to database"), Self::WriteFailed(reason) => write!(f, "Failed to write to database: {}", reason), } } } impl std::error::Error for DatabaseError {}
thiserror를 사용하는 코드:
use thiserror::Error; #[derive(Debug, Error)] enum DataFetchError { #[error("HTTP error with code: {0}")] HttpError(u16), #[error("Data fetching timed out")] Timeout, #[error("Invalid payload received")] InvalidPayload, } #[derive(Debug, Error)] enum DatabaseError { #[error("Failed to connect to database")] ConnectionFailed, #[error("Failed to write to database: {0}")] WriteFailed(String), }
분석
- 코드 감소: 각 오류 타입에 대해 별도의
Display및Errortrait 구현이 더 이상 필요하지 않습니다. 이를 통해 상용구 코드가 크게 줄고 코드 가독성이 향상됩니다. - 정의와 함께 있는 오류 메시지:
thiserror를 사용하면 오류 정의 바로 옆에 오류 메시지를 작성할 수 있습니다. 이렇게 하면 코드가 더 체계적이고 찾고 수정하기 쉬워집니다. - 유지 관리성 향상: 오류 타입을 추가하거나 제거해야 하는 경우 enum 정의를 수정하고 오류 메시지를 업데이트하기만 하면 되며, 코드의 다른 부분을 변경할 필요가 없습니다.
따라서 오류 타입과 시나리오가 더 복잡해질수록 thiserror 사용의 이점이 더욱 분명해집니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다중 언어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불하세요. 요청이 없으면 요금이 부과되지 않습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하세요.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장.
- 운영 오버헤드가 전혀 없으므로 빌드에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ



