Rust

Rust 강좌 5 - 안전하고 효율적인 Hyper로 HTTP 요청 처리하기

드리프트2 2024. 9. 19. 09:18

Rust 강좌 5 - 안전하고 효율적인 Hyper로 HTTP 요청 처리하기

안녕하세요! 오늘은 Rust의 대표적인 HTTP 라이브러리인 hyper를 알아볼 텐데요.

 

Rust의 HTTP 라이브러리 생태계는 1.0 이전까지는 변화가 많았지만, 이제는 hyper가 많은 Rust 개발자들의 사랑을 받는 대표 라이브러리로 자리 잡았습니다.

 

이 장에서는 hyper를 클라이언트로 사용하는 방법에 집중해 볼까 합니다.

 

이 라이브러리는 서버 구현도 포함하고 있지만, 대부분의 개발자들은 서버를 직접 만드는 것보다는, 이미 만들어진 서버의 API를 소비하는 경우가 많습니다.

 

그러면, hyper가 어떻게 우리에게 도움을 줄 수 있을까요?


1. 기본적인 요청 보내기

우선, Cargo.toml 파일에 의존성을 추가해 보겠습니다.

[dependencies]
hyper = "~0.7"

 

cargo build 명령을 실행하면, hyper가 의존하는 몇 가지 추가적인 크레이트(예: URL 처리, mimetype 지원, OpenSSL 바인딩 등)를 다운로드하고 컴파일하게 됩니다.

 

이제 첫 번째 HTTP 요청을 작성해 볼 차례입니다!

extern crate hyper;

use std::io::Read;
use hyper::Client;

fn main() {
    let client = Client::new();
    let url = "http://httpbin.org/status/201";
    let mut response = match client.get(url).send() {
        Ok(response) => response,
        Err(_) => panic!("Whoops."),
    };
    let mut buf = String::new();
    match response.read_to_string(&mut buf) {
        Ok(_) => (),
        Err(_) => panic!("I give up."),
    };
    println!("buf: {}", buf);
}

 

이 코드, 좀 길어 보이지 않나요? 에러 처리를 간단하게 unwrap()로 할 수도 있겠지만, 그건 좋은 스타일이 아니죠.

 

panic!을 남발하는 것도 바람직하지 않습니다. HTTP 요청 및 응답 과정에서는 여러 가지 문제가 발생할 수 있기 때문에 에러 처리가 중요합니다.

 

하지만 이 패턴에는 개선의 여지가 있습니다. 좀 더 깔끔하게 바꿔볼까요?


2. 요청 로직 리팩토링

우리는 HTTP 요청 로직을 별도의 함수로 분리하고, try! 매크로를 사용해 코드를 단순화할 수 있습니다.

 

이 매크로는 Result 타입을 처리하는 데 유용한 도구입니다.

fn get_content(url: &str) -> hyper::Result<String> {
    let client = Client::new();
    let mut response = client.get(url).send()?;
    let mut buf = String::new();
    response.read_to_string(&mut buf)?;
    Ok(buf)
}

 

이제 코드는 훨씬 간결해졌습니다.

 

try! 매크로 덕분에 Result 타입의 변수를 명시적으로 매칭하지 않아도 되고, 첫 번째 실패 시 즉시 함수에서 반환되기 때문에 코드 흐름도 명확해졌습니다.


3. POST 요청과 쿼리 파라미터

POST 요청을 보내는 것은 약간 더 복잡하지만, 크게 다르지 않습니다. 이번에는 쿼리 파라미터를 추가로 받는 함수를 작성해 보겠습니다.

type Query<'a> = Vec<(&'a str, &'a str)>;

fn post_query(url: &str, query: Query) -> hyper::Result<String> {
    let client = Client::new();
    let body = form_urlencoded::Serializer::new(String::new())
        .extend_pairs(query.iter())
        .finish();
    let mut response = client.post(url).body(&body[..]).send()?;
    let mut buf = String::new();
    response.read_to_string(&mut buf)?;
    Ok(buf)
}

 

여기서 get_content 함수와의 차이점은 url 크레이트를 사용해 쿼리 파라미터를 직렬화하는 부분에 있습니다.

 

key=value&foo=bar 형태로 요청 본문을 만들고, 이를 body() 메서드에 전달한 후 나머지 코드는 GET 요청과 동일하게 처리됩니다.


4. JSON 데이터 전송하기

이번에는 post_query 함수를 조금 수정해서 구조체를 받아 JSON으로 직렬화한 후 전송해 보겠습니다.

fn post_json<T>(url: &str, payload: &T) -> hyper::Result<String>
    where T: Encodable
{
    let client = Client::new();
    let body = json::encode(payload).unwrap();
    let mut response = client.post(url).body(&body[..]).send()?;
    let mut buf = String::new();
    response.read_to_string(&mut buf)?;
    Ok(buf)
}

 

이 함수는 payload 인자로 아무 구조체나 받을 수 있으며, 이 구조체는 Encodable 트레이트를 구현하고 있어야 합니다.

 

이제 이 함수를 실제로 사용해 봅시다.

#[derive(RustcDecodable, RustcEncodable)]
struct Movie {
    title: String,
    bad_guy: String,
}

fn main() {
    let movie = Movie {
        title: "You Only Live Twice".to_string(),
        bad_guy: "Blofeld".to_string(),
    };

    let query = vec![("key", "value"), ("foo", "bar")];
    println!("{}", post_query("http://httpbin.org/post", query).unwrap());
    println!("{}", post_json("http://httpbin.org/post", &movie).unwrap());
}

 

이 코드는 You Only Live Twice라는 영화 정보를 JSON 형식으로 전송하고, 응답을 출력하는 예제입니다.

 

Movie 구조체는 RustcEncodable을 구현하고 있어 JSON으로 직렬화될 수 있습니다.


5. 결론

오늘 우리는 hyper 라이브러리를 사용해 GETPOST 요청을 처리하고, 쿼리 파라미터와 JSON 데이터를 전송하는 방법을 알아보았습니다.

 

hyper는 Rust에서 널리 사용되는 HTTP 클라이언트 라이브러리로, 다양한 웹 API와의 상호작용을 쉽게 만들어 줍니다.

 

특히, try! 매크로와 같은 Rust의 강력한 에러 처리 도구 덕분에 코드가 간결하고 안전하게 유지됩니다.

 

다음 번에는 더 깊이 있는 주제로, Rust를 사용해 어떻게 더 복잡한 웹 애플리케이션을 만들 수 있는지 살펴보겠습니다.