러스트(Rust) 실전 프로젝트 예제 따라하기 시리즈 - 2편: 간단한 웹 서버 애플리케이션 만들기

이전 글에서는 러스트를 활용해 간단한 CLI 유틸리티를 만들며, 명령행 인자 파싱, 파일 시스템 접근, 에러 처리, 테스트, 패키징 등을 체험해보았습니다. 이번에는 CLI 범위를 벗어나, 웹 서버 애플리케이션을 직접 구현해보며 러스트 생태계가 제공하는 네트워크 프로그래밍의 강점을 살펴보겠습니다.

 

이번 프로젝트의 주요 목표는 다음과 같습니다.

  • 간단한 HTTP 서버 구현: 기본적인 HTTP 요청을 받아 “Hello, world!” 혹은 간단한 JSON 응답을 반환하는 서버를 만들어봅니다.
  • Actix-web 프레임워크 사용: 러스트 생태계의 대표적인 웹 프레임워크 중 하나인 Actix-web을 통해 라우팅, 핸들러 작성, 응답 처리 방법을 익힙니다.
  • 비동기/async 지원 이해: 비동기 IO 모델을 활용해 효율적인 요청 처리를 시도합니다.
  • 에러 처리, 테스트, 린팅, 문서화: 이전 글에서 경험한 부분들을 웹 서버 맥락에서도 재활용하며, 프로덕션 코드에 가까운 서버 애플리케이션 설계를 체험해봅니다.

C++로 웹 서버를 구현할 때는 Boost.Asio, cpprestsdk, Crow 등을 고려해야 하고, 비동기 모델 구현 시 복잡한 코드나 포인터 관리가 필요했습니다. 러스트와 Actix-web, async/await 조합은 이러한 난관을 언어 차원에서 줄여주어, 안전하고 안정적인 웹 서버를 보다 쉽게 작성할 수 있습니다.

프로젝트 개요

이번 프로젝트는 간단한 웹 서버를 구현할 것입니다.

  • 주요 기능:
    • / 경로로 접근 시 단순한 "Hello, world!" 문자열 반환
    • /api/greet?name=... 경로로 접근 시 쿼리 파라미터로 받은 name 값을 포함한 JSON 응답 반환
  • 목표:
    • Actix-web 기반 기본 라우팅, 요청 파싱, JSON 응답 반환 방식 이해
    • 비동기 처리(Async/Await) 패턴 체득
    • 테스트 코드로 라우팅 핸들러 검증하기

프로젝트 초기화

새 프로젝트를 만들고 Actix-web 및 serde(직렬화/역직렬화), anyhow(에러 처리), tokio(비동기 런타임) 등을 추가하겠습니다.

cargo new web_server_example
cd web_server_example

Cargo.toml을 수정합니다.

[package]
name = "web_server_example"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
anyhow = "1.0"

# actix-web은 비동기 런타임으로 tokio를 사용하므로 추가
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }

간단한 서버 핸들러 작성

actix-web을 사용하면 함수나 클로저 형태로 요청 핸들러를 정의한 뒤, App에 라우팅 정보를 등록할 수 있습니다. main 함수는 #[actix_web::main] 매크로로 비동기 런타임으로 변환할 수 있습니다.

// src/main.rs
use actix_web::{get, web, App, HttpServer, Responder, HttpResponse};
use serde::Serialize;
use anyhow::Result;

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello, world!")
}

#[derive(Serialize)]
struct GreetResponse {
    message: String,
}

async fn greet_handler(query: web::Query<std::collections::HashMap<String, String>>) -> impl Responder {
    let name = query.get("name").cloned().unwrap_or_else(|| "Guest".to_string());
    let response = GreetResponse {
        message: format!("Hello, {}!", name),
    };
    HttpResponse::Ok().json(response)
}

#[actix_web::main]
async fn main() -> Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .route("/api/greet", web::get().to(greet_handler))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await?;

    Ok(())
}

