Quipu-Log 교과서
파트 7 · 쓰기·읽기 경로

31 · tower 미들웨어: HTTP 요청 자동 기록

서비스 코드를 고치지 않고 모든 HTTP 요청을 자동으로 감사할 수 있다면 어떨까요? AuditLayer가 바로 그 역할입니다. Express의 미들웨어, Spring의 인터셉터처럼 — 요청이 실제 핸들러에 도달하기 전후를 감싸 자동으로 기록합니다.

비유

Express의 app.use(auditMiddleware), Spring의 HandlerInterceptor와 같은 자리입니다. 비즈니스 로직에는 손 대지 않고, 바깥 껍질에서 감사가 일어납니다.

tower Layer = 데코레이터 패턴

Rust의 tower 라이브러리는 미들웨어를 Layer라는 개념으로 추상화합니다. Layer는 내부 서비스(inner service)를 받아 그것을 감싼 새 서비스를 반환합니다. 여러 Layer를 쌓으면 양파처럼 감쌉니다.

AuditLayer (tower Layer) inner service (axum router) ① pre-filter ② 요청 캡처 ③ 응답 캡처 ④ emit(event) HTTP 요청 HTTP 응답 AuditHandle.emit_unchecked(event)
AuditLayer가 inner service를 감싸고, 요청/응답 흐름의 앞뒤에서 감사 이벤트를 만들어 파이프라인에 emit한다.

EndpointRule: 어떤 엔드포인트를 감사할까

모든 요청을 감사할 수도 있지만, 보통은 EndpointRule로 범위를 좁힙니다. 경로 접두사와 HTTP 메서드로 매칭하고, 요청/응답 바디를 캡처할지도 지정합니다.

examples/axum-demo/src/main.rslet audit_layer = AuditLayer::new(handle.clone())
    .rules(vec![
        EndpointRule::prefix("/api/")
            .method(Method::PUT)
            .capture_request()    // 요청 바디 캡처
            .capture_response()   // 응답 바디 캡처
    ])
    .filters(FilterSet::new().pre(|req| {
        // 헬스체크는 감사 불필요 → Skip
        if req.uri.path() == "/api/health" { FilterDecision::Skip }
        else { FilterDecision::Audit }
    }))
    .target_extractor(|req, _res, _req_body, _res_body| {
        // URL 경로에서 타깃 엔티티를 추출
        req.uri.path().strip_prefix("/api/docs/")
            .map(|id| vec![TargetSpec::new("default_target", EntityInput::new(id)...)])
            .unwrap_or_default()
    });

레이어를 axum 라우터에 붙이는 건 단 한 줄입니다. 기존 핸들러 코드에는 변경이 없습니다.

examples/axum-demo/src/main.rslet app = Router::new()
    .route("/api/docs/{id}", put(my_handler))
    .layer(audit_layer)  // 이 한 줄로 /api/** 자동 감사
    .route("/audit/logs", get(query_logs))
    .with_state(handle);

내부 흐름: call()에서 무슨 일이

요청이 들어오면 AuditService::call은 다음 순서로 동작합니다.

  1. EndpointRule 매칭 — 이 요청이 감사 대상인지 확인합니다.
  2. pre-filter — 면제 조건 검사(헬스체크, 정적 파일 등). Skip이면 그냥 프록시만 합니다.
  3. 요청 바디 캡처capture_request_body가 설정된 경우 바디를 읽고, inner service에는 동일 바이트를 재조립해서 넘깁니다.
  4. inner service 호출 — 실제 핸들러가 실행됩니다.
  5. 응답 바디 캡처capture_response_body가 설정된 경우 응답 바이트를 읽습니다.
  6. target_extractor 실행 — 요청·응답 정보로 타깃 엔티티를 파생합니다.
  7. post-filter — 응답을 보고 최종 면제 여부 결정(예: 304는 건너뜀).
  8. emit_unchecked — 이벤트를 파이프라인에 넣습니다. 논블로킹이므로 응답 경로를 막지 않습니다.
주의

바디 캡처를 켜면 내용 전체를 메모리에 올립니다. max_capture_bytes(기본 1 MiB)를 초과하는 바디는 캡처하지 않고 크기 메모만 남깁니다. 큰 파일 업로드 엔드포인트는 캡처를 끄거나 한계를 낮추세요.

스스로 확인

① tower Layer가 "데코레이터 패턴"이라는 말의 의미를, 양파 비유로 설명해 보세요.
② pre-filter와 post-filter의 차이는 무엇인가요? 각각 어떤 경우에 쓸까요?
.layer(audit_layer) 한 줄이 내부적으로 어떤 타입 변환을 만들어내는지 생각해 보세요.