#명시적 검색 파라미터 Cookbook

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


#개요

Ranvier는 쿼리 파라미터 추출에 derive 매크로 대신 명시적 검색 구조체를 사용합니다. 각 파라미터의 이름, 타입, 기본값이 코드에서 직접 보입니다 — P4(Bus = 명시적 타입 인덱스), P5(Convention-over-config 금지) 준수.

핵심 타입: PageParams, Paginated<T> (ranvier_http::prelude에서 제공)


#1. `PageParams` — 내장 페이지네이션

PageParamspageper_page 쿼리 파라미터를 자동 클램핑으로 추출합니다:

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

#[transition]
async fn list_items(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<Item>, String> {
    let page = PageParams::from_bus(bus);
    // page.page = 1 (기본값), page.per_page = 20 (기본값)
    // ?page=3&per_page=50 요청 시 → page.page = 3, page.per_page = 50

    let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");
    let total: i64 = try_outcome!(
        sqlx::query_scalar("SELECT COUNT(*) FROM items")
            .fetch_one(&*pool).await, "count"
    );
    let items: Vec<Item> = try_outcome!(
        sqlx::query_as("SELECT * FROM items ORDER BY id LIMIT $1 OFFSET $2")
            .bind(page.per_page)
            .bind(page.offset())
            .fetch_all(&*pool).await, "query"
    );
    Outcome::Next(Paginated::new(items, total, &page))
}

응답:

{
  "items": [...],
  "total_count": 142,
  "page": 3,
  "per_page": 50,
  "total_pages": 3
}

#2. 커스텀 검색 구조체 — 명시적 패턴

도메인별 필터가 필요한 경우, from_bus 메서드를 가진 일반 구조체를 작성합니다:

use ranvier_http::prelude::*;

struct InterfaceSearch {
    page: PageParams,
    intf_id: Option<String>,
    intf_nm: Option<String>,
    status: Option<String>,
    use_yn: Option<String>,
}

impl InterfaceSearch {
    fn from_bus(bus: &Bus) -> Self {
        Self {
            page: PageParams::from_bus(bus),
            intf_id: bus.query_param("intf_id"),
            intf_nm: bus.query_param("intf_nm"),
            status: bus.query_param("status"),
            use_yn: bus.query_param("use_yn"),
        }
    }
}

Transition에서의 사용:

use crate::safe_query_builder::QueryBuilder;

#[transition]
async fn search_interfaces(_: (), _: &(), bus: &mut Bus) -> Outcome<Paginated<InterfaceInfo>, String> {
    let search = InterfaceSearch::from_bus(bus);
    let pool = try_outcome!(bus.get_cloned::<PgPool>(), "PgPool");

    // db-sqlx-demo/src/safe_query_builder.rs helper를 앱 코드에 복사해 사용
    let query = QueryBuilder::new("SELECT * FROM tb_intf_inf")
        .filter_optional("intf_id", &search.intf_id)
        .filter_optional("intf_nm", &search.intf_nm)
        .filter_optional("use_yn", &search.use_yn)
        .paginate(search.page.per_page, search.page.offset())
        .build();

    // 실행 및 반환...
    Outcome::Next(Paginated::new(items, total, &search.page))
}

#3. `#[derive(QueryParams)]`를 사용하지 않는 이유

derive 매크로는 필드명으로 자동 추출합니다:

// ❌ 미지원 — P4/P5 위반
#[derive(QueryParams)]
struct Search { intf_id: Option<String>, page: i64 }

이 접근법은 어떤 쿼리 파라미터가 읽히는지, 어떤 기본값이 적용되는지 숨깁니다. 명시적 패턴은:

  1. from_bus()에서 모든 파라미터명을 확인 가능 — 네이밍 컨벤션 암기 불필요
  2. 파라미터별 기본값 제어query_param_or("page", 1) vs query_param("status")
  3. 인라인으로 검증 로직 추가 가능 — if per_page > 200 { ... }
  4. from_bus() 직접 테스트 가능 — BusQueryParams::from_query("...")로 구성

비용은 검색 구조체당 약 10행. EIMS 기준 3개 검색 엔드포인트 = 약 30행 — 완전한 가시성의 합리적 대가.


#관련 문서

  • `PageParams` API — 내장 페이지네이션 타입
  • Outcome 패턴 Cookbook — Outcome 합성
  • JSON Outcomes Cookbookget_json_out / post_typed_json_out