여기서 HttpServer::new 안에 App::new()를 정의하고, .service(hello)로 매핑한 / 라우트, 그리고 .route("/api/greet", ...)로 매핑한 /api/greet 라우트를 등록했습니다.

hello 핸들러는 단순한 문자열을 반환하고, greet_handler는 쿼리 파라미터에서 name을 가져와 JSON으로 응답합니다.

서버 실행 및 테스트

cargo run으로 서버를 실행한 뒤 브라우저나 curl을 통해 접속해보세요.

  • curl http://127.0.0.1:8080/ → "Hello, world!" 응답
  • curl http://127.0.0.1:8080/api/greet?name=Rust → {"message":"Hello, Rust!"} 응답

쿼리 파라미터가 없으면 {"message":"Hello, Guest!"}를 반환합니다.

테스트 코드 작성

테스트는 통합 테스트를 위해 tests 디렉토리를 활용합니다. Actix-web에서는 actix_web::test 모듈을 통해 테스트 서버를 간편히 생성할 수 있습니다.

mkdir tests
touch tests/integration_test.rs

tests/integration_test.rs:

use web_server_example::greet_handler; // greet_handler를 public으로 노출해야 합니다.
// src/main.rs 상단에 `pub async fn greet_handler...`로 변경하거나, 별도 모듈로 추출하세요.

use actix_web::{test, App, web};
use serde_json::Value;

#[actix_web::test]
async fn test_greet_handler() {
    let app = test::init_service(
        App::new().route("/api/greet", web::get().to(greet_handler))
    ).await;

    let req = test::TestRequest::get().uri("/api/greet?name=Rust").to_request();
    let resp = test::call_and_read_body_json::<Value>(&app, req).await;

    assert_eq!(resp["message"], "Hello, Rust!");
}

여기서 greet_handler를 테스트하기 위해 코드 구조를 조정해야 할 수 있습니다. 예를 들어 greet_handler를 pub으로 공개하거나, 별도 mod handlers;로 분리한 뒤 pub fn greet_handler(...)로 정의하면 tests에서 임포트할 수 있습니다.

cargo test로 테스트를 실행해보면 핸들러 로직 검증이 가능합니다.

코드 품질 관리

cargo fmt, cargo clippy를 이용해 코드 정리와 개선을 수행합니다.

cargo fmt
cargo clippy

에러나 개선점이 있으면 바로잡아 코드 품질을 유지할 수 있습니다.

다음 단계

이 예제는 매우 단순한 웹 서버이지만, 여기서 다양한 확장을 고려할 수 있습니다.

  • 라우트 추가: POST 요청 처리, JSON 바디 파싱, DB 연동 등 실제 백엔드 서비스 로직 접목
  • 에러 처리 개선: 커스텀 에러 타입 정의, map_err나 thiserror 크레이트 사용
  • 성능 개선: 로깅, 캐싱, Arc를 통한 공유 상태 관리, async/await 패턴 최적화
  • C++ 대비 장점 재확인: C++ Asio 기반 서버와 비교했을 때 비동기 모델 구현이 훨씬 명확하고 안전함을 경험

결론

이번 글에서는 러스트와 Actix-web을 사용해 간단한 웹 서버 애플리케이션을 만들어보았습니다. 이를 통해 비동기/async 패턴, 라우팅, JSON 응답, 테스트 방법 등을 익혔으며, C++와 비교했을 때 러스트 기반 비동기 서버 개발이 얼마나 간결하고 안전한지 확인할 수 있었습니다.

 

다음 글에서는 더 복잡한 요구사항을 갖춘 네트워크 애플리케이션이나 WebAssembly 연동, FFI를 통한 C++ 코드 협업 예제 등을 다루며, 실전에서 러스트를 활용하는 방법을 계속 탐구할 예정입니다. 여기서 배운 기반을 바탕으로 더욱 다양하고 강력한 서버 애플리케이션 개발에 도전해보세요!

유용한 링크와 리소스

 

반응형