#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 ForbiddenAuthGuard-> 401 Unauthorized / 403 Forbidden (정책 위반)RateLimitGuard-> 429 Too Many RequestsRequestSizeLimitGuard-> 413 Payload Too LargeContentTypeGuard-> 415 Unsupported Media TypeTimeoutGuard-> 408 Request Timeout
Guard Fault 메시지가 3자리 상태 코드로 시작하면(예: "413 Payload Too Large")
해당 코드를 사용하고, 그렇지 않으면 각 Guard의 default_status를 적용합니다.
#관련 문서
- Bus 접근 패턴 -- Bus 읽기/쓰기 결정 가이드
- HttpIngress 패턴 Cookbook -- 라우트 등록 패턴
- 보안 강화 가이드 -- 프로덕션 보안 체크리스트