IntoResponse를 통한 Axum/Actix Web에서의 우아한 오류 처리
James Reed
Infrastructure Engineer · Leapcell

소개
웹 서비스의 세계에서 강력한 오류 처리는 매우 중요합니다. 잘못된 요청, 데이터베이스 문제 또는 내부 서버 오류 등 문제가 발생하면 서버는 이를 클라이언트에게 명확하고 효과적으로 전달해야 합니다. 이는 일반적으로 적절한 HTTP 상태 코드와 설명적인 오류 메시지를 반환하는 것을 포함합니다. 타입 안전성과 오류 처리 기능으로 유명한 Rust 언어에서 Result 열거형은 오류 전파를 위한 기본 빌딩 블록입니다. 그러나 Axum 또는 Actix Web과 같은 웹 프레임워크의 핸들러 함수에서 단순히 Result를 반환하는 것이 사용자 친화적인 HTTP 응답으로 직접 변환되지는 않습니다. 여기서 IntoResponse 트레잇이 필수적이 됩니다. 이를 통해 애플리케이션의 Result 타입, 특히 Err 변형을 잘 구조화된 HTTP 오류 응답으로 원활하게 매핑하여 개발자 경험과 API 명확성을 모두 향상시킬 수 있습니다. 이 강력한 메커니즘이 웹 서비스의 오류 처리를 향상시키는 데 어떻게 작동하는지 살펴보겠습니다.
핵심 개념 이해
구체적인 내용에 들어가기 전에 논의의 핵심이 되는 몇 가지 용어를 명확히 해보겠습니다.
Result<T, E>: 성공하거나 실패할 수 있는 작업에 대한 Rust의 표준 열거형입니다.Ok(T)는 값T로 성공을 나타내고Err(E)는 오류 값E로 실패를 나타내는 두 가지 변형이 있습니다.IntoResponse트레잇: Axum과 Actix Web 모두(약간 다른 이름인 Axum의IntoResponse와 Actix Web의Responder를 사용하지만 유사한 목적을 수행함) 제공하는 이 트레잇은 타입이 HTTP 응답으로 변환될 수 있는 방법을 정의합니다.IntoResponse를 구현하는 모든 타입은 웹 핸들러에서 직접 반환될 수 있습니다.- HTTP 상태 코드: HTTP 요청의 결과를 나타내는 표준 세 자리 숫자입니다. 오류의 경우 일반적인 코드는
400 Bad Request,401 Unauthorized,403 Forbidden,404 Not Found,500 Internal Server Error등입니다. - JSON (JavaScript Object Notation): 웹 서비스에서 클라이언트로 구조화된 오류 메시지를 보내는 데 자주 사용되는 가벼운 데이터 교환 형식입니다.
우아한 오류 변환의 원리
핵심 아이디어는 사용자 지정 오류 타입에 대해 IntoResponse 트레잇을 구현하는 것입니다. 핸들러 함수가 Result<T, E>를 반환하고 Err(e)로 평가되면 웹 프레임워크는 해당 E 타입에 대한 IntoResponse 구현을 찾습니다. 발견되면 해당 구현을 사용하여 오류를 적절한 HTTP 응답으로 변환합니다. 이를 통해 오류-HTTP-응답 매핑 로직을 중앙 집중화하여 핸들러 함수를 깨끗하게 유지하고 비즈니스 로직에 집중할 수 있습니다.
Axum 및 Actix Web 각각에 대한 예제를 통해 이를 설명해 보겠습니다.
Axum 구현
Axum은 오류 처리를 위해 IntoResponse 트레잇을 효과적으로 활용합니다. 사용자 지정 오류 열거형을 정의한 다음 이에 대해 IntoResponse를 구현합니다.
use axum::{ http::StatusCode, response::{IntoResponse, Response}, Json, }; use serde::Serialize; use thiserror::Error; // 오류 타입 파생을 위한 인기 있는 크레이트 // 1. 사용자 지정 오류 열거형 정의 #[derive(Error, Debug)] pub enum AppError { #[error("Invalid input data: {0}")] ValidationError(String), #[error("Resource not found: {0}")] NotFound(String), #[error("Database error: {0}")] DatabaseError(#[from] sqlx::Error), // DB 오류와의 통합 예시 #[error("Internal server error")] InternalServerError, } // 2. 표준화된 HTTP 오류 본문용 구조체 정의 #[derive(Serialize)] struct ErrorResponse { code: u16, message: String, details: Option<String>, } // 3. 사용자 지정 오류 열거형에 대한 IntoResponse 구현 impl IntoResponse for AppError { fn into_response(self) -> Response { let (status, error_message) = match self { AppError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), AppError::DatabaseError(err) => { eprintln!("Database error: {:?}", err); // 내부 오류 로깅 (StatusCode::INTERNAL_SERVER_ERROR, "Database operation failed".to_string()) }, AppError::InternalServerError => (StatusCode::INTERNAL_SERVER_ERROR, "An unexpected error occurred".to_string()), }; let body = Json(ErrorResponse { code: status.as_u16(), message: error_message, details: None, // 필요한 경우 여기에 더 많은 세부 정보 추가 가능 }); (status, body).into_response() } } // 예제 Axum 핸들러 async fn create_user() -> Result<Json<String>, AppError> { // 일부 유효성 검사 로직 시뮬레이션 let is_valid = false; if !is_valid { return Err(AppError::ValidationError("Username cannot be empty".to_string())); } // 데이터베이스 작업 시뮬레이션 let db_success = false; if !db_success { // 실제 앱에서는 이것이 실제 sqlx::Error가 될 것입니다. return Err(AppError::DatabaseError(sqlx::Error::RowNotFound)); } Ok(Json("User created successfully".to_string())) } // 이것을 실행하려면: // async fn main() { // let app = axum::Router::new().route("/users", axum::routing::post(create_user)); // let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); // axum::serve(listener, app).await.unwrap(); // }
이 Axum 예제에서:
AppError를 정의하여 애플리케이션별 다양한 오류를 캡슐화합니다.ErrorResponse는 클라이언트에 전송되는 오류 메시지에 대한 일관된 구조를 제공합니다.impl IntoResponse for AppError블록에는 핵심 로직이 포함되어 있습니다. 각AppError변형은 적절한StatusCode와 오류 메시지로 매핑되며, 이는 JSON으로 직렬화되어 HTTP 응답 본문으로 반환됩니다.create_user핸들러는 이제 단순히Result<_, AppError>를 반환할 수 있으며,Err(AppError::...)가 반환되면 Axum은 자동으로into_response를 호출합니다.
Actix Web 구현
Actix Web은 유사한 기능을 위해 Responder 트레잇을 사용합니다.
use actix_web::{ dev::HttpResponseBuilder, // 사용자 지정 응답을 구축하기 위해 http::StatusCode, web::Json, HttpResponse, ResponseError, // 구현할 트레잇 }; use serde::Serialize; use thiserror::Error; // 1. 사용자 지정 오류 열거형 정의 #[derive(Error, Debug)] pub enum ServiceError { #[error("Validation failed: {0}")] ValidationFailed(String), #[error("Authentication required")] Unauthorized, #[error("Database issue: {0}")] DbError(#[from] sqlx::Error), #[error("Something went wrong")] InternalError, } // 2. 표준화된 HTTP 오류 본문용 구조체 정의 #[derive(Serialize)] struct ApiError { status: u16, message: String, } // 3. 사용자 지정 오류 열거형에 대한 ResponseError 구현 impl ResponseError for ServiceError { fn status_code(&self) -> StatusCode { match *self { ServiceError::ValidationFailed(_) => StatusCode::BAD_REQUEST, ServiceError::Unauthorized => StatusCode::UNAUTHORIZED, ServiceError::DbError(_) => StatusCode::INTERNAL_SERVER_ERROR, ServiceError::InternalError => StatusCode::INTERNAL_SERVER_ERROR, } } fn error_response(&self) -> HttpResponse { let status = self.status_code(); let error_message = match self { ServiceError::ValidationFailed(msg) => msg.clone(), ServiceError::Unauthorized => "Authentication failed".to_string(), ServiceError::DbError(err) => { eprintln!("Actix DB error: {:?}", err); // 내부 오류 로깅 "Database error occurred".to_string() }, ServiceError::InternalError => "An unexpected error happened".to_string(), }; HttpResponseBuilder::new(status).json(ApiError { status: status.as_u16(), message: error_message, }) } } // 예제 Actix Web 핸들러 async fn get_item() -> Result<Json<String>, ServiceError> { let item_id_exists = false; // 항목 없음 시뮬레이션 if !item_id_exists { return Err(ServiceError::ValidationFailed( "Item ID is missing or invalid".to_string(), )); } // 권한 부여 확인 시뮬레이션 let is_authorized = false; if !is_authorized { return Err(ServiceError::Unauthorized); } Ok(Json("Item details".to_string())) } // 이것을 실행하려면: // #[actix_web::main] // async fn main() -> std::io::Result<()> { // use actix_web::{web, App, HttpServer}; // HttpServer::new(|| { // App::new().route("/items", web::get().to(get_item)) // }) // .bind("127.0.0.1:8080")? // .run() // .await // }
Actix Web 예제에서:
ResponseError를 구현하는ServiceError를 정의합니다.ResponseError트레잇에는status_code와error_response메서드가 필요합니다.status_code는 HTTP 상태를 제공하고error_response는 종종 JSON 본문을 포함하는 전체HttpResponse객체를 구성합니다.get_item과 같은 핸들러 함수는Result<_, ServiceError>를 반환할 수 있으며,actix-web은ServiceError를HttpResponse로 자동 변환하여 처리합니다.
애플리케이션 시나리오 및 모범 사례
- 중앙 집중식 오류 처리: 이 패턴은 다양한 애플리케이션 오류가 HTTP 응답으로 번역되는 방법에 대한 단일 진실 공급원을 촉진합니다. 이를 통해 API는 더 일관되고 클라이언트가 이해하기 쉬워집니다.
- 가독성: 핸들러 함수는
Result만 반환하므로 깨끗하게 유지됩니다. 매핑 로직은IntoResponse/ResponseError구현 내에 캡슐화됩니다. - 맥락적 로깅:
DatabaseError및DbError사례에서 볼 수 있듯이 기본 내부 오류 세부 정보(예: 스택 추적, 특정 데이터베이스 오류 메시지)를 로깅하면서 클라이언트에게는 더 일반적이고 안전한 메시지를 반환할 수 있습니다. 이는 민감한 정보를 노출하지 않고 디버깅하는 데 중요합니다. - 사용자 지정 오류 페이로드: 오류 응답의 JSON 구조를 완전히 제어할 수 있어 클라이언트가 쉽게 파싱할 수 있는 풍부하고 설명적인 오류 메시지를 제공할 수 있습니다.
- 오류 집계:
thiserror를 사용하여 다른 모듈이나 타사 크레이트(#[from]사용)의 오류를 포함하는 복잡한 오류 열거형을 쉽게 만들어 오류 처리를 더욱 중앙 집중화합니다.
결론
사용자 지정 오류 타입에 대해 IntoResponse (Axum) 또는 ResponseError (Actix Web) 트레잇을 구현함으로써 Rust 웹 애플리케이션은 매우 우아하고 유지 관리하기 쉬운 오류 처리를 달성할 수 있습니다. 이 패턴은 핸들러 함수에서 반환된 Result 타입이 의미 있는 HTTP 오류 응답으로 우아하게 변환되도록 보장하여 클라이언트에게 일관된 API 경험을 제공하는 동시에 애플리케이션 로직을 깨끗하고 집중적으로 유지합니다. Rust에서 강력하고 개발자 친화적인 웹 서비스를 구축하기 위한 기본 관행입니다.

