Rust의 Axum 및 Actix Web을 사용한 API 버전 관리 탐색
Lukas Schneider
DevOps Engineer · Leapcell

소개
웹 애플리케이션이 발전함에 따라 API 또한 필연적으로 변화합니다. 새로운 기능이 추가되고, 기존 기능은 폐기되며, 데이터 구조는 개선됩니다. 이러한 변경 사항이 기존 클라이언트 애플리케이션을 손상시키지 않도록 하려면 강력한 API 버전 관리가 필수적인 관행이 됩니다. 명확한 전략 없이는 API 업데이트가 심각한 호환성 문제, 좌절한 사용자, 그리고 과중한 유지보수 부담으로 이어질 수 있습니다. Rust 웹 생태계에서 Axum 및 Actix Web과 같은 프레임워크는 성능이 뛰어나고 안정적인 API 구축을 위한 강력한 도구를 제공합니다. 이 글에서는 API 버전 관리의 두 가지 주요 전략, 즉 URL 경로 사용과 Accept
헤더 활용을 심도 있게 다루고, 이러한 인기 있는 Rust 프레임워크의 맥락에서 구현 세부 정보, 장점 및 단점을 살펴봅니다.
API 버전 관리 전략 이해
세부 사항을 살펴보기 전에 API 버전 관리와 관련된 핵심 개념을 정의해 보겠습니다. API 버전 관리는 API의 여러 버전을 동시에 유지 관리하여 클라이언트가 상호 작용할 버전을 선택할 수 있도록 하는 관행입니다. 이를 통해 기존 클라이언트에 영향을 미치는 호환성이 없는 변경 사항을 방지하는 동시에 새로운 클라이언트가 최신 기능을 활용할 수 있습니다.
우리가 탐색할 두 가지 주요 전략은 다음과 같습니다.
- URL 경로 버전 관리: API 버전을 URL 경로에 직접 포함시키는 것입니다. 예를 들어,
/api/v1/users
및/api/v2/users
와 같습니다. - Accept 헤더 버전 관리: 여기서 클라이언트는
Accept
HTTP 헤더 내에 사용자 정의 미디어 타입을 지정하여 원하는 API 버전을 지정합니다. 예를 들어,Accept: application/vnd.myapi.v1+json
입니다.
URL 경로 버전 관리
원칙 및 구현
URL 경로 버전 관리는 아마도 가장 직관적이고 널리 채택된 방법일 것입니다. 버전 번호는 엔드포인트 경로의 명확하고 가시적인 부분입니다. 이는 개발자가 어떤 버전을 사용하고 있는지 직관적으로 이해할 수 있게 하고 간단한 라우팅 규칙을 허용합니다.
Rust에서 Axum과 Actix Web 모두 라우팅 메커니즘을 통해 URL 경로 버전 관리를 매우 자연스럽게 구현할 수 있도록 합니다.
Axum 예제
Axum의 라우팅은 서비스 기반이며, 버전 접두사가 있는 경로를 정의하는 것이 매우 깔끔합니다.
use axum::* routing::get, Router, response::IntoResponse, ; async fn get_users_v1() -> impl IntoResponse { "Getting users from API V1!" } async fn get_users_v2() -> impl IntoResponse { "Getting users from API V2!" } #[tokio::main] async fn main() { let app = Router::new() .nest("/api/v1", Router::new().route("/users", get(get_users_v1))) .nest("/api/v2", Router::new().route("/users", get(get_users_v2))); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
이 Axum 예제에서 /api/v1
및 /api/v2
에 대한 두 개의 중첩된 Router
인스턴스를 생성합니다. 각 인스턴스는 /users
엔드포인트를 독립적으로 처리하여 해당 버전별 핸들러로 요청을 라우팅합니다.
Actix Web 예제
Actix Web은 라우팅을 위해 매크로와 함수 속성을 사용하며, 이는 경로 기반 버전 관리에도 매우 적합합니다.
use actix_web::* get, web, App, HttpServer, Responder ; #[get("/api/v1/users")] async fn get_users_v1_actix() -> impl Responder { "Getting users from API V1!" } #[get("/api/v2/users")] async fn get_users_v2_actix() -> impl Responder { "Getting users from API V2!" } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(get_users_v1_actix) .service(get_users_v2_actix) }) .bind(("127.0.0.1", 8080))? .run() .await }
여기서 Actix Web의 #[get]
매크로는 버전을 포함한 전체 경로를 직접 정의하여 매우 명시적입니다. 각 버전의 핸들러는 별도의 서비스로 등록됩니다.
URL 경로 버전 관리의 장점
- 검색 용이성: URL에서 버전이 즉시 보이므로 개발자와 문서에서 쉽게 알 수 있습니다.
- 캐싱: 프록시와 캐시는 다른 버전을 완전히 별도의 리소스로 취급할 수 있어 캐싱 전략을 단순화합니다.
- 브라우저 호환성: 웹 브라우저를 통해 다른 API 버전에 직접 액세스할 수 있습니다.
- 단순성: 라우팅은 일반적으로 구현하고 이해하기 쉽습니다.
URL 경로 버전 관리의 단점
- URL 오염: URL에 버전 번호가 포함되어 더 길고 덜 세련될 수 있습니다.
- 클라이언트 측 "고정": URL에 버전 번호를 하드코딩한 클라이언트는 새 버전이 기본값이 되거나 이전 버전이 폐기될 때 대규모로 업데이트해야 할 수 있습니다.
- 라우팅 오버헤드: 많은 엔드포인트가 버전 관리되는 경우 관리해야 할 경로가 더 많을 수 있습니다.
Accept 헤더 버전 관리
원칙 및 구현
Accept 헤더 버전 관리(콘텐츠 협상이라고도 함)는 Accept
HTTP 헤더에 의존합니다. 클라이언트는 API 버전을 포함하는 사용자 정의 미디어 타입을 지정하고 서버는 그에 따라 응답합니다. 이를 통해 URL이 깔끔하게 유지되고 동일한 URL 경로에서 리소스의 여러 버전을 제공할 수 있습니다.
사용자 정의 미디어 타입은 일반적으로 application/vnd.company.resource.vX+json
과 같은 규칙을 따릅니다.
이 전략을 구현하려면 Accept
헤더를 검사하고 해당 내용에 따라 요청을 라우팅해야 합니다.
Axum 예제
Axum의 추출기는 헤더 기반 버전 관리를 구현하는 데 탁월합니다. Accept
헤더를 구문 분석하는 사용자 정의 추출기를 만들 수 있습니다.
use axum::* routing::get, Router, async_trait, extract::{FromRequestParts, Request, State}, http::{header, request::Parts}, response::{IntoResponse, Response} ; use std::collections::HashMap; // API 버전용 사용자 정의 추출기 pub struct ApiVersion(pub u8); #[async_trait] impl<S> FromRequestParts<S> for ApiVersion where S: Send + Sync, { type Rejection = Response; async fn from_request_parts(parts: &mut Parts, _state: &S) -> Result<Self, Self::Rejection> { let accept_header = parts.headers .get(header::ACCEPT) .and_then(|h| h.to_str().ok()) .unwrap_or_default(); // 예: "application/vnd.myapi.v1+json" 구문 분석 if let Some(version_str) = accept_header.find("vnd.myapi.v") { let start = version_str + "vnd.myapi.v".len(); if let Some(end) = accept_header[start..].find('+') { if let Ok(version) = accept_header[start..start + end].parse::<u8>() { return Ok(ApiVersion(version)); } } } // 특정 버전 또는 지원되지 않는 버전이 요청되지 않은 경우 기본값으로 버전 1 Ok(ApiVersion(1)) } } async fn get_users_versioned(ApiVersion(version): ApiVersion) -> impl IntoResponse { match version { 1 => "Getting users from API V1 via Accept header!".to_string(), 2 => "Getting users from API V2 via Accept header!".to_string(), _ => format!("Unsupported API version: {}", version), } } #[tokio::main] async fn main() { let app = Router::new() .route("/api/users", get(get_users_versioned)); let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap(); println!("Listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); }
Axum에서는 Accept
헤더를 처리하는 사용자 정의 추출기로 ApiVersion
을 정의합니다. from_request_parts
구현은 헤더에서 버전 번호를 구문 분석하려고 시도합니다. get_users_versioned
핸들러는 이 ApiVersion
을 사용하여 실행할 논리를 결정합니다.
Actix Web 예제
Actix Web은 사용자 정의 추출기를 지원하며 헤더 검사에 대한 강력한 지원을 제공합니다.
use actix_web::* web, App, HttpServer, HttpRequest, HttpResponse, Responder, HttpMessage ; use actix_web::http::header::{ACCEPT, CONTENT_TYPE}; // Actix Web에서 API 버전용 사용자 정의 추출기 struct ApiVersionActix(u8); impl actix_web::FromRequest for ApiVersionActix { type Error = actix_web::Error; type Future = std::future::Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _: &mut actix_web::dev::Payload) -> Self::Future { let accept_header = req.headers() .get(ACCEPT) .and_then(|h| h.to_str().ok()) .unwrap_or_default(); let mut version = 1; // V1로 기본값 설정 if let Some(version_str_idx) = accept_header.find("vnd.myapi.v") { let start = version_str_idx + "vnd.myapi.v".len(); if let Some(end_idx) = accept_header[start..].find('+') { if let Ok(parsed_version) = accept_header[start..start + end_idx].parse::<u8>() { version = parsed_version; } } } std::future::ready(Ok(ApiVersionActix(version))) } } async fn get_users_versioned_actix(api_version: ApiVersionActix) -> impl Responder { match api_version.0 { 1 => HttpResponse::Ok().body("Getting users from API V1 via Accept header!"), 2 => HttpResponse::Ok().body("Getting users from API V2 via Accept header!"), _ => HttpResponse::BadRequest().body(format!("Unsupported API version: {}", api_version.0)), } } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service( web::resource("/api/users") .route(web::get().to(get_users_versioned_actix)) ) }) .bind(("127.0.0.1", 8080))? .run() .await }
Actix Web의 ApiVersionActix
도 FromRequest
를 구현하여 핸들러 함수의 인수로 사용할 수 있습니다. Accept
헤더를 구문 분석하는 논리는 Axum 예제와 유사합니다.
Accept 헤더 버전 관리의 장점
- 깔끔한 URL: URL은 API 버전에 관계없이 동일하게 유지되어 미관과 공개 API의 SEO를 개선합니다.
- 여러 버전에 대한 단일 엔드포인트: 동일한 경로
/api/users
가 클라이언트가 요청한 버전에 따라 사용자 표현 방식을 다르게 제공할 수 있습니다. - 유연성: 클라이언트가 전체 URL 구조를 변경하지 않고도 특정 버전을 쉽게 요청할 수 있습니다.
Accept 헤더 버전 관리의 단점
- 검색 용이성 부족: 버전이 헤더 내에 숨겨져 있어 네트워크 요청을 검사하지 않고는 어떤 버전이 액세스되는지 명확하지 않습니다.
- 캐싱 복잡성: 캐싱 프록시는 캐시 키에
Accept
헤더를 포함해야 하므로 캐시 실패로 이어질 수 있으므로 응답을 효과적으로 캐싱하는 데 어려움을 겪을 수 있습니다. - 브라우저 비호환성: 브라우저는 일반적으로
Accept
헤더를 직접 사용자 정의하도록 허용하지 않으므로 웹 브라우저를 통해 버전에 직접 액세스하기 어렵습니다. - 오류 처리: 지원되지 않는 버전을 요청하고 있거나
Accept
헤더가 잘못 형식화되었음을 클라이언트에게 전달하기 어려울 수 있습니다.
올바른 전략 선택
URL 경로와 Accept 헤더 버전 관리 간의 선택은 종종 몇 가지 주요 고려 사항으로 귀결됩니다.
- API 대상: 검색 용이성과 직접적인 브라우저 액세스가 중요한 공개 API의 경우 URL 경로 버전 관리가 선호될 수 있습니다. 내부 또는 기계 간 API의 경우 Accept 헤더 버전 관리가 더 깔끔한 URL을 제공합니다.
- 캐싱 요구 사항: 캐싱이 중요하고 외부 프록시를 사용하여 구현된 경우 URL 경로 버전 관리가 캐싱 전략을 단순화합니다.
- 클라이언트 측 유연성: 클라이언트가 동일한 리소스에 대해 여러 버전 간에 자주 전환해야 하는 경우 Accept 헤더 버전 관리가 더 적응력이 좋습니다.
- 아키텍처 단순성: URL 경로 버전 관리는 대부분의 개발자가 일반적으로 생각하고 처음 구현하기 더 쉽습니다.
이러한 전략이 상호 배타적이지 않다는 점에 유의할 가치가 있습니다. 일부 API는 하이브리드 접근 방식을 사용하거나 사소한 변경에 대해 날짜 기반 버전 관리를 사용할 수도 있습니다. 그러나 대부분의 일반적인 사용 사례의 경우 이 두 가지 중 하나로 충분할 것입니다.
결론
API 버전 관리는 확장 가능하고 유지 관리 가능한 웹 서비스를 설계하는 데 없어서는 안 될 측면입니다. Rust 생태계에서 Axum과 Actix Web은 URL 경로 및 Accept 헤더 버전 관리 모두를 효과적으로 구현하는 데 필요한 도구와 유연성을 제공합니다. URL 경로 버전 관리는 단순성과 검색 용이성을 제공하는 반면, Accept 헤더 버전 관리는 더 깔끔한 URL과 리소스 표현에서의 유연성을 제공합니다. 프로젝트에 맞는 올바른 전략을 신중하게 선택하면 API의 수명과 사용성을 크게 향상시킬 수 있습니다.