#Guard 패턴 Cookbook

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


#개요

Guard는 보안 및 정책 제약을 파이프라인 내 명시적이고 추적 가능한 단계로 적용하는 타입 기반 Transition<T, T> 노드입니다. 숨겨진 Tower 미들웨어 레이어 대신, Bus를 통해 데이터를 읽고 쓰는 일급 파이프라인 구성원으로 동작합니다.

이 Cookbook은 가장 일반적인 Guard 구성 패턴을 다룹니다.


#1. 글로벌 Guard 등록

HttpIngress에서 .guard()로 Guard를 등록합니다. 글로벌 Guard는 등록 순서대로 모든 요청에 대해, 라우트 회로 실행 전에 실행됩니다.

use ranvier_http::prelude::*;
use ranvier_guard::prelude::*;

Ranvier::http()
    .guard(RequestIdGuard::new())
    .guard(AccessLogGuard::new())
    .guard(CorsGuard::new(CorsConfig {
        allowed_origins: vec!["https://app.example.com".into()],
        allowed_methods: vec!["GET".into(), "POST".into(), "PUT".into(), "DELETE".into()],
        allowed_headers: vec!["Content-Type".into(), "Authorization".into()],
        max_age_seconds: 86400,
        allow_credentials: true,
    }))
    .guard(SecurityHeadersGuard::new(SecurityPolicy::default()))
    .guard(CompressionGuard::new().prefer_brotli())
    .get("/api/health", health_circuit)
    .run(())
    .await?;

실행 순서: RequestIdGuard -> AccessLogGuard -> CorsGuard -> SecurityHeadersGuard -> CompressionGuard -> 라우트 회로.


#2. `guards![]`를 이용한 라우트별 Guard

post_with_guards() (또는 get_with_guards, put_with_guards 등)와 guards![] 매크로를 사용하여 특정 라우트에만 Guard를 적용합니다. 라우트별 Guard는 글로벌 Guard 이후에 실행됩니다.

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

Ranvier::http()
    .guard(AccessLogGuard::new())                    // 글로벌 — 모든 라우트
    .guard(CorsGuard::<()>::permissive())    // 글로벌 — 모든 라우트
    .post_with_guards("/api/orders", order_circuit, guards![
        ContentTypeGuard::json(),                    // 라우트별 — POST만
        IdempotencyGuard::ttl_5min(),                // 라우트별 — POST만
        RequestSizeLimitGuard::max_2mb(),            // 라우트별 — POST만
    ])
    .get("/api/orders", list_circuit)                // 추가 Guard 없음
    .run(())
    .await?;

guards![] 매크로는 각 Guard 표현식에 대해 GuardIntegration::register()를 호출하여 Vec<RegisteredGuard>를 생성합니다.


#3. 커스텀 Guard 구현

모든 Transition<T, T>가 Guard로 사용될 수 있습니다. 패턴: Bus에서 컨텍스트를 읽고, 검증한 다음, 입력을 통과시키거나(Outcome::next) 거부합니다(Outcome::fault).

use async_trait::async_trait;
use ranvier_core::prelude::*;
use serde::{Deserialize, Serialize};

/// Bus 타입: Accept 헤더에서 추출한 API 버전.
#[derive(Debug, Clone)]
pub struct ApiVersion(pub String);

/// 지원되는 API 버전이 없는 요청을 거부하는 Guard.
#[derive(Debug, Clone)]
pub struct ApiVersionGuard<T> {
    supported: Vec<String>,
    _marker: std::marker::PhantomData<T>,
}

impl<T> ApiVersionGuard<T> {
    pub fn new(supported: Vec<String>) -> Self {
        Self {
            supported,
            _marker: std::marker::PhantomData,
        }
    }
}

#[async_trait]
impl<T: Send + Sync + 'static> Transition<T, T> for ApiVersionGuard<T> {
    type Error = String;
    type Resources = ();

    async fn run(
        &self,
        input: T,
        _resources: &Self::Resources,
        bus: &mut Bus,
    ) -> Outcome<T, Self::Error> {
        let version = bus.get_cloned::<ApiVersion>()
            .map(|v| v.0)
            .unwrap_or_default();

        if self.supported.contains(&version) {
            Outcome::next(input)
        } else {
            Outcome::fault(format!(
                "406 Not Acceptable: API 버전 '{}'은(는) 지원되지 않습니다",
                version
            ))
        }
    }
}

#4. Guard Bus 주입

Guard는 Bus를 통해 통신합니다. GuardIntegration 트레이트가 HTTP 헤더를 Bus 타입으로, Bus 타입을 응답 헤더로 자동 변환합니다.

읽기 흐름: HTTP 요청 -> BusInjectorFn -> Bus -> Guard 읽기 쓰기 흐름: Guard 쓰기 -> Bus -> ResponseExtractorFn -> HTTP 응답

내장 Bus 연결 예시:

Guard Bus에서 읽기 Bus에 쓰기
CorsGuard RequestOrigin CorsHeaders
AuthGuard AuthorizationHeader IamIdentity
RateLimitGuard ClientIdentity --
CompressionGuard AcceptEncoding CompressionConfig
RequestIdGuard RequestId (선택) RequestId
ContentTypeGuard RequestContentType --
TimeoutGuard -- TimeoutDeadline
IdempotencyGuard IdempotencyKey IdempotencyCachedResponse

#프로덕션 `bus_injector` 패턴

Guard와 애플리케이션 데이터를 함께 사용할 때, bus_injector에서 데이터베이스 풀, PathParams, 요청 헤더를 하나의 클로저로 주입합니다:

