Rust 웹 서비스에서 기본 결과 처리부터 견고한 오류 관리까지
Daniel Hayes
Full-Stack Engineer · Leapcell

소개
Rust의 세계에서 견고성과 신뢰성은 가장 중요합니다. 웹 서비스를 구축할 때 오류를 우아하게 처리하는 것은 단순한 모범 사례가 아니라 안정적이고 사용자 친화적인 애플리케이션을 만드는 데 필수적입니다. 개발자는 종종 성공(Ok) 또는 실패(Err)를 나타내는 강력한 열거형인 Rust의 Result 타입으로 여정을 시작합니다. 이는 많은 시나리오에 완벽하게 적합하지만, 웹 서비스가 복잡해짐에 따라 일반적인 오류 타입이나 기본 String 오류에만 의존하면 코드가 복잡해지고 디버깅이 어려워지며 클라이언트에게 의미 있는 응답을 제공하지 못하게 될 수 있습니다. 이 문서는 간단한 Result 처리에서 시작하여 사용자 지정 오류 타입을 만들고, 궁극적으로 IntoResponse 트레이트를 사용하여 이러한 사용자 지정 오류를 웹 프레임워크와 통합하여 API가 성공과 실패의 언어를 명확하고 표현력 있게 구사하도록 보장하는 여정을 시작할 것입니다.
핵심 개념
구현 세부 사항을 자세히 살펴보기 전에 Rust에서 견고한 오류 관리를 뒷받침하는 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
Result<T, E>: 성공하거나 실패할 수 있는 계산을 나타내는 Rust 표준 라이브러리 열거형입니다.T는 성공적인 값의 타입이고E는 오류 값의 타입입니다. 이 타입은 개발자가 잠재적 실패를 명시적으로 처리하도록 강제하며, 이는 Rust 안전성의 기반입니다.Error트레이트: 오류를 나타내는 타입에 대한 Rust 표준 라이브러리의 기본 트레이트입니다. 이 트레이트를 구현하면 사용자 지정 오류 타입이?연산자 전파 및 오류 보고 라이브러리와 같은 다른 오류 처리 메커니즘과 상호 운용할 수 있습니다.Debug및Display를 구현해야 하며, 오류 연결을 위한 선택적source메서드가 필요합니다.From<T> for E트레이트: 이 트레이트는 한 타입T에서 다른 타입E로의 오류 없는 변환을 가능하게 합니다. 오류 처리에서 더 구체적인 오류 타입을 더 일반적인 사용자 지정 오류 열거형으로 변환하여 오류 전파를 더 쉽게 만드는 데 자주 사용됩니다.IntoResponse트레이트 (웹 프레임워크): 많은 Rust 웹 프레임워크(예: Axum, Actix Web, Rocket)는 사용자 지정 타입이 HTTP 응답으로 변환될 수 있도록IntoResponse또는 유사한 이름의 트레이트를 제공합니다. 이는 사용자 지정 오류 타입이 HTTP 상태 코드, 헤더 및 클라이언트에 다시 전송되는 본문을 직접 제어할 수 있도록 하므로 오류 처리에 중요합니다.
간단한 결과에서 사용자 지정 오류로
ID별로 사용자를 가져오는 간단한 API 엔드포인트를 구축한다고 상상해 봅시다. 처음에는 String 오류가 있는 기본 Result를 사용할 수 있습니다.
// 기본 Result 처리 async fn get_user_simple(user_id: u32) -> Result<String, String> { if user_id % 2 == 0 { Ok(format!("User {user_id} found!")) } else { Err("User not found".to_string()) } }
이것은 작동하지만 String 오류에는 구조가 부족합니다. 애플리케이션이 성장함에 따라 문자열에만 의존하여 "사용자를 찾을 수 없음"과 "데이터베이스 연결 실패"를 구별하는 것은 불안정합니다. 사용자 지정 오류 타입이 빛을 발하는 곳입니다.
사용자 지정 오류 열거형 구현
고유한 오류 조건을 열거하기 위해 enum을 정의할 수 있습니다. 이 열거형을 ?로 전파하고 형식화할 수 있는 올바른 오류 타입으로 만들기 위해 Debug, Display 및 Error 트레이트를 구현합니다.
use std::fmt::{Display, Formatter}; use std::error::Error; #[derive(Debug)] pub enum AppError { UserNotFound(u32), DatabaseError(String), IOError(std::io::Error), InvalidInput(String), // 더 구체적인 오류를 여기에 추가할 수 있습니다. } impl Display for AppError { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { AppError::UserNotFound(id) => write!(f, "User with ID {id} was not found."), AppError::DatabaseError(msg) => write!(f, "Database error: {}", msg), AppError::IOError(err) => write!(f, "IO error: {}", err), AppError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), } } } impl Error for AppError { fn source(&self) -> Option<&(dyn Error + 'static)> { match self { AppError::IOError(err) => Some(err), _ => None, } } } // 서비스 함수에서의 예시 사용 async fn get_user_complex(user_id: u32) -> Result<String, AppError> { if user_id == 0 { return Err(AppError::InvalidInput("User ID cannot be zero".to_string())); } match fetch_user_from_db(user_id).await { Ok(user_data) => Ok(user_data), Err(db_err) => { if db_err.contains("not found") { Err(AppError::UserNotFound(user_id)) } else { Err(AppError::DatabaseError(db_err)) } } } } // 목업 데이터베이스 함수 async fn fetch_user_from_db(user_id: u32) -> Result<String, String> { if user_id % 2 == 0 { Ok(format!("User data for ID {user_id}")) } else { Err("User not found in database".to_string()) } }
이제 우리의 오류 타입은 더 많은 컨텍스트를 담고 있어 디버깅과 오류 처리가 훨씬 명확해집니다.
From을 사용한 원활한 오류 변환
get_user_complex 함수는 여전히 fetch_user_from_db의 String 오류를 AppError::DatabaseError로 수동으로 매핑합니다. 이는 지루해질 수 있습니다. From 트레이트를 활용하여 ? 연산자를 사용하여 호환되는 오류를 AppError 열거형으로 자동으로 변환할 수 있습니다.
다른 외부 서비스 오류 또는 std::io::Error에 대한 AppError 변형이 있다고 가정해 봅시다.
// 더 쉬운 오류 변환을 위한 From 구현 implement From<std::io::Error> for AppError { fn from(err: std::io::Error) -> Self { AppError::IOError(err) } } // 실제 오류 타입을 반환하는 목업 DB가 있다면 변환할 수도 있습니다. // 시연을 위해 파싱 오류를 가정해 봅시다. #[derive(Debug, Display, Error)] #[display(fmt = "Parse error: {}", _0)] pub struct ParseError(String); impl From<ParseError> for AppError { fn from(err: ParseError) -> Self { AppError::InvalidInput(format!("Parsing failed: {}", err)) } } // `io::Error`에 대해 `?`를 이제 사용하는 서비스 함수 async fn read_user_file(file_path: &str) -> Result<String, AppError> { let content = std::fs::read_to_string(file_path)?; Ok(content) }
이것은 오류 전파 로직을 크게 정리하여 성공 경로에 집중할 수 있도록 합니다.
웹 프레임워크와의 통합: IntoResponse 트레이트
웹 서비스의 경우 오류는 단순한 내부 상태가 아니라 클라이언트가 이해할 수 있는 HTTP 응답으로 변환되어야 합니다. 여기에는 적절한 HTTP 상태 코드(예: 404 Not Found, 500 Internal Server Error, 400 Bad Request)와 오류를 설명하는 JSON 본문을 설정하는 것이 포함됩니다. 많은 Rust 웹 프레임워크는 Axum의 IntoResponse와 같은 트레이트를 제공합니다.
AppError를 HTTP 응답으로 직접 변환해 봅시다.
use axum::{ body::Bytes, response::{IntoResponse, Response}, http::StatusCode, Json, }; use serde::Serialize; // 우리 오류 세부 정보를 JSON으로 직렬화하기 위한 것 #[derive(Serialize)] struct ErrorResponse { code: u16, message: String, details: Option<String>, } impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message, details) = match self { AppError::UserNotFound(_) => (StatusCode::NOT_FOUND, self.to_string(), None), AppError::DatabaseError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, "Database operation failed".to_string(), Some(msg)), AppError::IOError(err) => (StatusCode::INTERNAL_SERVER_ERROR, "File system error".to_string(), Some(err.to_string())), AppError::InvalidInput(msg) => (StatusCode::BAD_REQUEST, "Invalid request input".to_string(), Some(msg)), }; // 디버깅을 위해 내부적으로 오류 로깅 eprintln!("{{}}", self); let error_body = Json(ErrorResponse { code: status.as_u16(), message: error_message, details, }); (status, error_body).into_response() } } // 사용자 지정 오류를 사용하는 예시 Axum 핸들러 async fn get_user_handler( axum::extract::Path(user_id): axum::extract::Path<u32>, ) -> Result<Json<String>, AppError> { let user_data = get_user_complex(user_id).await?; Ok(Json(user_data)) } // 실제 Axum 애플리케이션에서는 이 핸들러를 등록합니다. // fn main() { // let app = axum::Router::new().route("/users/:id", axum::routing::get(get_user_handler)); // // ... 서버 실행 // }
이 향상된 예에서:
- 클라이언트에 반환되는 JSON 오류 형식을 표준화하기 위해
ErrorResponse구조체를 정의합니다. AppError에 대해IntoResponse를 구현합니다. 이 구현 내에서 각AppError변형을 적절한 HTTPStatusCode에 매핑하고Json응답 본문을 구성합니다.get_user_handler의?연산자는get_user_complex에서 반환된 모든AppError를IntoResponse호환 오류로 자동 변환하며, Axum은 이를 사용하여 HTTP 응답을 생성합니다.
이 최종 단계는 오류 처리 여정을 완료하여 웹 서비스가 내부 애플리케이션 오류를 잘 구조화되고 클라이언트에 친숙한 HTTP 응답으로 자동 변환할 수 있도록 하여 API를 복원력이 있고 예측 가능하며 즐겁게 상호 작용할 수 있도록 합니다.
결론
간단한 Result 타입을 사용하여 오류를 처리하는 것은 Rust에서 훌륭한 시작이지만, 복잡한 웹 서비스의 경우 사용자 지정 오류 열거형으로 전환하면 필요한 명확성과 구조를 제공할 수 있습니다. Error 및 Display 트레이트를 구현하고 From을 사용하여 원활한 변환을 활용하고 웹 프레임워크에 대해 IntoResponse를 활용함으로써 개발자는 내부 개발을 위해 표현력이 풍부하고 외부 클라이언트와 오류를 효과적으로 통신하도록 완벽하게 맞춤화된 오류 처리 시스템을 구축할 수 있습니다. 이 포괄적인 접근 방식은 혼란스러운 잠재력을 예측 가능하고 우아한 실패로 바꿉니다.

