개발자

인기 러스트 웹 프레임워크 5종 "내게 맞는 것은 무엇일까?"

Serdar Yegulalp 2024.06.14
지난 10년 동안 서로 조금씩 다른 사용자와 기능 요구를 염두에 두고 만들어진 많은 러스트 웹 프레임워크가 등장했다. 모두 형식 안전성, 메모리 안전성, 속도, 정확성과 같은 러스트의 장점을 잘 살린 프레임워크다.
 
ⓒ Getty Images Bank

이 기사에서는 가장 인기 있는 5개 러스트 웹 프레임워크인 액틱스 웹(Actix Web), 로켓(Rocket), 워프(Warp), 액섬(Axum), 포엠(Poem)에 대해 간략히 살펴본다. 모두 웹 서비스를 위한 일반적인 요소인 라우팅, 요청 처리, 복수 응답 형식, 미들웨어를 제공한다. 참고로 이들 프레임워크는 템플릿은 제공하지 않는다. 템플릿은 일반적으로 별도의 크레이트에서 처리된다.
 

액틱스 웹

액틱스 웹은 러스트에서 가장 인기 있는 웹 프레임워크로, 거의 모든 주요 요구사항을 충족한다. 고성능이고 광범위한 서버 기능을 지원하며 번거로운 절차 없이 기본적인 사이트를 구성할 수 있다.
 
"액틱스 웹"이라는 이름의 기원은 actix 액터 프레임워크에 대한 종속성에 있지만, 꽤 오래 전에 대부분의 종속성을 떨쳐냈다. 액틱스 웹의 모든 기능은 러스트 스테이블 브랜치에서 사용이 가능하다.
 
액틱스 웹으로 된 기본적인 "hello world" 앱은 다음과 같다.
 

use actix_web::{get, App, HttpResponse, HttpServer, Responder};

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

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().service(hello))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

hello() 함수의 get() 속성은 서비스할 경로를 나타내지만 .service() 메서드를 사용해 App 객체에 추가될 때까지는 활성화되지 않는다. 또한 액틱스 웹은 더 고급 경로 구조도 지원한다. 예를 들어 URL에서 위치 변수를 캡처해서 get()을 사용하지 않는 함수로 요청을 라우팅하는 데 사용할 수 있다.
 
성능은 액틱스 웹의 큰 장점이다. 모든 요청과 응답은 개별적인 형식으로 처리된다. 서버는 스레드 풀을 사용해서 요청을 처리하며, 성능을 극대화하기 위해 스레드 간에는 아무것도 공유되지 않는다. 필요한 경우 Arc<>를 사용해 수동으로 상태를 공유할 수 있지만, 액틱스 웹의 유지관리자들은 성능 저하를 이유로 작업자 스레드를 차단하는 어떠한 행위도 지양할 것을 권한다. 장시간 실행되는 비 CPU 바인딩 작업의 경우 Future 또는 async를 사용한다.
 
액틱스 웹은 오류 코드에 대한 형식 기반 핸들러도 제공하며, 내장된 미들웨어 시스템을 사용하여(개발자가 직접 사용할 수도 있음) 로깅을 구현한다. 이 프레임워크에는 쿠키를 기본 저장 형식으로 사용하는 범용 사용자 세션 관리 시스템도 포함되지만 원할 경우 다른 시스템을 추가할 수 있다. 정적 파일과 디렉터리 역시 자체 전용 핸들러를 통해 제공이 가능하다.
 
액틱스 웹에는 많은 일반적인 웹 서비스 함수는 물론 일반적이지 않은 몇몇 함수도 번들로 포함된다. 여기에는 양식을 위한 URL 인코딩 본문 처리, HTTPS/2로의 자동 승격, Brotli, gzip, deflate, zstd 압축 데이터의 압축 해제, 청크 인코딩 처리가 포함된다. 웹소켓의 경우 액틱스 웹에는 주된 종속 항목인 actix-web-actors 크레이트가 필요하다. 마찬가지로 멀티파트 스트림의 경우 actix-multipart 크레이트가 필요하다. JSON으로의 변환 또는 그 반대 변환에는 serde 및 serde_json을 사용한다. 러스트 사용자라면 일반적으로 익숙할 것이다.
 
