Rust http 크레이트: HTTP 생태계의 공용 어휘

Rust http 크레이트: HTTP 생태계의 공용 어휘

Rust로 HTTP를 다뤄본 분이라면 한 가지 묘한 사실을 눈치채셨을 거예요. reqwest로 요청을 보내든, hyper로 저수준 서버를 짜든, axum으로 핸들러를 만들든 같은 타입 이름이 자꾸 등장합니다. StatusCode, HeaderMap, Method, Uri 같은 것들이요.

이건 우연이 아닙니다. 이 타입들은 모두 http라는 별도의 크레이트에 정의되어 있고, Rust HTTP 생태계 전체가 이 어휘를 공유하고 있는 거죠. 덕분에 reqwest로 받은 응답의 헤더를 axum 응답에 그대로 옮길 수 있고, tower 미들웨어가 reqwest와 axum 양쪽 모두에서 동작합니다.

이번 글에서는 이 공용 어휘를 제공하는 http 크레이트를 한 바퀴 둘러보겠습니다. 이걸 익혀두면 위에서 언급한 다른 크레이트들이 훨씬 자연스럽게 읽히실 거예요.

왜 별도의 크레이트일까

처음 보면 좀 이상합니다. HTTP 클라이언트라면 Request, Response 같은 타입을 직접 가지고 있어도 됐을 텐데, 왜 외부 크레이트에 뽑아두었을까요?

답은 호환성에 있습니다. hyper가 자신만의 Request 타입을 정의하고 reqwest도 따로 정의했다면, 두 라이브러리 사이에서 데이터를 옮길 때마다 변환 코드를 짜야 합니다. 하지만 둘 다 http::Request를 사용한다면 그냥 넘겨주면 끝나죠.

그래서 hyper 팀이 핵심 타입만 모아 http 크레이트로 분리했고, 이제는 사실상 Rust HTTP 표준 라이브러리처럼 쓰입니다. reqwest, hyper, axum, tower-http, actix-web까지 모두 같은 http 타입을 받고 돌려줍니다.

설치는 간단합니다.

Cargo.toml
[dependencies]
http = "1"

직접 의존성을 추가하는 경우는 많지 않습니다. 보통은 reqwest나 axum 같은 상위 크레이트를 통해 자동으로 끌려오는데, 가끔 명시적으로 http::StatusCode 같은 걸 import해야 할 때 직접 추가합니다.

Method: 요청 메서드

가장 단순한 타입부터 보겠습니다. Method는 HTTP 메서드를 나타내는 열거형인데, 자주 쓰이는 메서드들은 상수로 제공됩니다.

use http::Method;

let m = Method::GET;
assert_eq!(m.as_str(), "GET");

let parsed: Method = "POST".parse().unwrap();
assert_eq!(parsed, Method::POST);

문자열에서 파싱할 수 있으니 사용자 입력이나 설정 파일에서 메서드를 받을 때 편리합니다. 잘못된 문자열을 넣으면 에러를 돌려주므로 안전하죠.

PATCH, OPTIONS, HEAD 같은 비표준에 가까운 메서드도 전부 상수로 들어있고, 커스텀 메서드가 필요하면 Method::from_bytes로 만들 수 있습니다.

StatusCode: 상태 코드

StatusCode는 응답 상태 코드를 표현합니다. 숫자로 비교해도 되지만, 분류를 묻는 메서드가 있어서 코드가 훨씬 읽기 좋아집니다.

use http::StatusCode;

let s = StatusCode::OK;
assert_eq!(s.as_u16(), 200);
assert!(s.is_success());

let not_found = StatusCode::from_u16(404).unwrap();
assert!(not_found.is_client_error());

is_success, is_client_error, is_server_error, is_redirection, is_informational 같은 메서드가 있어서 200~299 범위를 일일이 체크할 필요가 없습니다.

let status = response.status();
if status.is_server_error() {
    // 5xx 응답이면 재시도
}

