본문 바로가기
Backend2026년 4월 22일13분 읽기

Rust Axum 마이크로서비스 — gRPC·SSE·WebSocket 풀 스택 구축

YS
김영삼
조회 1
Rust Axum 마이크로서비스 — gRPC·SSE·WebSocket 풀 스택 구축

핵심 요약

Rust Axum 0.8은 단일 프로세스 안에 REST·gRPC·SSE·WebSocket을 모두 호스팅 가능. 메모리 안전 + 성능 + 명확한 ownership으로 마이크로서비스의 baseline language로 자리잡았다.

  • Axum 0.8 + tokio 1.40+
  • tonic 0.12로 gRPC 통합
  • tower-http로 미들웨어 표준화
  • 처리량: 350K req/s (8 core, JSON 응답)

1. 기본 구조

// main.rs
use axum::{Router, routing::get};
use tower_http::{trace::TraceLayer, cors::CorsLayer};

#[tokio::main]
async fn main() {
    let app = Router::new()
        .route("/health", get(|| async { "OK" }))
        .nest("/api/v1", api_routes())
        .nest("/api/v2", v2_routes())
        .layer(TraceLayer::new_for_http())
        .layer(CorsLayer::permissive());

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

2. 핸들러 — 타입 안전

use axum::{extract::{State, Path, Json}, http::StatusCode};
use serde::{Deserialize, Serialize};

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

#[derive(Serialize)]
struct UserOut { id: i64, name: String }

async fn create_user(
    State(db): State<DbPool>,
    Json(payload): Json<CreateUser>,
) -> Result<Json<UserOut>, StatusCode> {
    let id = sqlx::query_scalar!(
        "INSERT INTO users (name, email) VALUES ($1, $2) RETURNING id",
        payload.name, payload.email
    )
    .fetch_one(&db)
    .await
    .map_err(|_| StatusCode::INTERNAL_SERVER_ERROR)?;
    
    Ok(Json(UserOut { id, name: payload.name }))
}

3. SSE — Server-Sent Events

use axum::response::sse::{Event, Sse, KeepAlive};
use futures::stream::Stream;
use std::time::Duration;

async fn events_stream() -> Sse<impl Stream<Item = Result<Event, std::convert::Infallible>>> {
    let stream = async_stream::stream! {
        loop {
            let event = Event::default()
                .id(uuid::Uuid::new_v4().to_string())
                .data(format!("{{\"timestamp\":\"{}\"}}", chrono::Utc::now()));
            yield Ok(event);
            tokio::time::sleep(Duration::from_secs(1)).await;
        }
    };
    
    Sse::new(stream).keep_alive(
        KeepAlive::new()
            .interval(Duration::from_secs(15))
            .text("keep-alive")
    )
}

4. WebSocket

use axum::extract::ws::{WebSocket, WebSocketUpgrade, Message};

async fn ws_handler(ws: WebSocketUpgrade) -> impl IntoResponse {
    ws.on_upgrade(handle_socket)
}

async fn handle_socket(mut socket: WebSocket) {
    while let Some(msg) = socket.recv().await {
        let msg = msg.unwrap();
        if socket.send(msg).await.is_err() {
            return;
        }
    }
}

5. gRPC (tonic) 통합

같은 프로세스 안에 gRPC 서비스도 호스팅. 내부 마이크로서비스 통신용.

// proto/user.proto → tonic-build로 코드 생성

use tonic::{transport::Server, Request, Response, Status};

pub struct UserSvc { db: DbPool }

#[tonic::async_trait]
impl user_proto::user_service_server::UserService for UserSvc {
    async fn get_user(
        &self,
        request: Request<GetUserRequest>,
    ) -> Result<Response<User>, Status> {
        let user = sqlx::query_as!(User, "SELECT * FROM users WHERE id = $1", request.into_inner().id)
            .fetch_one(&self.db)
            .await
            .map_err(|_| Status::not_found("User not found"))?;
        Ok(Response::new(user))
    }
}

// main에서 동시 호스팅
tokio::spawn(async move {
    Server::builder()
        .add_service(UserServiceServer::new(svc))
        .serve("0.0.0.0:50051".parse().unwrap())
        .await
});

6. Database — sqlx

use sqlx::postgres::{PgPoolOptions, PgPool};

let db = PgPoolOptions::new()
    .max_connections(50)
    .min_connections(5)
    .connect(&database_url)
    .await?;

// migrations
sqlx::migrate!("./migrations").run(&db).await?;

// query — compile-time check
let user = sqlx::query_as!(
    User,
    "SELECT id, name, email FROM users WHERE id = $1",
    user_id
).fetch_one(&db).await?;

7. 미들웨어 — Tower

use tower_http::{
    trace::TraceLayer,
    cors::CorsLayer,
    timeout::TimeoutLayer,
    compression::CompressionLayer,
};
use axum::middleware;

let app = Router::new()
    .route("/api/users", post(create_user))
    .layer(middleware::from_fn(auth_middleware))
    .layer(TraceLayer::new_for_http())
    .layer(CorsLayer::permissive())
    .layer(TimeoutLayer::new(Duration::from_secs(30)))
    .layer(CompressionLayer::new());

8. 관측성 — OpenTelemetry

use opentelemetry::trace::{Tracer, TracerProvider};
use opentelemetry_otlp::WithExportConfig;

let tracer = opentelemetry_otlp::new_pipeline()
    .tracing()
    .with_exporter(opentelemetry_otlp::new_exporter().tonic())
    .install_batch(opentelemetry_sdk::runtime::Tokio)?;

tracing_subscriber::registry()
    .with(tracing_opentelemetry::layer().with_tracer(tracer))
    .with(tracing_subscriber::fmt::layer())
    .init();

9. 에러 핸들링

use thiserror::Error;

#[derive(Error, Debug)]
enum AppError {
    #[error("not found")]
    NotFound,
    #[error("unauthorized")]
    Unauthorized,
    #[error("db: {0}")]
    Database(#[from] sqlx::Error),
}

impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let (status, msg) = match self {
            AppError::NotFound => (StatusCode::NOT_FOUND, "Not Found"),
            AppError::Unauthorized => (StatusCode::UNAUTHORIZED, "Unauthorized"),
            AppError::Database(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal Error"),
        };
        (status, Json(json!({ "error": msg }))).into_response()
    }
}

10. 배포 — multi-stage Docker

FROM rust:1.78 AS builder
WORKDIR /app
COPY . .
RUN cargo build --release

FROM debian:bookworm-slim
RUN apt-get update && apt-get install -y ca-certificates
COPY --from=builder /app/target/release/myservice /usr/local/bin/
EXPOSE 3000
CMD ["myservice"]

이미지 약 80MB. 콜드 스타트 50ms.

실측 — Express 비교

지표Express (Node)Axum (Rust)
JSON API req/s32K350K
p99 지연11ms2.4ms
메모리 (idle)140MB14MB
이미지 크기180MB80MB

자주 묻는 질문

Rust 학습 부담?

borrow checker가 가파른 학습 곡선. 기본 CRUD 마스터까지 2~4주.

Express에서 Axum으로 마이그레이션?새 서비스로 시작 권장. 기존 코드 재작성보다 점진적 리팩토링.

Go vs Rust?Go: 더 단순·생태계·러닝커브 낮음. Rust: 메모리 안전·성능·embedded까지. 팀 역량에 따라 선택.

댓글 0

아직 댓글이 없습니다.
Ctrl+Enter로 등록