#HttpIngress 패턴 Cookbook

버전: 0.36.0 | 업데이트: 2026-04-03 | 적용 대상: ranvier-http 0.36+ | 카테고리: 쿡북


#개요

HttpIngress는 웹 서버가 아닌 Ingress Circuit Builder입니다. Axon 회로를 HTTP 라우트에 연결하고, Guard를 관리하며, 실제 요청 처리는 hyper에 위임합니다. 이 Cookbook에서는 라우팅, 바디 파싱, 정적 자산 제공의 주요 패턴을 다룹니다.


#1. `post()` vs `post_typed::()` 결정 트리

flowchart TD
    Q{"라우트가 JSON 요청 바디를 받는가?"}
    Q -->|아니오| P["post() 사용\nAxon‹(), Out, E, R›\n바디 무시; 회로는 ()를 받음"]
    Q -->|예| PT["post_typed::‹T›() 사용\nAxon‹T, Out, E, R›\n바디가 자동으로 T로 역직렬화됨\nT: DeserializeOwned + Serialize + JsonSchema"]

#`post()` -- 바디 없음

use ranvier_http::prelude::*;

let trigger_circuit = Axon::simple::<String>("trigger")
    .then(start_job);

Ranvier::http()
    .post("/api/jobs/start", trigger_circuit)
    .run(())
    .await?;

#`post_typed::()` -- 타입 기반 JSON 바디

use ranvier_http::prelude::*;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};

#[derive(Debug, Deserialize, Serialize, JsonSchema)]
struct CreateOrder {
    product_id: String,
    quantity: u32,
    shipping_address: String,
}

let order_circuit = Axon::typed::<CreateOrder, String>("create-order")
    .then(validate_order)
    .then(persist_order);

Ranvier::http()
    .post_typed::<CreateOrder, _, _>("/api/orders", order_circuit)
    .run(())
    .await?;

post_typed는 자동으로:

  • 요청 바디를 T로 JSON 역직렬화
  • 파싱 실패 시 400 Bad Request 반환
  • OpenAPI 스키마 생성을 위해 JsonSchema 기록

put_typed::<T>()patch_typed::<T>()에도 동일 패턴이 적용됩니다.

#`schemars` Feature 요구 사항

JsonSchema derive는 요청 구조체의 필드 타입에 따라 추가 schemars feature가 필요할 수 있습니다. derive 컴파일이 실패하면 아래 표를 확인하세요:

필드 타입 필요 feature Cargo.toml 예시
Uuid uuid1 schemars = { version = "1", features = ["derive", "uuid1"] }
NaiveDate, DateTime<Utc> chrono04 schemars = { version = "1", features = ["derive", "chrono04"] }
Decimal rust_decimal1 schemars = { version = "1", features = ["derive", "rust_decimal1"] }
Url url2 schemars = { version = "1", features = ["derive", "url2"] }

여러 feature를 조합할 수 있습니다: features = ["derive", "uuid1", "chrono04"].


#2. 경로 파라미터 추출

라우트 경로에 :param 구문을 사용합니다. 파라미터는 Bus의 PathParams를 통해 접근합니다.

use ranvier_http::prelude::*;
use ranvier_core::prelude::*;

let get_user = Axon::simple::<String>("get-user")
    .then_fn("extract-id", |_input, _res, bus| async move {
        let params = bus.require::<PathParams>();
        let user_id = params.get("id").unwrap_or("unknown");
        Outcome::next(format!("User: {}", user_id))
    });

Ranvier::http()
    .get("/api/users/:id", get_user)
    .get("/api/users/:id/posts/:slug", get_user_post)
    .run(())
    .await?;

다중 파라미터 지원: /api/orgs/:org_id/teams/:team_id.

#`PathParams::get()`은 `&str`를 반환

PathParams::get()Option<&String>이 아닌 Option<&str>를 반환합니다. 소유된 String이 필요하면 .cloned() 대신 .map(|s| s.to_string())를 사용하세요:

// 경로 파라미터를 소유된 String으로 읽기
let id: Option<String> = bus.get_cloned::<PathParams>().ok()
    .and_then(|params| params.get("id").map(|s| s.to_string()));

// 경로 파라미터를 Uuid로 파싱
let uuid: Option<uuid::Uuid> = bus.get_cloned::<PathParams>().ok()
    .and_then(|params| params.get("id").and_then(|s| s.parse().ok()));

마이그레이션 참고: HashMap<String, String> 패턴에서 마이그레이션하는 경우, PathParams::get()&String이 아닌 &str를 반환하므로 .cloned().map(|s| s.to_string())로 교체하세요.


#3. 요청 컨텍스트용 Bus 인젝터

bus_injector()를 사용하면 Guard나 회로 실행 전에 임의의 HTTP 요청 데이터를 Bus에 주입할 수 있습니다. 내장 Guard가 다루지 않는 데이터를 전달할 때 유용합니다.

use ranvier_http::prelude::*;
use ranvier_core::bus::Bus;

#[derive(Debug, Clone)]
struct UserAgent(String);

#[derive(Debug, Clone)]
struct AcceptLanguage(String);

Ranvier::http()
    .bus_injector(|parts: &http::request::Parts, bus: &mut Bus| {
        if let Some(ua) = parts.headers.get("user-agent") {
            if let Ok(s) = ua.to_str() {
                bus.insert(UserAgent(s.to_string()));
            }
        }
        if let Some(lang) = parts.headers.get("accept-language") {
            if let Ok(s) = lang.to_str() {
                bus.insert(AcceptLanguage(s.to_string()));
            }
        }
    })
    .get("/api/data", data_circuit)
    .run(())
    .await?;

