본문으로 건너뛰기
Kreath Archive
TechProjectsBooksAbout
TechProjectsBooksAbout

내비게이션

  • Tech
  • Projects
  • Books
  • About
  • Tags

카테고리

  • AI / ML
  • 웹 개발
  • 프로그래밍
  • 개발 도구

연결

  • GitHub
  • Email
  • RSS
© 2026 Kreath Archive. All rights reserved.Built with Next.js + MDX
홈TechProjectsBooksAbout
//
  1. 홈
  2. 테크
  3. 6장: Axum 고급 패턴
2026년 2월 21일·프로그래밍·

6장: Axum 고급 패턴

중첩 라우터, 인증/인가 미들웨어, 요청 검증, 웹소켓, SSE, API 테스트까지 프로덕션 수준의 Axum 패턴을 다룹니다.

10분1,058자10개 섹션
rust
공유
rust-backend6 / 11
1234567891011
이전5장: Axum 웹 프레임워크 기초다음7장: SQLx 데이터베이스 연동

학습 목표

  • 중첩 라우터와 구조화된 프로젝트 레이아웃을 설계합니다
  • 커스텀 미들웨어와 인증/인가 패턴을 구현합니다
  • 요청 검증과 구조화된 에러 응답을 적용합니다
  • 웹소켓과 SSE를 활용한 실시간 통신을 이해합니다
  • axum::test를 활용한 API 테스트를 작성합니다

구조화된 프로젝트 레이아웃

실전 프로젝트에서는 모든 코드를 main.rs에 넣을 수 없습니다. 계층별로 모듈을 분리하는 것이 핵심입니다.

권장 프로젝트 구조
text
src/
  main.rs              # 진입점, 서버 시작
  lib.rs               # 공개 모듈 선언
  config.rs            # 설정 로드
  routes/
    mod.rs             # 라우터 조립
    users.rs           # /users 라우트
    posts.rs           # /posts 라우트
    auth.rs            # /auth 라우트
  handlers/
    mod.rs
    users.rs           # 사용자 핸들러
    posts.rs           # 게시물 핸들러
  models/
    mod.rs
    user.rs            # User 도메인 모델
    post.rs            # Post 도메인 모델
  middleware/
    mod.rs
    auth.rs            # 인증 미들웨어
    logging.rs         # 로깅 미들웨어
  error.rs             # 에러 타입 정의
  state.rs             # AppState 정의

중첩 라우터

src/routes/mod.rs
rust
use axum::Router;
use std::sync::Arc;
use crate::state::AppState;
 
pub mod auth;
pub mod posts;
pub mod users;
 
pub fn create_router(state: Arc<AppState>) -> Router {
    Router::new()
        .nest("/api/v1", api_v1(state.clone()))
        .nest("/auth", auth::routes(state))
}
 
fn api_v1(state: Arc<AppState>) -> Router {
    Router::new()
        .nest("/users", users::routes(state.clone()))
        .nest("/posts", posts::routes(state))
}
src/routes/users.rs
rust
use axum::{Router, routing::{get, post, put, delete}};
use std::sync::Arc;
use crate::handlers::users;
use crate::state::AppState;
 
pub fn routes(state: Arc<AppState>) -> Router {
    Router::new()
        .route("/", get(users::list).post(users::create))
        .route("/{id}", get(users::get_one).put(users::update).delete(users::remove))
        .with_state(state)
}

커스텀 미들웨어

Axum에서 미들웨어를 만드는 가장 간단한 방법은 axum::middleware::from_fn을 사용하는 것입니다.

요청 타이밍 미들웨어
rust
use axum::{
    middleware::{self, Next},
    extract::Request,
    response::Response,
};
use std::time::Instant;
 
async fn timing_middleware(
    request: Request,
    next: Next,
) -> Response {
    let start = Instant::now();
    let method = request.method().clone();
    let uri = request.uri().clone();
 
    let response = next.run(request).await;
 
    let duration = start.elapsed();
    tracing::info!(
        method = %method,
        uri = %uri,
        duration_ms = duration.as_millis(),
        status = response.status().as_u16(),
        "요청 처리 완료"
    );
 
    response
}
 