액틱스 웹은 2020년 unsafe 코드 사용에 대한 과도한 비판을 이유로 원래의 유지관리자가 프로젝트에서 탈퇴하는 사건으로 홍역을 치렀지만 다른 리드 유지관리자가 계속해서 프레임워크를 개발하면서 이후 몇 년 동안 성장해왔다. unsafe 코드는 대부분 제거됐다.
 

로켓

러스트 웹 프레임워크에서 로켓의 큰 차별점은 최소한의 코드로 최대한의 결과를 얻을 수 있게 해준다는 점이다. 로켓에서 작성하는 기본적인 웹 애플리케이션에는 코드 라인도 많이 필요하지 않고 의례적인 절차도 거의 없다. 로켓은 이를 위해 러스트의 형식 시스템을 사용하여 많은 동작을 설명해서 컴파일 시에 적용해 인코딩할 수 있도록 한다.
 
로켓을 사용한 기본적인 "hello world" 앱은 다음과 같다.
 

#[macro_use] extern crate rocket;

#[get("/")]
fn hello_world() -> &'static str {
    "Hello, world!"
}

#[launch]
fn rocket() -> _ {
    rocket::build().mount("/", routes![hello_world])
}

로켓은 속성을 사용해서 매우 간결하게 작동한다. 경로는 메서드와 URL 패턴에 대한 속성으로 데코레이션된다. 이 예제에서 볼 수 있듯이 #[launch] 속성은 경로를 마운트하고 요청을 수신하도록 애플리케이션을 설정하는 데 사용되는 함수를 가리킨다.
 
"hello world" 예제의 경로는 동기적이지만 로켓의 경로는 비동기적일 수 있고, 가능한 경우 대체로 비동기적이 되어야 한다. 로켓은 동기화 작업을 비동기로 변환하는 등의 작업을 처리하기 위해 기본적으로 tokio 런타임을 사용한다.
 
로켓은 요청을 처리하기 위한 많은 일반적인 기능(예를 들어 URL 요소에서 변수 추출하기)을 제공한다. 한 가지 독특한 기능은 로켓의 FromRequest 트레잇을 구현하는 러스트 형식을 사용하여 경로에 대한 유효성 검사 정책을 설명하는 "요청 가드" 기능이다.
 
예를 들어 맞춤형 형식을 만들어서 요청 헤더에 특정 정보가 존재하고 이를 검증할 수 없는 경우(예를 들어 이와 연결된 특정 권한이 있는 쿠키) 경로가 실행되지 않도록 차단할 수 있다. 이를 통해 권한 등을 러스트의 컴파일 타임 형식 안전성 안에 구축할 수 있다.
 
유용하면서 독특한 로켓의 또 다른 기능은 로켓 버전의 미들웨어인 페어링(fairing)이다. Fairing 트레잇을 구현하는 형식은 요청 또는 응답과 같은 이벤트에 콜백을 추가하는 데 사용할 수 있다. 그러나 페어링은 요청을 변경하거나 중단할 수는 없다(요청 데이터의 복사본에 액세스할 수는 있음).
 
페어링은 전역 동작(로깅, 성능 지표 수집, 전체적인 보안 정책)이 있는 요소에 가장 적합하다. 인증과 같은 작업에는 요청 가드를 사용하라.
 

워프

워프가 다른 러스트 웹 프레임워크와 크게 구분되는 점은 체인으로 연결해 서비스를 만들 수 있는 조합 가능한 구성요소, 워프 용어로 "필터"를 사용하는 방식이다.
 
워프로 된 기본적인 "hello world"에는 이 기능이 잘 드러나지 않지만 프레임워크가 얼마나 간결할 수 있는지는 볼 수 있다.
 

use warp::Filter;

#[tokio::main]
async fn main() {
    let hello = warp::path!().map(|| "Hello world");
    warp::serve(hello).run(([127, 0, 0, 1], 8080)).await;
}

필터는 Filter 트레잇을 구현하는데, 각 필터는 다른 필터로 출력을 전달해 동작을 수정할 수 있다. 이 예제에서 warp::path는 .map()과 같은 다른 작업에 연결해 함수를 적용할 수 있는 필터다.
 
