Axum에서 오류 처리

발행: (2026년 5월 3일 AM 02:06 GMT+9)
10 분 소요
원문: Dev.to

Source: Dev.to

Axum은 Tokio와 Hyper 위에 구축된 Rust용 웹 프레임워크입니다.
그 오류‑처리 모델은 단순하고 예측 가능합니다: 모든 오류는 결국 HTTP 응답이 되어야 합니다. 이를 가능하게 하는 핵심 트레이트는 IntoResponse입니다. IntoResponse를 구현하는 모든 것은 핸들러에서 반환될 수 있으며, Axum은 이를 HTTP 응답으로 변환합니다.

1. StatusCode를 직접 반환하기

오류를 표시하는 가장 쉬운 방법은 핸들러에서 StatusCode를 반환하는 것입니다.

use axum::{
    extract::Path,
    http::StatusCode,
    routing::get,
    Router,
};

async fn get_user(Path(id): Path) -> Result {
    if id == 0 {
        return Err(StatusCode::NOT_FOUND);
    }
    Ok(format!("User #{}", id))
}

#[tokio::main]
async fn main() {
    // ✅ Use `{id}` in the route pattern
    let app = Router::new().route("/users/{id}", get(get_user));
    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}
  • 핸들러가 Ok를 반환하면 Axum은 200 OK를 전송합니다.
  • Err(StatusCode)를 반환하면 Axum은 전달된 상태 코드를 전송합니다.

단순히 StatusCode만 반환하면 실패했는지에 대한 설명이 부족하므로, 대부분의 경우 더 많은 정보를 함께 반환하는 것이 좋습니다.

2. (StatusCode, String) 튜플 반환하기

상태 코드와 일반 텍스트 메시지를 포함하는 튜플을 반환할 수 있습니다. 이 튜플은 자동으로 IntoResponse를 구현합니다.

use axum::{
    extract::Path,
    http::StatusCode,
    routing::post,
    Json,
    Router,
};
use serde::Deserialize;

#[derive(Deserialize)]
struct CreateUser {
    username: String,
}

async fn create_user(
    Json(payload): Json,
) -> Result {
    if payload.username.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            "Username cannot be empty".to_string(),
        ));
    }

    Ok(format!("Created user: {}", payload.username))
}

3. 전체 애플리케이션을 위한 단일 오류 enum

실제 프로젝트에서는 보통 모든 가능한 실패 모드를 포괄하는 하나의 오류 타입을 정의하고, 그에 대해 IntoResponse를 구현합니다.

use axum::{
    extract::Path,
    http::StatusCode,
    response::{IntoResponse, Response},
    routing::get,
    Router,
};

/// 애플리케이션 전역 오류 타입
enum AppError {
    NotFound(String),
    Unauthorized,
    InternalError(String),
}

/// `AppError`를 HTTP 응답으로 변환
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            AppError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            AppError::Unauthorized => (
                StatusCode::UNAUTHORIZED,
                "You are not authorized".to_string(),
            ),
            AppError::InternalError(msg) => (StatusCode::INTERNAL_SERVER_ERROR, msg),
        };

        (status, message).into_response()
    }
}

// 커스텀 오류 타입을 사용하는 예시 핸들러
async fn get_document(Path(id): Path) -> Result {
    if id == 99 {
        return Err(AppError::Unauthorized);
    }
    if id > 100 {
        return Err(AppError::NotFound(format!("Document {} not found", id)));
    }
    Ok(format!("Document #{}", id))
}

Pattern: 하나의 오류 타입, 응답으로 변환되는 방식을 정의하는 한 곳.

4. JSON error bodies

APIs는 일반 텍스트 대신 JSON 오류 페이로드를 반환하는 경우가 많습니다. 아래는 이를 구현하는 일반적인 방법입니다.

use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
    Json,
};
use serde::Serialize;

#[derive(Serialize)]
struct ErrorBody {
    error: String,
    code: u16,
}

enum ApiError {
    NotFound(String),
    BadRequest(String),
    Internal,
}

impl IntoResponse for ApiError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
            ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
            ApiError::Internal => (
                StatusCode::INTERNAL_SERVER_ERROR,
                "Something went wrong".to_string(),
            ),
        };

        let body = Json(ErrorBody {
            error: message,
            code: status.as_u16(),
        });

        (status, body).into_response()
    }
}

Resulting response example

{
  "error": "Document 999 not found",
  "code": 404
}

5. ?From을 이용한 오류 전파

실제 코드에서는 하위 레벨 라이브러리에서 발생한 오류를 ? 연산자를 사용해 상위로 전파하고 싶을 때가 많습니다. AppError에 대해 From을 구현하면 변환이 자동으로 이루어집니다.

use std::num::ParseIntError;

enum AppError {
    BadRequest(String),
    Internal(String),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        // Same implementation as before (omitted for brevity)
        unimplemented!()
    }
}

// Convert `ParseIntError` into `AppError`
impl From<ParseIntError> for AppError {
    fn from(err: ParseIntError) -> Self {
        AppError::BadRequest(format!("Invalid number: {}", err))
    }
}

async fn parse_id(Path(raw): Path) -> Result {
    // `?` calls the `From` impl on failure
    let id: u32 = raw.parse()?;
    Ok(format!("Parsed id: {}", id))
}

raw.parse() 뒤의 ?ParseIntError를 자동으로 AppError::BadRequest로 변환하므로, 별도의 수동 변환이 필요하지 않습니다.

6. 빠른 프로토타이핑을 위한 anyhow 사용

세밀한 오류 타입이 필요하지 않을 때, anyhow 크레이트는 편리한 범용 오류 타입을 제공합니다.

use anyhow::Context;
use axum::{
    http::StatusCode,
    response::{IntoResponse, Response},
};