// 라우터에 적용
let app = Router::new()
    .route("/users", get(list_users))
    .layer(middleware::from_fn(timing_middleware));

인증 미들웨어

JWT 기반 인증 미들웨어를 구현합니다.

src/middleware/auth.rs
rust
use axum::{
    extract::Request,
    http::{header, StatusCode},
    middleware::Next,
    response::Response,
    Json,
};
use jsonwebtoken::{decode, DecodingKey, Validation};
use serde::{Deserialize, Serialize};
 
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct Claims {
    pub sub: String,    // 사용자 ID
    pub role: String,   // 역할
    pub exp: usize,     // 만료 시간
}
 
pub async fn auth_middleware(
    mut request: Request,
    next: Next,
) -> Result<Response, StatusCode> {
    // Authorization 헤더에서 토큰 추출
    let token = request
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|value| value.to_str().ok())
        .and_then(|value| value.strip_prefix("Bearer "))
        .ok_or(StatusCode::UNAUTHORIZED)?;
 
    // JWT 검증
    let secret = std::env::var("JWT_SECRET").unwrap_or_else(|_| "secret".to_string());
    let token_data = decode::<Claims>(
        token,
        &DecodingKey::from_secret(secret.as_bytes()),
        &Validation::default(),
    )
    .map_err(|_| StatusCode::UNAUTHORIZED)?;
 
    // 검증된 클레임을 요청 확장에 삽입
    request.extensions_mut().insert(token_data.claims);
 
    Ok(next.run(request).await)
}

핸들러에서 인증 정보 사용

인증된 핸들러
rust
use axum::Extension;
 
async fn get_profile(
    Extension(claims): Extension<Claims>,
) -> Json<serde_json::Value> {
    Json(serde_json::json!({
        "user_id": claims.sub,
        "role": claims.role,
    }))
}
 
// 인증이 필요한 라우트에만 미들웨어 적용
let protected_routes = Router::new()
    .route("/profile", get(get_profile))
    .route("/settings", get(get_settings).put(update_settings))
    .layer(middleware::from_fn(auth_middleware));
 
let public_routes = Router::new()
    .route("/health", get(health_check))
    .route("/auth/login", post(login));
 
let app = Router::new()
    .merge(public_routes)
    .merge(protected_routes);
Tip

인증 미들웨어를 전체 라우터가 아닌 특정 라우트 그룹에만 적용할 수 있습니다. 공개 라우트와 보호 라우트를 분리하여 merge하는 패턴이 가장 깔끔합니다.

요청 검증

요청 데이터의 유효성을 검증하는 커스텀 추출자를 만듭니다.

validator를 활용한 요청 검증
rust
use axum::{
    async_trait,
    extract::{FromRequest, Request, rejection::JsonRejection},
    Json,
    http::StatusCode,
    response::{IntoResponse, Response},
};
use serde::de::DeserializeOwned;
use validator::Validate;
 
// 검증 가능한 Json 추출자
pub struct ValidatedJson<T>(pub T);
 
#[async_trait]
impl<S, T> FromRequest<S> for ValidatedJson<T>
where
    T: DeserializeOwned + Validate,
    S: Send + Sync,
    Json<T>: FromRequest<S, Rejection = JsonRejection>,
{
    type Rejection = ValidationError;
 
    async fn from_request(req: Request, state: &S) -> Result<Self, Self::Rejection> {
        let Json(value) = Json::<T>::from_request(req, state)
            .await
            .map_err(|e| ValidationError::InvalidJson(e.to_string()))?;
 
        value.validate().map_err(|e| ValidationError::Validation(e.to_string()))?;
 
        Ok(ValidatedJson(value))
    }
}
 
#[derive(Debug)]
pub enum ValidationError {
    InvalidJson(String),
    Validation(String),
}
 