흔히 쓰는 코드는 StatusCode::OK, NOT_FOUND, INTERNAL_SERVER_ERROR, BAD_REQUEST 같은 상수로 제공되니까 매직 넘버를 쓰지 않아도 됩니다.

Uri: URL 파싱과 분해

Uri는 URL을 분해해서 보여줍니다. 직접 문자열을 자르지 말고 이걸 쓰세요.

use http::Uri;

let uri: Uri = "https://api.example.com/users?id=42".parse().unwrap();

assert_eq!(uri.scheme_str(), Some("https"));
assert_eq!(uri.host(), Some("api.example.com"));
assert_eq!(uri.path(), "/users");
assert_eq!(uri.query(), Some("id=42"));

스킴, 호스트, 경로, 쿼리를 각각 깔끔하게 꺼낼 수 있고, 잘못된 URL은 파싱 단계에서 걸러집니다. 다만 Uri는 쿼리 파라미터를 개별 키-값으로 분해해주지는 않으니, 쿼리 파싱이 필요하면 url 크레이트나 serde_urlencoded를 함께 쓰는 편이 좋습니다.

HeaderMap: 헤더 컬렉션

헤더는 HTTP에서 가장 까다로운 부분 중 하나입니다. 대소문자를 구분하지 않고, 같은 이름의 헤더가 여러 번 등장할 수도 있죠. HeaderMap은 이걸 깔끔하게 처리해줍니다.

use http::{HeaderMap, HeaderValue};

let mut headers = HeaderMap::new();
headers.insert("content-type", HeaderValue::from_static("application/json"));
headers.append("accept", HeaderValue::from_static("text/html"));
headers.append("accept", HeaderValue::from_static("application/xml"));

assert_eq!(headers.get_all("accept").iter().count(), 2);

insert는 같은 이름의 기존 값을 덮어쓰지만, append는 추가합니다. Accept처럼 여러 값을 받는 헤더는 append로 쌓고 get_all로 한꺼번에 꺼내는 패턴이 정석이에요.

HeaderValue가 별도 타입인 이유는 헤더 값에 들어갈 수 없는 문자(개행 같은)를 미리 막기 위해서입니다. 하드코딩된 문자열은 from_static으로 만들면 컴파일 타임에 검증되고, 런타임 문자열은 HeaderValue::from_str 같은 메서드로 안전하게 변환합니다.

Request와 Response: 제네릭 컨테이너

이제 핵심인 Request<T>Response<T>입니다. 둘 다 본문(body) 타입에 대해 제네릭이라는 게 가장 큰 특징입니다.

use http::{Method, Request, Response, StatusCode};

