Rust 열거형 및 매칭을 사용한 강력한 웹 핸들러 구축: 상태 머신
Emily Parker
Product Engineer · Leapcell

소개
웹 개발의 세계에서 복잡한 사용자 상호 작용이나 다단계 프로세스를 처리하는 것은 종종 다양한 상태를 관리하는 것으로 귀결됩니다. 사용자 온보딩 흐름, 주문 처리 파이프라인 또는 여러 유효성 검사 단계를 거치는 간단한 양식 제출을 상상해 보십시오. 이러한 프로세스가 상태 간에 원활하고 예측 가능하게 전환되도록 하는 것은 원활한 사용자 경험과 강력한 백엔드를 위해 매우 중요합니다. 구조화된 접근 방식 없이는 이러한 상태 변경을 관리하는 것이 스파게티 코드로 빠르게 이어질 수 있으며, 추적하기 어려운 버그와 영원히 좌절스러운 개발 경험을 초래할 수 있습니다. 이것이 바로 계산을 위한 형식 모델을 제공하여 특정 상태를 추적하고 응답하는 상태 머신의 개념이 빛나는 지점입니다. Rust는 강력한 enum 및 match 구성을 통해 이러한 상태 머신을 웹 핸들러에 구현할 수 있는 탁월한 방법을 제공하여 보다 유지보수하기 쉽고 오류에 강하며 이해하기 쉬운 코드를 만들 수 있습니다. 이 글에서는 이러한 Rust 기능을 활용하여 웹 애플리케이션 내에서 강력한 상태 머신을 구축하는 방법을 자세히 살펴보겠습니다.
핵심 개념
구현 세부 사항을 자세히 살펴보기 전에 Rust 웹 핸들러에서 상태 머신을 이해하는 데 기본이 되는 몇 가지 핵심 개념을 명확히 해 보겠습니다.
- Enum (열거형 타입): Rust에서
enum은 몇 가지 다른 변형 중 하나가 될 수 있는 타입을 정의할 수 있게 해줍니다. 각 변형은 선택적으로 다른 타입과 양의 데이터를 보유할 수 있습니다. 이는 각 상태가 특정 컨텍스트 정보를 포함할 수 있는 고유한 상태를 나타내는 데 이상적입니다.enum은 Rust의 강력한 타입 시스템 및 패턴 매칭 기능의 핵심 기능입니다. - Match 표현식: Rust의
match표현식은 값을 일련의 패턴과 비교하고 일치하는 패턴에 따라 코드를 실행할 수 있게 해주는 제어 흐름 구성 요소입니다. 이 표현식은 포괄적이므로 (_로 명시적으로 제외하지 않는 한) 매칭하는 타입에 대한 모든 가능한 경우를 다루어야 합니다. 컴파일 타임의 이러한 포괄성 검사는 런타임에 처리되지 않는 상태가 없도록 하는 강력한 안전망입니다. - 상태 머신 (State Machine): 상태 머신은 시스템이 특정 상태에 있을 때 어떻게 작동하는지와 외부 입력 또는 이벤트에 따라 한 상태에서 다른 상태로 어떻게 변경되는지를 설명하는 계산의 수학적 모델입니다. 유한한 수의 상태, 상태 간의 전환, 상태 내 또는 전환 중에 수행되는 작업을 갖습니다.
- 웹 핸들러 (Web Handler): Axum, Actix-Web 또는 Warp와 같은 웹 프레임워크로 구축된 웹 애플리케이션의 맥락에서 웹 핸들러(또는 라우트 핸들러)는 들어오는 HTTP 요청을 수신하고 처리한 다음 HTTP 응답을 생성하는 함수입니다. 이러한 핸들러는 애플리케이션 로직에 대한 외부 상호 작용의 진입점입니다.
강력한 상태 머신 구현
Rust의 enum 및 match를 사용하여 웹 핸들러 내에서 상태 머신을 구현하면 비교할 수 없는 안전성과 명확성을 얻을 수 있습니다. enum은 가능한 상태를 정의하고 match는 이벤트 또는 요청에 대한 응답으로 시스템이 어떻게 전환되고 작동하는지를 결정합니다.
다단계 사용자 등록 프로세스를 고려해 보겠습니다.
시나리오: 사용자 등록 프로세스
저희 등록 프로세스는 다음과 같은 상태를 가집니다.
Initial: 사용자가 등록을 시작했습니다.ProfileDetails: 사용자가 기본 프로필 정보(예: 이름, 이메일)를 제공했으며 유효성을 검사해야 합니다.AccountConfirmation: 사용자의 프로필 세부 정보가 유효하며 이메일 확인을 기다리고 있습니다.Completed: 사용자가 계정을 성공적으로 확인했습니다.
그리고 다음과 같은 이벤트/작업이 있습니다.
- 초기 양식 제출
- 프로필 세부 정보 제출
- 이메일 링크 확인
enum으로 상태 정의하기
먼저 enum을 사용하여 상태를 정의해 보겠습니다. 각 상태는 관련 데이터를 보유할 수 있습니다.
#[derive(Debug, PartialEq)] enum RegistrationState { Initial, ProfileDetails { user_id: String, email: String, full_name: String, }, AccountConfirmation { user_id: String, email: String, token: String, }, Completed { user_id: String, }, // 더 복잡한 흐름을 위해 PendingValidation, Rejected 등을 추가할 수 있습니다. }
여기서 ProfileDetails 및 AccountConfirmation 변형은 해당 상태와 관련된 데이터를 보유합니다. 이를 통해 풍부한 컨텍스트 정보가 현재 상태에 직접 연관될 수 있습니다.
웹 핸들러에서 match를 사용한 전환 처리
이제 등록 요청을 처리하는 웹 핸들러를 상상해 보겠습니다. 데이터베이스나 세션에 현재 상태를 저장한다고 시뮬레이션해 보겠습니다. 이 예에서는 시연 목적으로 간단한 인메모리 HashMap을 "세션 저장소"로 사용하겠습니다.
use std::collections::HashMap; use std::sync::{Arc, Mutex}; use axum::{ extract::{Path, State}, http::StatusCode, response::{IntoResponse, Html}, Json, }; use serde::{Deserialize, Serialize}; // 요청/응답 본문용 // 저희 Axum 핸들러를 위한 일반적인 상태 type AppState = Arc<Mutex<HashMap<String, RegistrationState>>>; // 초기 세부 정보 제출을 위한 요청 본문 #[derive(Debug, Deserialize)] struct SubmitProfileDetails { email: String, full_name: String, } // 계정 확인을 위한 요청 본문 #[derive(Debug, Deserialize)] struct ConfirmAccount { token: String, } // 상태 업데이트를 위한 응답 본문 #[derive(Debug, Serialize)] struct StateResponse { current_state: String, message: String, } // 예: 등록 단계 처리를 위한 웹 핸들러 시뮬레이션 async fn process_registration_step( Path(user_id): Path<String>, State(app_state): State<AppState>, Json(payload): Json<serde_json::Value>, // 시연을 위해 제네릭 Value 사용 ) -> impl IntoResponse { let mut store = app_state.lock().unwrap(); let current_state = store.entry(user_id.clone()) .or_insert_with(|| RegistrationState::Initial); // 이것은 `match`와 함께 상태 머신 로직이 살아나는 곳입니다. let (next_state, response_message) = match current_state { RegistrationState::Initial => { // Initial에서 ProfileDetails로 전환 시도 if let Ok(details) = serde_json::from_value::<SubmitProfileDetails>(payload) { // 프로필 세부 정보 저장 및 확인 토큰 생성 시뮬레이션 let new_state = RegistrationState::ProfileDetails { user_id: user_id.clone(), email: details.email.clone(), full_name: details.full_name, }; (Some(new_state), "Profile details submitted. Please confirm your email.".to_string()) } else { (None, "Invalid profile details provided.".to_string()) } }, RegistrationState::ProfileDetails { user_id: current_id, email, .. } => { // ProfileDetails에서 AccountConfirmation으로 전환 시도 if let Ok(confirm) = serde_json::from_value::<ConfirmAccount>(payload) { // 토큰 유효성 검사 시뮬레이션 (예: 이 사용자/이메일에 대한 저장된 값과 비교) if confirm.token == "correct_token_for_email_confirmation" { // 실제 유효성 검사로 대체 let new_state = RegistrationState::AccountConfirmation { user_id: current_id.clone(), email: email.clone(), token: confirm.token, }; (Some(new_state), "Account awaiting email verification.".to_string()) } else { (None, "Invalid confirmation token.".to_string()) } } else { (None, "Waiting for email confirmation.".to_string()) } }, RegistrationState::AccountConfirmation { user_id: current_id, token, .. } => { // 외부 이벤트 시뮬레이션 (예: 올바른 토큰으로 이메일 링크 클릭) // 이 예에서는, 현재 토큰이 페이로드 토큰과 일치하면 완료됩니다. if let Ok(confirm) = serde_json::from_value::<ConfirmAccount>(payload) { if confirm.token == *token { // 이미 확인되었으며 저장된 토큰과 일치 let new_state = RegistrationState::Completed { user_id: current_id.clone(), }; (Some(new_state), "Registration complete!".to_string()) } else { (None, "Invalid confirmation details for this stage.".to_string()) } } else { // 이 단계에서 다른 것을 제출하려고 하면 (None, "Registration requires account confirmation.".to_string()) } }, RegistrationState::Completed { .. } => { (None, "User registration already completed.".to_string()) }, }; if let Some(new_state) = next_state { let state_name = format!("{:?}", new_state); *current_state = new_state; (StatusCode::OK, Json(StateResponse { current_state: state_name, message: response_message, })).into_response() } else { // 상태 변경 없음, 그러나 매칭 로직에 기반한 피드백 제공 let state_name = format!("{:?}", current_state); (StatusCode::BAD_REQUEST, Json(StateResponse { current_state: state_name, message: response_message, })).into_response() } }
설명 및 이점
- 명확한 상태 정의:
RegistrationState열거형은 모든 가능한 상태를 명시적으로 정의하여 시스템의 동작을 즉시 이해할 수 있게 합니다. - 포괄적인 패턴 매칭:
match표현식은 모든RegistrationState변형을 고려하도록 강제합니다. 변형을 잊어버리면 Rust 컴파일러가 오류를 발생시켜 런타임에 처리되지 않은 상태를 방지합니다. 이것은 거대한 컴파일 타임 안전 보장입니다. - 상태 종속 로직: 각
match팔 내의 로직은 특정 상태에 따라 달라집니다. 시스템은 예상되는 입력 종류와 현재 상태에서 허용되는 전환을 올바르게 식별합니다. 주어진 상태에 대한 잘못된 동작(예: 초기 세부 정보만 제출되었을 때 이메일 확인 시도)은 우아하게 거부될 수 있습니다. - 상태에서 데이터 추출: 패턴 매칭을 사용하여 상태 변형에 저장된 관련 데이터(예:
ProfileDetails의user_id,email)를 쉽게 추출할 수 있습니다. - 유지보수성: 등록 프로세스가 발전함에 따라 새로운 상태를 추가하거나 전환을 수정하는 것은
enum정의와 해당match팔에 국한되어 영향이 최소화됩니다. - 가독성: 코드 구조는 상태 머신의 흐름을 자연스럽게 반영하여 새로운 개발자가 이해하고 기여하기 쉽게 만듭니다.
애플리케이션 시나리오
이 패턴은 많은 웹 핸들러 시나리오에 적용할 수 있는 매우 다재다능합니다.
- 주문 처리:
PendingConfirmation,Processing,Shipped,Delivered,Cancelled. - 승인 워크플로우:
Draft,SubmittedForReview,Approved,Rejected. - API 페이징:
InitialLoad,LoadingNextPage,Complete. - 사용자 온보딩:
PendingProfile,PendingVerification,Active.
결론
Rust의 enum 및 match 표현식을 활용함으로써 개발자는 웹 핸들러 내에서 매우 강력하고 유지보수 가능한 상태 머신을 직접 구축할 수 있습니다. 이 접근 방식은 처리되지 않은 상태에 대한 컴파일 타임 보장을 제공하고, 상태별 로직에 대한 비교할 수 없는 명확성을 제공하며, 궁극적으로 더 복원력 있고 이해하기 쉬운 웹 애플리케이션을 만듭니다. 복잡한 워크플로우를 관리하기 위해 Rust의 이러한 핵심 기능을 활용하는 것은 백엔드 서비스의 안전성과 유지보수성을 크게 향상시킬 것입니다.

