Axum 및 Actix Web의 사용자 지정 Extractor를 통한 핸들러 간소화
Olivia Novak
Dev Intern · Leapcell

소개
Rust를 사용한 웹 개발의 세계에서 Axum 및 Actix Web과 같은 프레임워크는 성능, 안전성 및 간결성 덕분에 상당한 주목을 받고 있습니다. 애플리케이션이 복잡해짐에 따라 요청 핸들러는 헤더를 구문 분석하거나, 쿼리 매개변수를 검증하거나, 요청 본문을 역직렬화하는 등 반복적인 코드로 어수선해지는 경우가 많습니다. 이렇게 하면 핵심 비즈니스 로직이 가려져 핸들러를 읽고, 테스트하고, 유지 관리하기가 더 어려워집니다. 다행히 Axum과 Actix Web 모두 이러한 반복성을 추상화할 수 있는 강력한 메커니즘인 사용자 지정 요청 Extractor를 제공합니다. 이를 활용하면 핸들러 로직을 순수한 본질로 증류하여 더 깔끔하고 유지 관리하기 쉬우며 궁극적으로 더 즐거운 코드베이스를 만들 수 있습니다. 이 문서는 사용자 지정 Extractor를 만드는 이유와 방법을 살펴보고 웹 애플리케이션 핸들러를 단순화하는 데 유용함을 시연합니다.
사용자 지정 Extractor 이해
구현에 대해 자세히 알아보기 전에 웹 프레임워크의 사용자 지정 Extractor와 관련된 몇 가지 주요 용어를 명확히 해 보겠습니다.
요청 핸들러: 웹 프레임워크에서 핸들러는 들어오는 HTTP 요청을 처리하고 HTTP 응답을 반환하는 함수입니다. 애플리케이션 로직이 주로 거주하는 곳입니다.
Extractor: Extractor는 구조화되고 재사용 가능한 방식으로 들어오는 HTTP 요청에서 데이터를 "추출"할 수 있도록 하는 메커니즘입니다. 각 핸들러 내에서 수동으로 요청 객체를 검사하는 대신 Extractor는 이 로직을 캡슐화하여 핸들러 인수로 직접 사용할 수 있는 데이터 유형을 제공합니다. 일반적인 내장 Extractor에는 Json
, Query
, Path
, HeaderMap
, State
가 있습니다.
미들웨어: 관련이 있지만 미들웨어 함수는 다른 계층에서 작동합니다. 핸들러 전 또는 후에 실행되어 요청 또는 응답을 수정하거나 로깅 또는 인증과 같은 교차 관심사를 수행할 수 있습니다. 반면에 Extractor는 핸들러에 데이터를 구문 분석하고 제공하도록 특별히 설계되었습니다.
사용자 지정 Extractor의 핵심 아이디어는 개발자가 핸들러 서명에 직접 주입할 수 있는 자체 유형을 정의할 수 있도록 하는 것입니다. 이렇게 하면 모듈성, 테스트 용이성이 향상되고 코드 중복이 줄어듭니다. 핸들러가 호출되면 프레임워크는 들어오는 Request
에서 필요한 정보를 추출하여 이러한 사용자 지정 유형을 자동으로 인스턴스화합니다.
사용자 지정 Extractor의 원칙
Axum
Axum에서 Extractor는 FromRequestParts
또는 FromRequest
트레이트를 구현하는 모든 유형입니다.
FromRequestParts
는 Extractor가 요청 부분(헤더, 메서드, URI 등)에 대한 불변의 액세스만 필요하고 요청 본문을 소비하지 않는 경우 사용됩니다.FromRequest
는 Extractor가 요청 본문을 소비하거나 요청을 수정해야 하는 경우 사용됩니다.FromRequest
를 구현할 때 일반적으로 부품만 필요한 경우FromRequestParts
에 위임하거나request.into_body()
와 직접 상호 작용합니다.
두 트레이트 모두 추출이 실패할 경우 반환될 연결된 Error
유형과 from_request_parts
또는 from_request
비동기 메서드가 필요합니다.
Actix Web
Actix Web에서 사용자 지정 Extractor는 사용자 지정 유형에 대해 FromRequest
트레이트를 구현하여 생성됩니다. 이 트레이트는 HttpRequest
및 Payload
를 인수로 받는 from_request
비동기 메서드를 제공합니다. Result<Self, Self::Error>
를 반환하며, 여기서 Self::Error
는 사용자 지정 오류 유형입니다.
실제 적용: 인증된 사용자 Extractor
일반적인 시나리오, 즉 JWT를 포함하는 Authorization
헤더에서 인증된 사용자를 추출하는 시나리오를 설명해 보겠습니다.
Axum 구현
먼저 User
구조체와 간단한 JWT 유효성 검사 함수가 있다고 가정해 보겠습니다.
// src/models.rs #[derive(Debug, Clone)] pub struct User { pub id: u32, pub username: String, } // src/auth.rs (예시로 단순화됨) pub async fn validate_jwt_and_get_user(token: &str) -> Option<User> { // 실제 애플리케이션에서는 JWT 디코딩, 서명 확인 및 // 잠재적으로 데이터베이스 조회가 포함될 것입니다. // 단순화하기 위해 "valid_token"인지 확인해 보겠습니다. if token == "valid_token_abc" { Some(User { id: 1, username: "john_doe".to_string() }) } else { None } }
이제 사용자 지정 AuthUser
Extractor를 정의해 보겠습니다.
// src/extractors.rs use axum::{ async_trait, extract::{FromRequestParts, TypedHeader}, headers::{authorization::{Bearer, Authorization}, Header}, http::{request::Parts, StatusCode}, response::{IntoResponse, Response}, Json, }; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUser(pub User); #[async_trait] impl FromRequestParts for AuthUser { type Rejection = AuthError; async fn from_request_parts(parts: &mut Parts, _state: &Self::State) -> Result<Self, Self::Rejection> { let TypedHeader(Authorization(bearer)) = parts .extract::<TypedHeader<Authorization<Bearer>>>() .await .map_err(|_| AuthError::InvalidToken)?; let token = bearer.token(); let user = validate_jwt_and_get_user(token) .await .ok_or(AuthError::InvalidToken)?; Ok(AuthUser(user)) } } pub enum AuthError { InvalidToken, // 기타 관련 인증 오류 추가 } impl IntoResponse for AuthError { fn into_response(self) -> Response { let (status, error_message) = match self { AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), // 기타 오류 처리 }; (status, Json(json!({"error": error_message}))) .into_response() } }
그리고 핸들러가 이를 사용하는 방법입니다.
// src/main.rs use axum::{routing::get, Router}; use std::net::SocketAddr; use crate::extractors::AuthUser; use crate::models::User; mod auth; mod extractors; mod models; async fn protected_handler(AuthUser(user): AuthUser) -> String { format!("Welcome, {} (ID: {})!", user.username, user.id) } #[tokio::main] async fn main() { let app = Router::new() .route("/protected", get(protected_handler)); let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); println!("listening on {}", addr); axum::Server::bind(&addr) .serve(app.into_make_service()) .await .unwrap(); }
유효한 Authorization: Bearer valid_token_abc
헤더를 사용하여 /protected
로 요청을 보내면 "Welcome, john_doe (ID: 1)!"이 반환됩니다. 토큰이 유효하지 않거나 누락된 경우 Axum은 AuthError::into_response
에 정의된 JSON 오류 메시지와 함께 401 Unauthorized
를 자동으로 반환합니다.
Actix Web 구현
마찬가지로 Actix Web의 경우에도 동일한 User
구조체와 validate_jwt_and_get_user
함수를 사용합니다.
// src/extractors.rs use actix_web::{ dev::Payload, error::ResponseError, http::{header, StatusCode}, web::Bytes, FromRequest, HttpRequest, HttpResponse, }; use futures::future::{ready, Ready}; use serde::Serialize; use serde_json::json; use crate::{auth::validate_jwt_and_get_user, models::User}; pub struct AuthUserActix(pub User); // 인증 실패에 대한 사용자 지정 오류 #[derive(Debug, Serialize)] pub enum AuthErrorActix { MissingToken, InvalidToken, } impl std::fmt::Display for AuthErrorActix { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{:?}", self) } } impl ResponseError for AuthErrorActix { fn error_response(&self) -> HttpResponse { let (status, message) = match self { AuthErrorActix::MissingToken => (StatusCode::UNAUTHORIZED, "Authorization token not found"), AuthErrorActix::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid authentication token"), }; HttpResponse::build(status) .json(json!({"error": message})) } } impl FromRequest for AuthUserActix { type Error = AuthErrorActix; type Future = Ready<Result<Self, Self::Error>>; fn from_request(req: &HttpRequest, _payload: &mut Payload) -> Self::Future { let auth_header = req.headers().get(header::AUTHORIZATION); let user_future = async move { let token = auth_header .ok_or(AuthErrorActix::MissingToken)? .to_str() .map_err(|_| AuthErrorActix::InvalidToken)? // 잘못된 헤더 값 .strip_prefix("Bearer ") .ok_or(AuthErrorActix::InvalidToken)?; // Bearer 토큰이 아님 let user = validate_jwt_and_get_user(token) .await .ok_or(AuthErrorActix::InvalidToken)?; Ok(AuthUserActix(user)) }; // Actix FromRequest는 비동기가 아니므로 비동기 블록을 Future로 변환하고 // 즉시 폴링합니다 (사용 가능한 도우미가 동기 작업의 경우). // 실제 비동기 작업을 `from_request` 내에서 수행하려면 일반적으로 작업을 스폰하거나 `web::block`을 사용합니다. // 이 예시를 위해 `Ready` 반환 유형에 맞추기 위해 **await**한 결과를 사용합니다. // Axum의 `validate_jwt_and_get_user`가 비동기라고 가정하고, 이를 `Ready` 타입에 넣기 위해 `futures::executor::block_on`을 사용합니다. // 실제 Actix 앱에서는 `web::block` 또는 Proper Async Future를 사용해야 합니다. ready(req.headers().get(header::AUTHORIZATION) .ok_or(AuthErrorActix::MissingToken) .and_then(|h_value| h_value.to_str().map_err(|_| AuthErrorActix::InvalidToken) ) .and_then(|s| s.strip_prefix("Bearer ").ok_or(AuthErrorActix::InvalidToken) ) .and_then(|token| // 이 await는 `ready` 외부에 있거나 동기 `FromRequest` 변형에 `web::block`을 사용해야 합니다. // 데모를 위해 결과를 시뮬레이트합니다. futures::executor::block_on(validate_jwt_and_get_user(token)) .ok_or(AuthErrorActix::InvalidToken) ) .map(AuthUserActix) ) } }
그리고 Actix Web 핸들러:
// src/main.rs use actix_web::{get, App, HttpResponse, HttpServer, Responder}; use crate::extractors::AuthUserActix; mod auth; mod extractors; mod models; #[get("/protected")] async fn protected_handler_actix(user: AuthUserActix) -> impl Responder { format!("Welcome, {} (ID: {})!", user.0.username, user.0.id) } #[actix_web::main] async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new() .service(protected_handler_actix) }) .bind(("127.0.0.1", 8080))? // 포트 8080으로 바인딩 .run() .await }
Actix Web에 Authorization: Bearer valid_token_abc
헤더와 함께 /protected
로 요청을 하면 "Welcome, john_doe (ID: 1)!"이 반환됩니다. 잘못되었거나 누락된 토큰은 JSON 오류와 함께 401 Unauthorized
응답을 생성합니다.
이점 및 사용 사례
위의 예제는 사용자 지정 Extractor의 몇 가지 주요 이점을 강조합니다.
- 더 깔끔한 핸들러: 핸들러 서명은
User
객체를 직접 받으므로 핸들러의 목적이 즉시 명확해지고 함수 본문의 상용구 코드가 줄어듭니다. - 코드 재사용성:
AuthUser
(또는AuthUserActix
) 로직은 한 번 정의되고 애플리케이션의 모든 보호된 핸들러에서 사용할 수 있습니다. - 향상된 테스트 용이성: 추출 로직은
FromRequestParts
/FromRequest
구현 내에서 격리됩니다. 이렇게 하면 핸들러와 독립적으로 추출 로직에 대한 단위 테스트를 작성하기가 더 쉬워집니다. - 오류 처리: 사용자 지정 오류를 정의하고 적절한 HTTP 응답으로 자동 변환하여 특정 관심사에 대한 오류 처리를 중앙 집중화할 수 있습니다.
- 캡슐화: (JWT 유효성 검사와 같은) 복잡한 로직이 캡슐화되어 핸들러 함수로 누출되지 않습니다.
인증 외에도 사용자 지정 Extractor는 다음과 같은 경우에 매우 유용합니다.
- 테넌트 ID 추출: 다중 테넌트 애플리케이션의 경우 Extractor는
X-Tenant-ID
헤더 또는 서브도메인을 구문 분석하여 테넌트 컨텍스트를 제공할 수 있습니다. - 권한/역할 확인: 핸들러가 실행되기 전에 사용자 역할을 추출하고 지정된 엔드포인트에 필요한 권한이 있는지 확인합니다.
- 복잡한 쿼리 매개변수 구문 분석: 논리적 단위를 형성하는 관련 쿼리 매개변수가 많은 경우(예: 페이징 필터
page
,limit
,sort_by
), 이러한Query
매개변수를 받아 단일PaginationParams
구조체를 구성하는 Extractor를 만들 수 있습니다. - 세션 관리: 세션 데이터 검색 또는 업데이트.
- API 키 유효성 검사: 헤더에서 유효한 API 키 확인.
결론
사용자 지정 요청 Extractor는 Axum 및 Actix Web에서 개발자 경험을 크게 향상시킵니다. 반복적이고 관심사별 로직을 핸들러 함수 밖으로 옮길 수 있기 때문입니다. FromRequestParts
/ FromRequest
트레이트를 구현하면 원시 요청 데이터를 비즈니스 로직에 사용할 수 있는 구조화된 준비된 유형으로 변환하는 강력한 재사용 가능한 구성 요소를 구축할 수 있습니다. 이렇게 하면 초점을 맞춘 읽기 쉬운 유지 관리가 쉬운 핸들러가 만들어져 Rust의 웹 개발 프로세스가 간소화됩니다. 사용자 지정 Extractor를 활용하는 것은 견고하고 잘 구조화된 웹 애플리케이션을 구축하기 위한 중요한 단계입니다.