핵심 요약
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/s | 32K | 350K |
| p99 지연 | 11ms | 2.4ms |
| 메모리 (idle) | 140MB | 14MB |
| 이미지 크기 | 180MB | 80MB |
자주 묻는 질문
Rust 학습 부담?
borrow checker가 가파른 학습 곡선. 기본 CRUD 마스터까지 2~4주.

댓글 0