/// Wrapper that implements `IntoResponse` for `anyhow::Error`
struct AnyhowError(anyhow::Error);

impl IntoResponse for AnyhowError {
    fn into_response(self) -> Response {
        (
            StatusCode::INTERNAL_SERVER_ERROR,
            format!("Internal error: {}", self.0),
        )
            .into_response()
    }
}

// Allow any error that can be turned into `anyhow::Error` to be converted
impl<E> From<E> for AnyhowError {
    fn from(err: E) -> Self {
        AnyhowError(err.into())
    }
}

async fn risky_handler() -> Result {
    let data = std::fs::read_to_string("config.txt")
        .context("Failed to read config file")?;
    Ok(data)
}

이제 EInto를 구현하는 모든 Result?를 사용할 수 있습니다.

Source:

7. HandleError 로 Tower 서비스 오류 변환하기

Tower 서비스나 미들웨어가 자체 오류 타입을 반환하는 경우, Axum의 HandleError 레이어를 사용하면 해당 오류를 응답으로 매핑할 수 있습니다.

use axum::{
    error_handling::HandleError,
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
};
use std::convert::Infallible;
use std::time::Duration;
use tower::timeout::TimeoutLayer;

// Example service that may time out
async fn timeout_handler() -> Result {
    Ok("All good".to_string())
}

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route(
            "/",
            get(
                timeout_handler
                    .layer(TimeoutLayer::new(Duration::from_secs(1)))
                    .handle_error(|err| async move {
                        // Convert the timeout error into a response
                        (
                            StatusCode::REQUEST_TIMEOUT,
                            format!("Request timed out: {}", err),
                        )
                            .into_response()
                    }),
            ),
        );

    let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
        .await
        .unwrap();
    axum::serve(listener, app).await.unwrap();
}

HandleError 를 사용하면 서비스의 오류 타입을 IntoResponse 구현으로 변환하는 사용자 정의 로직을 정의할 수 있어, 나머지 코드를 깔끔하게 유지할 수 있습니다.

요약

  • **IntoResponse**는 Rust 오류와 HTTP 응답을 연결하는 다리입니다.
  • StatusCode 또는 (StatusCode, String)와 같이 간단하게 시작하세요.
  • 규모가 큰 애플리케이션에서는 단일 오류 열거형을 정의하고 IntoResponse를 한 번만 구현합니다.
  • API 사용자를 위해 JSON 본문을 반환합니다.
  • From 구현을 사용하면 ? 연산자가 오류를 자동으로 전파하도록 할 수 있습니다.
  • anyhow는 상세한 오류 계층 구조가 필요 없는 프로토타입에 편리합니다.
  • HandleError는 Tower 서비스나 미들웨어에서 발생한 오류를 변환하는 데 도움을 줍니다.

이러한 패턴을 사용하면 어떤 Axum 애플리케이션에서도 견고하고 사용하기 쉬운 오류 처리를 구축할 수 있습니다.

Tower & Axum 오류 처리 예제

use axum::{
    http::StatusCode,
    response::IntoResponse,
    routing::get,
    Router,
};
use std::convert::Infallible;

// A function that may fail
async fn might_fail() -> Result {
    Err("Something broke".to_string())
}

// Wrap the service to handle its errors
let service = tower::service_fn(|_req| async {
    Ok::("ok")
});

let handled = HandleError::new(service, |err: String| async move {
    (StatusCode::INTERNAL_SERVER_ERROR, err)
});

이것은 더 고급이며 주로 서드파티 Tower 컴포넌트를 통합할 때 관련됩니다.

오류 처리 패턴

패턴적합한 경우
Result간단한, 코드 전용 오류
Result with (StatusCode, String)메시지가 포함된 오류
Custom enum + impl IntoResponse타입이 지정된 오류가 필요한 프로덕션 앱
impl IntoResponse returning JsonJSON 오류 본문을 반환하는 API
impl From for AppError라이브러리 오류와 ? 사용
anyhow wrapper빠른 프로토타입이나 스크립트

대부분의 앱에 권장되는 접근 방식

  1. 중심 오류 타입 정의

    #[derive(Debug)]
    enum AppError {
        NotFound,
        InvalidInput(String),
        Internal(String),
        // …other variants
    }
  2. IntoResponse 구현 (보통 JSON 반환이 가장 좋음)

    impl IntoResponse for AppError {
        fn into_response(self) -> axum::response::Response {
            let (status, body) = match self {
                AppError::NotFound => (
                    StatusCode::NOT_FOUND,
                    json!({ "error": "resource not found" })
                ),
                AppError::InvalidInput(msg) => (
                    StatusCode::BAD_REQUEST,
                    json!({ "error": msg })
                ),
                AppError::Internal(msg) => (
                    StatusCode::INTERNAL_SERVER_ERROR,
                    json!({ "error": msg })
                ),
            };
            (status, Json(body)).into_response()
        }
    }
  3. 서드파티 오류 변환

    impl From<LibError> for AppError {
        fn from(err: LibError) -> Self {
            // map `LibError` variants to `AppError` variants
            AppError::Internal(err.to_string())
        }
    }
  4. 핸들러에서 Result 사용

    async fn handler() -> Result {
        let data = some_lib_call().await?; // `?` works because of `From`
        Ok(Json(data))
    }

그게 전부입니다. 숨겨진 panic 없이 명시적이고 타입이 지정된 오류 경로만 있습니다.

0 조회
Back to Blog

관련 글

더 보기 »

Rust가 Outreachy에 참여합니다

소개 Rust 프로젝트는 Google Summer of Code를 포함한 오픈 소스 멘토십 프로그램에 참여해 온 강력한 역사를 구축해 왔습니다. https://bl...