let req: Request<String> = Request::builder()
    .method(Method::POST)
    .uri("https://api.example.com/items")
    .header("content-type", "application/json")
    .body(r#"{"name":"book"}"#.to_string())
    .unwrap();

assert_eq!(req.method(), Method::POST);
assert_eq!(req.uri().path(), "/items");

빌더 패턴이 한눈에 들어옵니다. body()를 호출하는 순간에 Request<String>이 완성되는데, 만약 바이트 슬라이스를 넣으면 Request<&[u8]>, 비워두면 Request<()>가 되는 식이죠.

응답도 똑같습니다.

let res: Response<&str> = Response::builder()
    .status(StatusCode::CREATED)
    .header("location", "/items/1")
    .body("created")
    .unwrap();

assert_eq!(res.status(), StatusCode::CREATED);

본문이 제네릭인 덕분에 라이브러리마다 자기 사정에 맞는 본문 타입을 골라 쓸 수 있습니다. hyper는 스트리밍을 위한 Body, axum은 추상화된 Body, reqwest는 자기만의 응답 본문을 쓰지만 헤더와 상태 코드 같은 메타데이터는 전부 같은 http 타입을 공유합니다.

본문과 메타데이터를 분리해서 다루고 싶을 때는 into_partsfrom_parts가 유용합니다.

let res: Response<String> = Response::builder()
    .status(200)
    .body("hello".to_string())
    .unwrap();

let (parts, body) = res.into_parts();
let res2 = Response::from_parts(parts, body.to_uppercase());
assert_eq!(res2.body(), "HELLO");

본문을 다른 형태로 변환하면서 헤더와 상태 코드는 그대로 유지하고 싶을 때 꼭 필요한 패턴입니다. 미들웨어를 작성할 때 자주 마주치는 모양이죠.

Version: HTTP 버전

HTTP 버전을 표현하는 Version 타입도 있습니다. HTTP_09, HTTP_10, HTTP_11, HTTP_2, HTTP_3 상수가 제공되고, 디버그 출력은 HTTP/2.0 같은 익숙한 형태로 나옵니다.

use http::Version;

let v = Version::HTTP_2;
assert_eq!(format!("{v:?}"), "HTTP/2.0");

실제 코드에서 직접 만들 일은 드물고, 받은 요청의 버전을 확인하거나 로깅에 사용하는 경우가 많습니다.

Extensions: 메타데이터 주머니

마지막으로 좀 특이한 기능을 하나 소개하겠습니다. RequestResponse에는 extensions()라는 메서드가 있는데, 임의의 타입을 끼워 넣을 수 있는 타입 안전한 컨테이너입니다.

use http::Request;

#[derive(Clone, Debug, PartialEq)]
struct RequestId(u64);

let mut req: Request<()> = Request::new(());
req.extensions_mut().insert(RequestId(42));

let id = req.extensions().get::<RequestId>().cloned();
assert_eq!(id, Some(RequestId(42)));

이게 왜 필요한가 싶을 텐데, 미들웨어를 보면 이해됩니다. 인증 미들웨어가 토큰을 검증해서 사용자 정보를 꺼냈다고 해볼게요. 이걸 다음 단계 핸들러에 전달하려면 어디에 넣어야 할까요?

헤더에 넣자니 응답으로 새어 나갈 수 있고, 별도의 채널을 만들자니 너무 복잡합니다. extensions는 바로 이런 용도예요. 타입을 키로 사용하기 때문에 충돌 걱정도 없습니다.

axum의 Extension 추출자, tower의 여러 미들웨어가 이 메커니즘 위에 만들어져 있습니다.

다른 크레이트와의 관계

이쯤 되면 그림이 보이실 거예요. HTTP 관련 크레이트들은 대략 이런 역할 분담을 하고 있습니다.

http는 어휘만 정의합니다. 실제로 네트워크를 통해 요청을 보내거나 받지는 않아요. hyper는 이 어휘 위에서 저수준 HTTP 구현(소켓 IO, 프로토콜 파싱)을 제공합니다. reqwest는 hyper를 감싸서 편한 클라이언트 API를 제공하고, axum은 hyper 위에 라우팅 레이어를 얹습니다. tower와 tower-http는 이 어휘를 입출력으로 하는 미들웨어 추상화를 제공하죠.

그래서 reqwest 응답의 헤더를 axum 응답에 그대로 옮기는 게 가능하고, tower 미들웨어를 reqwest 클라이언트와 axum 서버 양쪽에 적용할 수 있는 겁니다.

마치며

http 크레이트는 평소엔 잘 의식하지 못하지만 Rust HTTP 생태계 전체를 떠받치고 있는 기반입니다. 한 번 익혀두면 reqwest 문서를 읽을 때도, axum 핸들러를 쓸 때도, 직접 미들웨어를 만들 때도 같은 어휘로 이야기할 수 있게 됩니다.

다음 단계로는 이 타입들을 실제로 사용하는 reqwest로 HTTP 요청 보내기Axum으로 REST API 만들기로 넘어가시면 좋습니다. 미들웨어 합성이 궁금하다면 Tower 관련 글들도 같은 어휘로 이어집니다.

더 자세한 내용은 http 크레이트 공식 문서를 참고하세요.

This work is licensed under CC BY 4.0 CC BY

개발자를 위한 뉴스레터

달레가 정리한 AI 개발 트렌드와 직접 만든 콘텐츠를 전해드립니다.

Discord