impl IntoResponse for ValidationError {
    fn into_response(self) -> Response {
        let (status, message) = match self {
            ValidationError::InvalidJson(msg) => (StatusCode::BAD_REQUEST, msg),
            ValidationError::Validation(msg) => (StatusCode::UNPROCESSABLE_ENTITY, msg),
        };
 
        (status, Json(serde_json::json!({ "error": message }))).into_response()
    }
}
검증 규칙이 있는 요청 모델
rust
use validator::Validate;
 
#[derive(Debug, Deserialize, Validate)]
struct CreateUserRequest {
    #[validate(length(min = 1, max = 100, message = "이름은 1-100자여야 합니다"))]
    name: String,
 
    #[validate(email(message = "유효한 이메일 주소를 입력하세요"))]
    email: String,
 
    #[validate(length(min = 8, message = "비밀번호는 8자 이상이어야 합니다"))]
    password: String,
}
 
// 핸들러에서 ValidatedJson 사용
async fn create_user(
    ValidatedJson(payload): ValidatedJson<CreateUserRequest>,
) -> (StatusCode, Json<serde_json::Value>) {
    // payload는 이미 검증 완료
    (StatusCode::CREATED, Json(serde_json::json!({
        "message": "사용자 생성 완료",
        "name": payload.name,
    })))
}

레이트 리미팅

Tower 기반 레이트 리미팅
rust
use tower::limit::RateLimitLayer;
use std::time::Duration;
 
let app = Router::new()
    .route("/api/search", get(search))
    // 1초에 최대 10개 요청
    .layer(RateLimitLayer::new(10, Duration::from_secs(1)));

보다 정교한 레이트 리미팅이 필요하면 tower-governor 크레이트를 사용합니다. IP별, 사용자별 제한을 설정할 수 있습니다.

웹소켓

Axum은 웹소켓을 기본 지원합니다.

웹소켓 핸들러
rust
use axum::{
    extract::ws::{Message, WebSocket, WebSocketUpgrade},
    response::Response,
};
 
async fn ws_handler(ws: WebSocketUpgrade) -> Response {
    ws.on_upgrade(handle_socket)
}
 
async fn handle_socket(mut socket: WebSocket) {
    // 메시지 수신 및 에코
    while let Some(Ok(msg)) = socket.recv().await {
        match msg {
            Message::Text(text) => {
                let reply = format!("에코: {}", text);
                if socket.send(Message::Text(reply.into())).await.is_err() {
                    break; // 클라이언트 연결 끊김
                }
            }
            Message::Close(_) => break,
            _ => {}
        }
    }
}
 
// 라우트 등록
let app = Router::new()
    .route("/ws", get(ws_handler));

SSE (Server-Sent Events)

서버에서 클라이언트로 단방향 실시간 데이터를 전송할 때 SSE를 사용합니다.

SSE 핸들러
rust
use axum::response::sse::{Event, Sse};
use tokio_stream::StreamExt;
use std::convert::Infallible;
 
async fn sse_handler() -> Sse<impl tokio_stream::Stream<Item = Result<Event, Infallible>>> {
    let stream = tokio_stream::wrappers::IntervalStream::new(
        tokio::time::interval(std::time::Duration::from_secs(1)),
    )
    .map(|_| {
        let now = chrono::Utc::now().to_rfc3339();
        Ok(Event::default().data(format!("서버 시간: {}", now)))
    });
 
    Sse::new(stream)
}

API 테스트

Axum은 서버를 시작하지 않고 핸들러를 직접 테스트할 수 있는 방법을 제공합니다.

API 통합 테스트
rust
#[cfg(test)]
mod tests {
    use super::*;
    use axum::{
        body::Body,
        http::{Request, StatusCode},
    };
    use http_body_util::BodyExt;
    use tower::ServiceExt;
 
    fn create_test_app() -> Router {
        let state = Arc::new(AppState {
            users: Arc::new(RwLock::new(HashMap::new())),
            next_id: Arc::new(tokio::sync::Mutex::new(1)),
        });
 
        Router::new()
            .route("/users", get(list_users).post(create_user))
            .route("/users/{id}", get(get_user))
            .with_state(state)
    }
 