워프 문서에 나온 또 다른 예제에서는 필터 시스템을 더 자세히 볼 수 있다.
 

use warp::Filter;

let hi = warp::path("hello")
    .and(warp::path::param())
    .and(warp::header("user-agent"))
    .map(|param: String, agent: String| {
        format!("Hello {}, whose agent is {}", param, agent)
    });

여러 필터를 체인으로 연결해서 다음과 같은 동작을 순서에 따라 생성한다.
 
  1. 경로 hello를 사용해 엔드포인트를 설정한다.
  2. 경로 끝에 매개변수를 추가해서 경로가 /hello/<something> 형식이 되도록 한다. (.and() 메서드는 워프에서 구성이 동작하는 방식 중 하나) 
  3. user-agent 헤더를 위한 파서를 추가해서 user-agent 헤더가 없는 수신 요청이 처리되지 않도록 한다.
  4. format! 매크로를 매개변수 param(수집된 매개변수), agent(user-agent 문자열)와 함께 문자열에 적용하고 이를 클라이언트에 반환한다.
 
조합 방식을 선호하는 개발자라면 워프가 개발자의 작업을 보완하는 방식이 마음에 들 것이다.
 
조합식 접근에 따르는 한 가지 특징은 동일한 작업을 다양한 방법으로 할 수 있는데 그 방법이 모두 직관적이지는 않다는 점이다. 워프 리포지토리의 예제를 통해 워프를 사용해서 일반적인 프로그래밍 시나리오를 해결하는 다양한 방법을 살펴볼 것을 권한다.
 
또 다른 특징은 컴파일 시 필터의 작동 방식과 관련된다. 많은 필터의 많은 경로를 구성하면 런타임에는 경로의 속도가 빠르지만 컴파일 시간이 더 길어질 수 있다. 다른 옵션은 경로가 많은 사이트에 동적 디스패치를 사용하는 방법이다. 이 경우 런타임 성능 측면에서 약간 손해를 본다. BoxedFilter 형식을 사용해서 이를 구현하는 방법을 보여주는 예제가 있다.
 

액섬

액섬 프레임워크는 모든 종류의 클라이언트/서버 애플리케이션을 위한 tower 크레이트 생태계와 async를 위한 tokio를 기반으로 한다. 즉, 이미 tower 경험이 있거나 관련 프로젝트에서 사용하고 있다면 액섬을 더 쉽게 사용할 수 있다.
 
액섬 문서에 포함된 기본적인 액섬 "hello world" 앱은 다음과 같다. 액틱스 등과 큰 차이가 없음을 알 수 있다.
 

use axum::{
    routing::get,
    Router,
};

#[tokio::main]
async fn main() {
    let app = Router::new().route("/", get(|| async { "Hello, World!" }));
    let listener = tokio::net::TcpListener::bind("127.0.0.1:8080").await.unwrap();
    axum::serve(listener, app).await.unwrap();
}

경로와 핸들러의 작동 방식 측면에서 액섬이 사용하는 패턴은 상당수가 액틱스와 동일하다. 경로 핸들러 함수는 .route() 메서드로 Router 객체에 추가되며 axum::extract 모듈에는 URL 구성요소 또는 POST 페이로드를 추출하기 위한 형식이 포함돼 있다. 응답은 IntoResponse 트레잇을 구현하며 오류는 tower의 자체적인 tower::Service Error 형식을 통해 처리된다.
 
주요 액섬 구성요소에 대해 tower에 의존하는 이 마지막 동작에는 액섬이 미들웨어를 처리하는 방식도 포함된다. 라우터, 메서드, 개별 핸들러에는 모두 tower 객체에서 다양한 .layer 메서드를 통해 미들웨어가 적용된다. 또한 tower::ServiceBuilder를 사용해서 여러 계층으로 구성된 집합을 만들어 함께 적용할 수도 있다.
 
액섬은 웹 서비스의 다른 일반적인 패턴을 위한 자체 툴을 제공한다. 예를 들어 핸들러 간의 상태 공유는 State 형식을 통해 형식 안전성을 확보하는 방식으로 가능하다. 정상 종료, 데이터베이스 연결 설정과 같은 일반적인 시나리오를 구현하는 방법은 액섬의 예제 디렉터리에서 찾아볼 수 있다.
포엠

