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
및Error
trait 구현이 더 이상 필요하지 않습니다. 이를 통해 상용구 코드가 크게 줄고 코드 가독성이 향상됩니다. - 정의와 함께 있는 오류 메시지:
thiserror
를 사용하면 오류 정의 바로 옆에 오류 메시지를 작성할 수 있습니다. 이렇게 하면 코드가 더 체계적이고 찾고 수정하기 쉬워집니다. - 유지 관리성 향상: 오류 타입을 추가하거나 제거해야 하는 경우 enum 정의를 수정하고 오류 메시지를 업데이트하기만 하면 되며, 코드의 다른 부분을 변경할 필요가 없습니다.
따라서 오류 타입과 시나리오가 더 복잡해질수록 thiserror
사용의 이점이 더욱 분명해집니다.
Rust 프로젝트 호스팅을 위한 최고의 선택, Leapcell입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다중 언어 지원
- Node.js, Python, Go 또는 Rust로 개발하세요.
무제한 프로젝트를 무료로 배포
- 사용량에 대해서만 비용을 지불하세요. 요청이 없으면 요금이 부과되지 않습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼만 지불하세요.
- 예: $25로 평균 응답 시간 60ms에서 694만 건의 요청을 지원합니다.
간소화된 개발자 경험
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
간편한 확장성 및 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 확장.
- 운영 오버헤드가 전혀 없으므로 빌드에만 집중하세요.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