서비스 코드를 고치지 않고 모든 HTTP 요청을 자동으로 감사할 수 있다면 어떨까요? AuditLayer가 바로 그 역할입니다. Express의 미들웨어, Spring의 인터셉터처럼 — 요청이 실제 핸들러에 도달하기 전후를 감싸 자동으로 기록합니다.
Express의 app.use(auditMiddleware), Spring의 HandlerInterceptor와 같은 자리입니다. 비즈니스 로직에는 손 대지 않고, 바깥 껍질에서 감사가 일어납니다.
tower Layer = 데코레이터 패턴
Rust의 tower 라이브러리는 미들웨어를 Layer라는 개념으로 추상화합니다. Layer는 내부 서비스(inner service)를 받아 그것을 감싼 새 서비스를 반환합니다. 여러 Layer를 쌓으면 양파처럼 감쌉니다.
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은 다음 순서로 동작합니다.
- EndpointRule 매칭 — 이 요청이 감사 대상인지 확인합니다.
- pre-filter — 면제 조건 검사(헬스체크, 정적 파일 등).
Skip이면 그냥 프록시만 합니다. - 요청 바디 캡처 —
capture_request_body가 설정된 경우 바디를 읽고, inner service에는 동일 바이트를 재조립해서 넘깁니다. - inner service 호출 — 실제 핸들러가 실행됩니다.
- 응답 바디 캡처 —
capture_response_body가 설정된 경우 응답 바이트를 읽습니다. - target_extractor 실행 — 요청·응답 정보로 타깃 엔티티를 파생합니다.
- post-filter — 응답을 보고 최종 면제 여부 결정(예: 304는 건너뜀).
emit_unchecked— 이벤트를 파이프라인에 넣습니다. 논블로킹이므로 응답 경로를 막지 않습니다.
바디 캡처를 켜면 내용 전체를 메모리에 올립니다. max_capture_bytes(기본 1 MiB)를 초과하는 바디는 캡처하지 않고 크기 메모만 남깁니다. 큰 파일 업로드 엔드포인트는 캡처를 끄거나 한계를 낮추세요.
① tower Layer가 "데코레이터 패턴"이라는 말의 의미를, 양파 비유로 설명해 보세요.
② pre-filter와 post-filter의 차이는 무엇인가요? 각각 어떤 경우에 쓸까요?
③ .layer(audit_layer) 한 줄이 내부적으로 어떤 타입 변환을 만들어내는지 생각해 보세요.