    #[tokio::test]
    async fn test_create_user() {
        let app = create_test_app();
 
        let response = app
            .oneshot(
                Request::builder()
                    .method("POST")
                    .uri("/users")
                    .header("content-type", "application/json")
                    .body(Body::from(
                        serde_json::to_string(&serde_json::json!({
                            "name": "Alice",
                            "email": "alice@example.com"
                        }))
                        .unwrap(),
                    ))
                    .unwrap(),
            )
            .await
            .unwrap();
 
        assert_eq!(response.status(), StatusCode::CREATED);
 
        let body = response.into_body().collect().await.unwrap().to_bytes();
        let user: serde_json::Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(user["name"], "Alice");
        assert_eq!(user["id"], 1);
    }
 
    #[tokio::test]
    async fn test_get_nonexistent_user() {
        let app = create_test_app();
 
        let response = app
            .oneshot(
                Request::builder()
                    .uri("/users/999")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
 
        assert_eq!(response.status(), StatusCode::NOT_FOUND);
    }
 
    #[tokio::test]
    async fn test_list_users_empty() {
        let app = create_test_app();
 
        let response = app
            .oneshot(
                Request::builder()
                    .uri("/users")
                    .body(Body::empty())
                    .unwrap(),
            )
            .await
            .unwrap();
 
        assert_eq!(response.status(), StatusCode::OK);
 
        let body = response.into_body().collect().await.unwrap().to_bytes();
        let users: Vec<serde_json::Value> = serde_json::from_slice(&body).unwrap();
        assert!(users.is_empty());
    }
}
Info

oneshot은 Tower의 ServiceExt 트레이트가 제공하는 메서드로, 서버를 시작하지 않고 라우터에 직접 요청을 보낼 수 있습니다. 단위 테스트부터 통합 테스트까지 이 방식으로 빠르게 테스트할 수 있습니다.

정리

이 장에서는 Axum의 고급 패턴을 다루었습니다.

  • 구조화된 프로젝트 레이아웃으로 코드를 계층별로 분리합니다
  • 커스텀 미들웨어를 from_fn으로 간편하게 작성합니다
  • JWT 인증 미들웨어로 보호 라우트를 구현합니다
  • ValidatedJson 커스텀 추출자로 요청 검증을 자동화합니다
  • 웹소켓과 SSE로 실시간 통신을 처리합니다
  • oneshot 테스트로 서버 없이 API를 검증합니다

다음 장에서는 SQLx 데이터베이스 연동을 다룹니다. 컴파일 타임 쿼리 검증, 연결 풀, 트랜잭션 등 실전 데이터베이스 패턴을 익힙니다.

이 글이 도움이 되셨나요?

관련 주제 더 보기

#rust

관련 글

프로그래밍

7장: SQLx 데이터베이스 연동

SQLx의 컴파일 타임 쿼리 검증, 연결 풀, CRUD 구현, 마이그레이션, 트랜잭션까지 Rust 백엔드의 데이터베이스 연동 패턴을 다룹니다.

2026년 2월 23일·11분
프로그래밍

5장: Axum 웹 프레임워크 기초

Axum의 Tower 기반 아키텍처, 라우팅, 핸들러, 추출자, 응답 타입, 미들웨어까지 Hello World에서 CRUD API까지 실습합니다.

2026년 2월 19일·10분
프로그래밍

8장: 테스트와 품질 보증

Rust의 단위 테스트, 통합 테스트, API 테스트, testcontainers를 활용한 DB 테스트, 프로퍼티 기반 테스트, 벤치마킹까지 다룹니다.

2026년 2월 25일·12분
이전 글5장: Axum 웹 프레임워크 기초
다음 글7장: SQLx 데이터베이스 연동

댓글

목차

약 10분 남음
  • 학습 목표
  • 구조화된 프로젝트 레이아웃
    • 중첩 라우터
  • 커스텀 미들웨어
  • 인증 미들웨어
    • 핸들러에서 인증 정보 사용
  • 요청 검증
  • 레이트 리미팅
  • 웹소켓
  • SSE (Server-Sent Events)
  • API 테스트
  • 정리