use ranvier_http::{PathParams, prelude::*};
use ranvier_guard::prelude::*;
use sqlx::PgPool;

let pool = PgPool::connect(&database_url).await?;

Ranvier::http()
    .bind("0.0.0.0:8080")
    .bus_injector({
        let pool = pool.clone();
        move |parts: &http::request::Parts, bus: &mut Bus| {
            // 애플리케이션 데이터
            bus.insert(pool.clone());
            // 경로 파라미터 (HttpIngress가 자동 추출)
            if let Some(params) = parts.extensions.get::<PathParams>() {
                bus.insert(params.clone());
            }
            // 요청 헤더를 Vec<(String, String)>으로 변환
            let headers: Vec<(String, String)> = parts.headers.iter()
                .map(|(k, v)| (k.to_string(), v.to_str().unwrap_or("").to_string()))
                .collect();
            bus.insert(headers);
        }
    })
    // Guard는 자체 Bus 타입(RequestOrigin 등)을 자동 주입
    .guard(CorsGuard::<()>::permissive())
    .guard(AccessLogGuard::new())
    .post_typed("/api/orders", order_pipeline())
    .get("/api/orders/:id", get_order_pipeline())
    .run(())
    .await?;

Guard Bus 타입(RequestOrigin, CorsHeaders 등)은 GuardIntegration 트레이트에 의해 자동 주입됩니다. bus_injector에는 애플리케이션 고유 데이터만 주입하면 됩니다.


#5. 인증 Guard 패턴

#Bearer Token

use ranvier_guard::prelude::*;

// 간단한 토큰 검증
let auth = AuthGuard::bearer(vec![
    std::env::var("API_TOKEN").expect("API_TOKEN must be set"),
]);

Ranvier::http()
    .guard(auth)
    .get("/api/protected", protected_circuit)
    .run(())
    .await?;

#Bearer + 역할 정책

use ranvier_core::iam::IamPolicy;
use ranvier_guard::prelude::*;

let admin_auth = AuthGuard::bearer(vec!["admin-token".into()])
    .with_policy(IamPolicy::RequireRole("admin".into()));

Ranvier::http()
    .get_with_guards("/api/admin", admin_circuit, guards![admin_auth])
    .run(())
    .await?;

#커스텀 JWT 검증기

use ranvier_core::iam::IamIdentity;
use ranvier_guard::prelude::*;

let jwt_auth = AuthGuard::custom(|header_value| {
    let token = header_value
        .strip_prefix("Bearer ")
        .ok_or("Bearer 스키마가 필요합니다")?;
    // JWT 검증 로직
    let claims = validate_jwt(token).map_err(|e| e.to_string())?;
    Ok(IamIdentity::new(&claims.sub).with_role(&claims.role))
});

#6. 실전 예제: API 서버

프로덕션 API를 위한 CORS + Auth + RateLimit + Compression + Logging 조합:

use ranvier_http::{guards, prelude::*};
use ranvier_guard::prelude::*;
use std::time::Duration;

let api_token = std::env::var("API_TOKEN").expect("API_TOKEN required");

Ranvier::http()
    .bind("0.0.0.0:8080")
    // --- 글로벌 Guard (모든 라우트) ---
    .guard(RequestIdGuard::new())
    .guard(AccessLogGuard::new().redact_paths(vec!["/auth/login".into()]))
    .guard(CorsGuard::new(CorsConfig {
        allowed_origins: vec!["https://app.example.com".into()],
        ..Default::default()
    }))
    .guard(SecurityHeadersGuard::new(
        SecurityPolicy::default().with_csp("default-src 'self'"),
    ))
    .guard(RateLimitGuard::new(100, 60_000))  // 분당 100회
    .guard(CompressionGuard::new().prefer_brotli())
    // --- 인증이 필요한 라우트 ---
    .get_with_guards("/api/users", list_users, guards![
        AuthGuard::bearer(vec![api_token.clone()]),
    ])
    .post_with_guards("/api/users", create_user, guards![
        AuthGuard::bearer(vec![api_token.clone()]),
        ContentTypeGuard::json(),
        RequestSizeLimitGuard::max_2mb(),
        IdempotencyGuard::ttl_5min(),
    ])
    // --- 공개 라우트 (인증 Guard 없음) ---
    .get("/health", health_circuit)
    .run(())
    .await?;

#7. Guard 오류 응답

Guard는 오류 메시지에 HTTP 상태 코드를 포함합니다. GuardIntegration 레이어가 Fault 메시지의 "NNN " 접두사를 파싱하여 적절한 응답 상태 코드를 설정합니다:

  • CorsGuard -> 403 Forbidden
  • AuthGuard -> 401 Unauthorized / 403 Forbidden (정책 위반)
  • RateLimitGuard -> 429 Too Many Requests
  • RequestSizeLimitGuard -> 413 Payload Too Large
  • ContentTypeGuard -> 415 Unsupported Media Type
  • TimeoutGuard -> 408 Request Timeout

Guard Fault 메시지가 3자리 상태 코드로 시작하면(예: "413 Payload Too Large") 해당 코드를 사용하고, 그렇지 않으면 각 Guard의 default_status를 적용합니다.


#관련 문서

  • Bus 접근 패턴 -- Bus 읽기/쓰기 결정 가이드
  • HttpIngress 패턴 Cookbook -- 라우트 등록 패턴
  • 보안 강화 가이드 -- 프로덕션 보안 체크리스트