러스트에서 오류 처리를 더욱 견고하게 하려면 표준 라이브러리에서 제공하는 Result<T, E> 패턴뿐만 아니라 프로젝트 상황에 맞는 커스텀 오류 타입을 정의하는 방식을 고려해볼 수 있다. 커스텀 오류 타입을 정의하면 발생할 수 있는 오류를 의미적으로 더 명확히 표현하고, 오류 발생 원인을 호출자에게 구체적으로 전달할 수 있으며, 오류를 좀 더 유연하게 핸들링할 수 있다는 장점이 있다.
가장 단순한 형태의 커스텀 오류 타입은 열거형(enum)으로 정의하고, Debug, Display, 그리고 std::error::Error 트레이트를 구현하는 것이다. 예를 들어 입출력과 정수 파싱 중에 생기는 오류를 하나의 타입으로 다루고 싶다면 다음처럼 작성할 수 있다.
위 코드에서 MyError는 입출력 오류, 정수 파싱 오류, 그 밖의 임의의 데이터 유효성 오류를 하나의 타입으로 표현한다. Debug는 #[derive(Debug)]로 자동 구현하고, Display는 구체적인 오류 메시지를 커스터마이징하기 위해 수동으로 구현한다. std::error::Error 트레이트를 구현할 때는 source 메서드를 통해 내부에 포함된 다른 오류를 반환하도록 할 수 있다. 이를 통해 오류 체인(chaining)이 가능해지고, 외부에서 오류를 디버깅하거나 로그를 분석할 때 추가 정보를 얻기 쉽다.
러스트 표준 라이브러리의 From 트레이트를 구현하면 기존의 오류 타입을 MyError 타입으로 쉽게 전환할 수 있다. 예를 들어 입출력 오류를 MyError로 손쉽게 변환하고 싶다면 다음과 같이 구현한다.
이런 식으로 From 트레이트가 구현되어 있으면 ? 연산자를 사용할 때 기존 오류 타입이 자동으로 MyError로 변환된다. 예를 들어 파일 읽기와 정수 파싱을 동시에 수행하는 함수를 작성한다고 해보자.
이 함수는 내부적으로 표준 라이브러리의 std::fs::read_to_string이 std::io::Error를 반환하더라도, From<std::io::Error>가 구현되어 있으므로 ? 연산자가 이를 자동으로 MyError::Io 형태로 변환한다. 정수 파싱에 실패할 경우에도 같은 방식으로 ParseIntError가 MyError::ParseInt가 되어 반환된다.
커스텀 오류 타입을 좀 더 편리하게 정의하기 위해서는 thiserror 크레이트를 사용할 수도 있다. 이를 이용하면 열거형을 간결하게 작성하고, Display와 Error 구현부도 자동으로 제공받을 수 있다. 예시는 다음과 같다.
thiserror::Error 매크로를 사용하면 Display 구현을 위해 필요한 문자열을 #[error(...)] 어트리뷰트로 작성하기만 하면 된다. 이때 #[from] 어트리뷰트를 적절히 활용하면 From 트레이트를 자동으로 구현할 수 있어 ? 연산자와 함께 사용하기 매우 편리하다.
커스텀 오류 타입을 정의하는 이유는 프로젝트의 오류 상황을 더욱 명확히 표현하고, 에러 체인 등을 통해 추적 가능성을 높이기 위함이다. 단순히 문자열만 반환하는 대신 열거형으로 오류를 구체화하면 호출자 입장에서도 오류를 세밀하게 구분해 핸들링할 수 있다. 예를 들어 MyError::InvalidData 변 variant를 만나면 사용자는 입력값이 잘못되었음을 즉시 파악해 입력 재시도를 유도할 수 있고, MyError::Io가 발생하면 외부 시스템 문제를 의심해볼 수 있다.
또한 커스텀 오류 타입을 사용할 때는 가능하면 소스 오류를 그대로 보존해주어야 한다. 예제 코드에서 보았듯이 source 메서드나 From 트레이트 구현을 통해 원본 오류를 담아주면 나중에 문제 분석이 쉬워진다. 예를 들어 로그를 남길 때 구체적인 IO 오류 유형(예: 권한 부족, 파일 없음 등)을 함께 기록할 수 있게 되어 유지보수가 좋아진다.
러스트 생태계에서 오류 처리는 여러 방법이 공존한다. 표준 라이브러리의 std::error::Error만 잘 활용해도 충분하지만, 코드 양이 많아지면 thiserror와 같은 서드파티 크레이트가 큰 도움이 된다. 반면 anyhow는 주로 애플리케이션 레벨에서 오류 전파를 단순화하기 위해 사용되고, 구체적인 오류 원인을 처리해야 할 때는 적합하지 않을 수 있다. 커스텀 오류 타입을 정의해두면 로직 계층에서 별도의 분기를 쉽게 처리할 수 있고, anyhow를 사용하는 상위 계층에서는 이 커스텀 오류를 포장하여 최종 사용자에게는 단순한 메시지만 노출하도록 하는 혼합 전략도 많이 쓰인다.
결론적으로, 커스텀 오류 타입 정의는 러스트 애플리케이션에서 오류 상황을 체계적으로 관리하기 위한 필수적인 기법이다. 간단한 프로토타입에서는 문자열 기반의 오류 처리만으로도 충분할 수 있지만, 코드가 복잡해지고 모듈 경계가 명확해질수록 커스텀 오류 타입이 주는 이점은 커진다. 열거형에 다양한 오류 항목을 정의하고, Display와 std::error::Error를 올바르게 구현한 뒤, From 트레이트나 thiserror로 전환 로직을 단순화하면, 이후 오류를 추적하고 수정하는 과정이 훨씬 수월해진다. 이러한 접근은 코드의 가독성과 유지보수성을 모두 높일 수 있는 좋은 방법이다.