Rust에서 에러 처리를 체계적으로 다루는 핵심 요소 중 하나가 바로 표준 라이브러리에 정의된 Error 트레이트다. 이 트레이트는 Result<T, E>에서 E로 사용될 수 있는 사용자 정의 에러 타입이나 표준 라이브러리 에러 타입이 지켜야 할 일종의 약속이다. Error 트레이트는 에러를 디버깅하기 위한 정보와 사용자에게 전달할 사람이 읽을 수 있는 메시지, 그리고 에러의 근본 원인이 되는 소스 에러를 연결할 수 있는 기능을 제공한다.
Error 트레이트는 std::error 모듈에 정의되어 있으며, Debug와 Display 트레이트 구현을 요구한다. 이는 에러를 표준 에러 출력이나 로그 등에 남길 때 필요한 출력 형식을 강제하는 것으로, Rust 에러가 디버깅이나 모니터링 상황에서 이해하기 쉽게 표현되도록 한다. 이를 충족하지 못하면 컴파일 시점에서 에러 타입을 Result의 E로 사용할 수 없게 된다.
사용자 정의 에러 타입에서 Error 트레이트를 구현하려면 Display와 Debug를 모두 구현한 뒤, Error 트레이트 자체에서 source 메서드를 필요에 따라 오버라이딩한다. source 메서드는 에러의 근본 원인이 되는 다른 에러를 반환하도록 하는데, 에러가 여러 단계로 래핑되어 있을 때 최종적으로 어디에서 기원하는지를 추적할 수 있게 한다. 예를 들어 IO 에러가 발생했고, 이를 사용자 정의 에러로 다시 래핑했다면, source 메서드를 통해 내부 IO 에러를 가리키도록 구현할 수 있다.
아래 예시는 간단한 사용자 정의 에러 타입을 정의하고 Error 트레이트를 구현하는 방법을 보인다.
usestd::error::Error;usestd::fmt;#[derive(Debug)]structMyError{message:String,cause:Option<Box<dynError>>,}implMyError{fnnew(msg:&str)->Self{MyError{message:msg.to_string(),cause:None,}}fnwith_cause(msg:&str,cause:Box<dynError>)->Self{MyError{message:msg.to_string(),cause:Some(cause),}}}implfmt::DisplayforMyError{fnfmt(&self,f:&mutfmt::Formatter<'_>)->fmt::Result{write!(f,"MyError: {}",self.message)}}implErrorforMyError{fnsource(&self)->Option<&(dynError+'static)>{// 내부적으로 보관 중인 에러가 있다면 참조를 돌려준다match&self.cause {Some(err)=>Some(&**err),None=>None,}}}
위 예시에서 MyError 구조체는 message 필드와 cause 필드를 가지고 있으며, cause 필드는 박싱(Box)된 형태로 다른 에러를 담을 수 있게 했다. 이렇게 하면 MyError가 다른 에러를 감싸는 래퍼 역할을 할 수 있고, source 메서드를 통해 원인이 된 에러를 추적할 수 있다. Debug와 Display를 구현해주어야만 Error 트레이트를 온전히 구현할 수 있다. source 메서드는 기본적으로 None을 반환하지만, 위와 같이 에러 체인을 엮고 싶다면 Some(err)를 반환해주면 된다.
Error 트레이트는 주로 Box와 함께 사용되어 다양한 에러 타입을 한 변수로 받아들일 수 있게 한다. 예를 들어, 함수에서 여러 종류의 에러가 발생할 수 있고, 이들을 하나로 통합해 반환하고 싶다면 Result<T, Box>를 사용할 수 있다. 이렇게 하면 해당 함수는 구현 상세에 종속되지 않는 일반화된 형태의 에러를 반환할 수 있어, 재사용성과 추상화가 한층 편리해진다.
위 함수는 파일을 열다가 발생할 수 있는 여러 종류의 IO 에러를 모두 자동으로 Box 형태로 승격해서 반환한다. 어떤 종류의 에러가 터지든지 간에 호출자 입장에서는 Result<String, Box>로 받기 때문에 에러 처리를 한결 단순화할 수 있다.
Rust 표준 라이브러리나 서드파티 라이브러리를 통해 에러를 표현할 때, 대부분 이 Error 트레이트를 구현해놓았으므로 상호 운용성이 높다. ? 연산자는 내부적으로 From 트레이트와 함께 Error 트레이트 구현을 고려해 에러를 자동 변환하는 편리함을 제공한다. Result<T, E>에서 E가 Error를 구현하고 있다면, 다른 Error 구현 타입으로 변환되거나 Box로 승격될 수 있다.
러스트 생태계에는 사용자 정의 에러 구현을 더욱 편리하게 해주는 서드파티 크레이트도 존재한다. 예를 들면 thiserror 같은 매크로 기반 크레이트로 Error 트레이트를 자동으로 구현하고, 에러 메시지와 소스 에러를 처리하는 부담을 줄일 수 있다. anyhow 같은 크레이트는 에러 타입을 구체적으로 정의하지 않고도 에러 문맥을 손쉽게 전달할 수 있도록 도와주므로, 상황에 따라 적절히 선택하면 된다. 그러나 이러한 크레이트의 동작 원리도 결국 Error 트레이트를 중심으로 이뤄진다.
Error 트레이트를 구현할 때에는 다음과 같은 점들을 주의해야 한다. Display 출력 문구가 사용자가 이해하기 쉬운지 검토해야 하고, 디버깅 목적으로 Debug 표현이 유용하게 되어 있는지 고려해야 하며, 정말 필요한 경우가 아니라면 source 메서드로 에러 체인을 지나치게 복잡하게 구성하지 말아야 한다. 에러가 깊게 중첩될수록 추적이 어려워질 수 있기 때문이다. 또한, 에러 타입이 불필요하게 크거나 복잡해지지 않도록 message나 cause 등을 필요한 만큼만 보관하고, 메모리 사용량이나 성능 저하를 유발하지 않게 설계하는 것이 좋다.
Error 트레이트의 근본적인 가치는 다양한 에러 타입을 동일하게 취급하고, 에러에 대한 정보를 최대한 표준화된 방식으로 저장 및 출력하며, 에러의 근원(원인)을 추적할 수 있다는 데 있다. Rust의 엄격한 타입 시스템과 결합하여, 에러 발생과 처리를 명확하고 안전하게 설계할 수 있도록 돕는 핵심 축이므로, 사용자 정의 에러를 제작하거나 라이브러리 에러를 래핑할 때 반드시 숙지하고 적절히 활용하는 것이 좋다.