회로 내에서 주입된 값을 읽습니다:

let circuit = Axon::simple::<String>("data")
    .then_fn("use-context", |_input, _res, bus| async move {
        let ua = bus.get_cloned::<UserAgent>()
            .map(|u| u.0)
            .unwrap_or_else(|_| "unknown".into());
        Outcome::next(format!("Hello from {}", ua))
    });

#4. Guard + 라우트 통합

Guard와 라우트는 자연스럽게 구성됩니다. 글로벌 Guard는 모든 라우트에, 라우트별 Guard는 특정 엔드포인트에만 적용됩니다.

use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;

Ranvier::http()
    // 글로벌: 모든 요청에 적용
    .guard(RequestIdGuard::new())
    .guard(AccessLogGuard::new())
    .guard(CorsGuard::<()>::permissive())
    // 읽기 엔드포인트: 추가 Guard 없음
    .get("/api/products", list_products)
    .get("/api/products/:id", get_product)
    // 쓰기 엔드포인트: 라우트별 Guard
    .post_with_guards("/api/products", create_product, guards![
        ContentTypeGuard::json(),
        RequestSizeLimitGuard::max_2mb(),
    ])
    .put_with_guards("/api/products/:id", update_product, guards![
        ContentTypeGuard::json(),
    ])
    .delete("/api/products/:id", delete_product)
    .run(())
    .await?;

#5. `serve_dir()`로 정적 자산 제공

정적 파일 제공을 위해 디렉터리를 마운트합니다. MIME 타입 감지, 304 캐싱, 디렉터리 인덱스, 사전 압축 파일 등을 Ranvier가 자동으로 처리합니다.

serve_dir()는 API 라우트와 빌드된 프론트엔드를 한 프로세스에서 함께 제공할 때 가장 적합합니다. 순수 정적 호스팅이나 CDN 중심 자산 배포는 nginx, Caddy, object storage/CDN, 또는 tower-http::ServeDir 쪽이 더 적합합니다.

#기본 설정

use ranvier_http::prelude::*;

Ranvier::http()
    .serve_dir("/static", "./public")
    .get("/api/data", data_circuit)
    .run(())
    .await?;

#SPA 폴백

Ranvier::http()
    .serve_dir("/", "./dist")
    .directory_index("index.html")
    .spa_fallback("./dist/index.html")
    .run(())
    .await?;

#프로덕션 정적 자산

Ranvier::http()
    .serve_dir("/assets", "./build/assets")
    .directory_index("index.html")
    .static_cache_control("public, max-age=3600")
    .immutable_cache()                  // 해시된 파일명에 max-age=31536000 적용
    .serve_precompressed()              // .br / .gz 파일 우선 확인
    .enable_range_requests()            // Range: bytes=X-Y 지원 (206 응답)
    .get("/api/data", data_circuit)
    .run(())
    .await?;

immutable_cache()는 콘텐츠 해시가 포함된 파일명(예: app.a1b2c3.js)을 감지하여 Cache-Control: public, max-age=31536000, immutable을 적용합니다.

serve_precompressed()는 원본 파일 제공 전에 .br.gz 사전 압축 파일을 확인합니다. 사전 압축 파일을 생성하는 빌드 도구와 함께 사용하면 효과적입니다.

enable_range_requests()는 부분 콘텐츠 응답(HTTP 206) 지원을 추가하며, 대용량 파일 다운로드와 미디어 스트리밍에 유용합니다.


#6. 헬스 엔드포인트

use ranvier_http::prelude::*;

Ranvier::http()
    .health_endpoint("/health")
    .readiness_liveness("/ready", "/live")
    .health_check("database", |resources| async move {
        // 헬스 체크 로직
        Ok(())
    })
    .get("/api/data", data_circuit)
    .run(())
    .await?;

기본 경로(/health/ready, /health/live) 사용:

Ranvier::http()
    .readiness_liveness_default()
    .run(())
    .await?;

#7. WebSocket 라우트

use ranvier_http::prelude::*;

Ranvier::http()
    .ws("/ws/chat", |mut conn, _resources, _bus| async move {
        conn.send(WebSocketEvent::text("Welcome!")).await.ok();
        while let Ok(Some(msg)) = conn.next_json::<serde_json::Value>().await {
            let echo = WebSocketEvent::json(&msg).unwrap();
            if conn.send(echo).await.is_err() {
                break;
            }
        }
    })
    .run(())
    .await?;

#8. 오류 핸들러

라우트에 커스텀 오류-응답 변환기를 등록합니다:

use ranvier_http::prelude::*;

Ranvier::http()
    .post_with_error("/api/orders", order_circuit, |error| {
        match error.to_string().as_str() {
            e if e.contains("duplicate") => (409, "Conflict: 중복 주문"),
            e if e.contains("invalid") => (400, "Bad Request: 유효하지 않은 입력"),
            _ => (500, "Internal Server Error"),
        }
    })
    .run(())
    .await?;

#관련 문서

  • Guard 패턴 Cookbook -- Guard 구성 패턴
  • Bus 접근 패턴 -- Bus 읽기/쓰기 결정 가이드
  • 배포 가이드 -- 프로덕션 배포 설정