Rust 웹 API의 우아한 오류 처리 및 통합 응답
Olivia Novak
Dev Intern · Leapcell

소개
웹 API 개발은 현대 소프트웨어의 초석입니다. 이러한 API가 복잡해짐에 따라 다양한 시나리오, 특히 예기치 않은 시나리오를 관리하는 것이 중요해집니다. 오류 처리는 종종 나중에 생각하게 되어 일관성 없는 응답, 열악한 디버깅 경험, 좌절한 클라이언트를 초래합니다. 마찬가지로 통합된 응답 형식의 부족은 API 소비를 번거롭게 만들 수 있으며, 클라이언트가 다른 엔드포인트 또는 오류 유형에 대해 분산된 로직을 구현하도록 요구합니다. 강력한 유형 시스템과 안정성에 중점을 둔 Rust 생태계에서는 기능적으로 정확할 뿐만 아니라 오류를 우아하게 처리하고 예측 가능하며 개발자 친화적인 응답을 제공하는 웹 서비스를 구축할 수 있는 독특한 기회가 있습니다. 이 글에서는 Rust 웹 API를 위한 견고한 오류 처리 전략과 통합 응답 형식을 설계하는 방법을 안내하여 유지 관리성과 사용성을 모두 향상시킵니다.
견고한 API를 위한 핵심 개념
구현에 뛰어들기 전에 오류 및 응답과 관련하여 효과적인 API 설계의 기초가 되는 몇 가지 기본 개념을 명확히 해 보겠습니다.
통합 응답 형식
통합 응답 형식은 요청이 성공했는지 또는 오류가 발생했는지에 관계없이 모든 API 응답에 대한 표준화된 구조를 정의합니다. 일반적으로 status
, message
, data
(성공적인 페이로드의 경우), errors
(오류 세부 정보의 경우)와 같은 공통 필드를 포함합니다. 이러한 일관성은 클라이언트 측 구문을 단순화하고 API를 사용하는 개발자의 인지 부하를 줄입니다.
사용자 지정 오류 유형
사용자 지정 오류 유형은 애플리케이션 내의 특정 오류 조건을 캡슐화하는 열거형 또는 구조체입니다. 일반적인 HTTP 상태 코드에만 의존하는 대신 사용자 지정 오류는 더 풍부하고 도메인별 컨텍스트를 제공합니다. 예를 들어, UserNotFound
, InvalidCredentials
또는 DatabaseConnectionError
등이 있습니다. 이러한 오류는 클라이언트를 위해 적절한 HTTP 상태 코드 및 자세한 메시지로 매핑될 수 있습니다.
오류 전파
오류 전파는 오류가 적절하게 처리될 때까지 호출 스택을 통해 전달되는 방식을 말합니다. Rust에서는 주로 Result
열거형(Ok(T)
또는 Err(E)
)과 ?
연산자를 통해 달성되며, 이를 통해 간결한 오류 전달이 가능합니다. 효과적인 전파는 오류가 조용히 삭제되지 않고 항상 사용자 친화적인 응답으로 변환될 수 있는 지점에 도달하도록 보장합니다.
직렬화/역직렬화를 위한 Serde
Serde는 Rust의 강력하고 고성능 직렬화/역직렬화 프레임워크입니다. 이를 통해 Rust 구조체 및 열거형을 JSON, YAML, Bincode와 같은 다양한 데이터 형식으로 변환하고 그 반대로 변환할 수 있습니다. 웹 API의 경우 Serde는 Rust 데이터 구조(사용자 지정 오류 유형 및 통합 응답 포함)를 클라이언트 소비를 위한 JSON으로 변환하고 들어오는 요청에 대해 그 반대로 변환하는 데 필수적입니다.
우아한 오류 처리 및 통합 응답 구현
인기 있는 actix-web
프레임워크를 사용하여 웹 API를 구축하는 데 중점을 둔 실제 Rust 코드로 이러한 개념을 설명해 보겠습니다. 예제에서는 actix-web
을 사용하지만, 원칙은 Axum
또는 Warp
와 같은 다른 Rust 웹 프레임워크에도 적용할 수 있습니다.
1. 통합 응답 구조 정의
먼저 통합 응답 형식을 정의해 보겠습니다. 성공적인 데이터 또는 오류 목록을 모두 보유할 수 있는 일반 ApiResponse
구조체를 만듭니다.
use serde::{Serialize, Deserialize}; use actix_web::{web, HttpResponse, ResponseError, http::StatusCode}; use std::fmt; /// 표준 API 응답을 나타냅니다. #[derive(Debug, Serialize, Deserialize)] #[serde(untagged)] // 외부 태그 없이 유연한 직렬화를 허용합니다. pub enum ApiResponse<T> { Success { status: String, message: String, #[serde(flatten)] // 데이터를 메인 객체로 평면화합니다. data: T, }, Error { status: String, message: String, errors: Vec<ApiErrorDetail>, }, } /// API 응답에 대한 자세한 오류 메시지를 나타냅니다. #[derive(Debug, Serialize, Deserialize)] pub struct ApiErrorDetail { code: String, field: Option<String>, message: String, } impl<T> ApiResponse<T> { pub fn success(data: T, message: impl Into<String>) -> Self { ApiResponse::Success { status: "success".to_string(), message: message.into(), data, } } pub fn error(errors: Vec<ApiErrorDetail>, message: impl Into<String>) -> Self { ApiResponse::Error { status: "error".to_string(), message: message.into(), errors, } } }
여기서 ApiResponse<T>
는 제네릭 데이터 T
를 가진 Success
이거나 ApiErrorDetail
목록을 가진 Error
일 수 있습니다. #[serde(untagged)]
속성은 중요합니다. Serde에 `