대부분의 언어에는 필요한 모든 기능을 갖춘 "맥시멀리스트" 웹 프레임워크(예를 들어 파이썬의 장고), 그리고 작고 간결한 "미니멀리스트 웹 프레임워크(예를 들어 다시 파이썬의 보틀)가 각각 하나 이상 있다. 포엠은 러스트를 위한 미니멀리스트 프레임워크에 해당하며, 기본적인 웹 서비스를 구축하기에 충분한 정도의 기능만 제공한다.
 
다음 "hello world" 예제는 사용자 이름이 URL에 포함된 경우 이를 반영한다.
 

use poem::{get, handler, listener::TcpListener, web::Path, Route, Server};

#[handler]
fn hello(Path(name): Path<String>) -> String {
    format!("hello: {}", name)
}

#[tokio::main]
async fn main() -> Result<(), std::io::Error> {
    let app = Route::new().at("/hello/:name", get(hello));
    Server::new(TcpListener::bind("0.0.0.0:3000"))
        .run(app)
        .await
}

경로를 설정하고 URL과 핸들러를 이 경로에 바인딩하고 요청에서 요소를 추출하는 등 이 앱의 많은 기능은 지금까지 살펴본 다른 프레임워크 및 예제를 통해 이미 익숙할 것이다.
 
포엠은 컴파일 시간을 짧게 유지하기 위해 기본적으로 특정 기능을 위한 지원 요소를 설치하지 않는다. 쿠키, CSRF 프로젝션, HTTP over TLS, 웹소켓, 국제화, 요청/응답 압축과 압축 해제를 비롯해서 그 외에도 여러 요소를 모두 수동으로 활성화해야 한다.
 
포엠은 단순하지만 여전히 풍부한 유틸리티를 제공한다. 일반적인 유용한 미들웨어 요소를 다양하게 갖추고 있으며, 직접 구현하기도 매우 쉽다. 한 가지 사려 깊은 편의 기능 중 하나는 요청 경로의 일관성을 확보하기 위한 메커니즘인 NormalizePath다. 여기에는 URL의 후행 슬래시를 처리하기 위한 범용 핸들러가 포함된다. 이 핸들러를 사용하면 선호하는 포맷을 애플리케이션 전체에 한 번에, 일관적으로 구현할 수 있다.
 
포엠의 예제 디렉터리는 여기서 소개한 다른 프레임워크에 비해 규모가 작은 편이지만, 예를 들어 AWS 람다와 포엠 함께 사용하기, 오픈API 사양에 부합하는 API 생성하기와 같이 주로 세부적인 문서가 필요한 예제에 초점을 둔다.
 

내게 가장 적합한 러스트 프레임워크는?

액틱스 웹은 전체적으로 고르게 균형이 잘 잡힌 솔루션이며 특히 성능이 목표인 경우 적합하다. 로켓을 사용하면 짧은 코드로 우수한 표현력을 얻을 수 있다. 또한 로켓의 "페어링" 시스템은 미들웨어 동작을 구현하기 위한 강력한 메타포를 제공한다.
 
구성 가능한 요소로 작업하기를 좋아하는 프로그래머라면 뛰어난 표현력으로 경로와 워크플로우를 프로그램 방식으로 구축할 수 있게 해주는 워프를 사용해볼 만하다. 액섬은 주로 tower 생태계에 이미 익숙한 러스트 사용자를 대상으로 하지만 그 외의 환경에서도 충분히 사용할 수 있을 만큼 유용하다. 포엠은 기본적으로 간단하며, 가장 기본적인 라우팅과 요청 처리만 필요하다면 좋은 선택이다. 필요한 경우 추가 기능을 설치할 수도 있다.
editor@itworld.co.kr
Sponsored
IDG 설문조사

회사명 : 한국IDG | 제호: ITWorld | 주소 : 서울시 중구 세종대로 23, 4층 우)04512
| 등록번호 : 서울 아00743 등록발행일자 : 2009년 01월 19일

발행인 : 박형미 | 편집인 : 박재곤 | 청소년보호책임자 : 한정규
| 사업자 등록번호 : 214-87-22467 Tel : 02-558-6950

Copyright © 2024 International Data Group. All rights reserved.