본문으로 건너뛰기
Previous
Next
Rust 웹 개발 | Actix-web으로 REST API 만들기

Rust 웹 개발 | Actix-web으로 REST API 만들기

Rust 웹 개발 | Actix-web으로 REST API 만들기

이 글의 핵심

Rust로 웹 API를 만들 때 Actix-web을 고르는 경우가 많습니다. Tokio 기반 비동기 런타임 위에서 동작하며, 라우팅·JSON·미들웨어·테스트를 한 프레임워크 안에서 다룰 수 있습니다.

시리즈 안내

#09 | 📋 전체 목차 | 이전: #08 비동기 · 다음: #10 테스팅


들어가며

Rust로 웹 API를 만들 때 Actix-web을 고르는 경우가 많습니다. Tokio 기반 비동기 런타임 위에서 동작하며, 라우팅·JSON·미들웨어·테스트를 한 프레임워크 안에서 다룰 수 있습니다. 이 글에서는 Hello World·CRUD·Todo로 기본을 익힌 뒤, CORS·인증·에러 미들웨어, Arc/Mutex/RwLock 공유 상태, Result·커스텀 에러, sqlx, 통합 테스트, 로깅·환경 변수·서버 설정 등 실무 주제를 이어서 정리합니다.

Rust와의 첫 만남

“빌려주기 검사기(Borrow Checker)와 싸우는 게 프로그래밍의 반”이라는 농담이 있을 정도로, Rust는 처음에 정말 어렵습니다. 저도 첫 프로젝트에서 컴파일러 에러와 씨름하며 “이게 정말 생산성이 높은 언어인가?” 의심했습니다. 하지만 몇 주간 고생 끝에 컴파일이 통과된 코드는 런타임 에러가 거의 없다는 걸 깨달았습니다. C++에서는 세그멘테이션 폴트가 프로덕션에서 터지는 악몽을 자주 겪었는데, Rust는 그런 걱정이 없습니다. 컴파일러가 미리 잡아주니까요. 특히 멀티스레드 코드를 작성할 때 이 차이가 극명합니다. C++에서는 데이터 레이스를 찾느라 디버거와 씨름했지만, Rust는 컴파일 단계에서 “이 코드는 스레드 안전하지 않아”라고 알려줍니다. 처음엔 답답했지만, 지금은 이 엄격함이 감사합니다.

1. Actix-web 설정

Cargo.toml

[dependencies]
actix-web = "4"
tokio = { version = "1", features = [full] }
serde = { version = "1", features = [derive] }
serde_json = "1"

2. 기본 서버

Hello World

use actix_web::{web, App, HttpServer, Responder};
async fn hello() -> impl Responder {
    "Hello, Rust!"
}
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("안녕하세요, {}님!", name)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    println!("서버 시작: http://127.0.0.1:8080");
    
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(hello))
            .route("/greet/{name}", web::get().to(greet))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

3. REST API

데이터 모델

use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
struct User {
    id: u32,
    name: String,
    email: String,
}

CRUD 핸들러

use actix_web::{web, HttpResponse, Result};
use std::sync::Mutex;
struct AppState {
    users: Mutex<Vec<User>>,
}
// GET /users
async fn get_users(data: web::Data<AppState>) -> Result<HttpResponse> {
    let users = data.users.lock().unwrap();
    Ok(HttpResponse::Ok().json(&*users))
}
// GET /users/{id}
async fn get_user(
    data: web::Data<AppState>,
    id: web::Path<u32>,
) -> Result<HttpResponse> {
    let users = data.users.lock().unwrap();
    
    if let Some(user) = users.iter().find(|u| u.id == *id) {
        Ok(HttpResponse::Ok().json(user))
    } else {
        Ok(HttpResponse::NotFound().body("사용자 없음"))
    }
}
// POST /users
async fn create_user(
    data: web::Data<AppState>,
    user: web::Json<User>,
) -> Result<HttpResponse> {
    let mut users = data.users.lock().unwrap();
    users.push(user.into_inner());
    Ok(HttpResponse::Created().finish())
}
// DELETE /users/{id}
async fn delete_user(
    data: web::Data<AppState>,
    id: web::Path<u32>,
) -> Result<HttpResponse> {
    let mut users = data.users.lock().unwrap();
    
    if let Some(pos) = users.iter().position(|u| u.id == *id) {
        users.remove(pos);
        Ok(HttpResponse::NoContent().finish())
    } else {
        Ok(HttpResponse::NotFound().body("사용자 없음"))
    }
}

라우팅 설정

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let app_state = web::Data::new(AppState {
        users: Mutex::new(vec![]),
    });
    
    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .route("/users", web::get().to(get_users))
            .route("/users/{id}", web::get().to(get_user))
            .route("/users", web::post().to(create_user))
            .route("/users/{id}", web::delete().to(delete_user))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

4. 미들웨어

로깅 미들웨어

use actix_web::middleware::Logger;
use env_logger::Env;
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init_from_env(Env::default().default_filter_or("info"));
    
    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .route("/", web::get().to(hello))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

5. 실전 예제

예제: Todo API

use actix_web::{web, App, HttpResponse, HttpServer, Result};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
struct Todo {
    id: u32,
    title: String,
    completed: bool,
}
struct AppState {
    todos: Mutex<Vec<Todo>>,
}
async fn get_todos(data: web::Data<AppState>) -> Result<HttpResponse> {
    let todos = data.todos.lock().unwrap();
    Ok(HttpResponse::Ok().json(&*todos))
}
async fn create_todo(
    data: web::Data<AppState>,
    todo: web::Json<Todo>,
) -> Result<HttpResponse> {
    let mut todos = data.todos.lock().unwrap();
    todos.push(todo.into_inner());
    Ok(HttpResponse::Created().json(&*todos))
}
async fn toggle_todo(
    data: web::Data<AppState>,
    id: web::Path<u32>,
) -> Result<HttpResponse> {
    let mut todos = data.todos.lock().unwrap();
    
    if let Some(todo) = todos.iter_mut().find(|t| t.id == *id) {
        todo.completed = !todo.completed;
        Ok(HttpResponse::Ok().json(todo.clone()))
    } else {
        Ok(HttpResponse::NotFound().body("Todo 없음"))
    }
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let app_state = web::Data::new(AppState {
        todos: Mutex::new(vec![]),
    });
    
    HttpServer::new(move || {
        App::new()
            .app_data(app_state.clone())
            .route("/todos", web::get().to(get_todos))
            .route("/todos", web::post().to(create_todo))
            .route("/todos/{id}/toggle", web::put().to(toggle_todo))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

6. 미들웨어 심화: CORS, 인증, 에러 핸들링

프로덕션 API에서는 브라우저 클라이언트(CORS), 토큰 검증, 공통 에러 응답을 미들웨어로 묶는 경우가 많습니다.

Cargo.toml (추가)

actix-cors = "0.7"

CORS

다른 출처(origin)의 프론트엔드가 API를 호출하려면 Access-Control-* 헤더가 필요합니다. actix-cors로 한 번에 설정합니다.

// 실행 예제
use actix_cors::Cors;
use actix_web::http::header;
use actix_web::{web, App, HttpServer};
HttpServer::new(move || {
    App::new()
        .wrap(
            Cors::default()
                .allowed_origin("http://localhost:3000")
                .allowed_methods(vec!["GET", "POST", "PUT", "DELETE"])
                .allowed_headers(vec![header::AUTHORIZATION, header::ACCEPT])
                .max_age(3600),
        )
        .route("/users", web::get().to(get_users))
        // ...
})

개발 환경에서만 모든 출처를 허용하려면 allowed_origin_fn으로 호스트를 검사하는 편이 안전합니다. 운영에서는 화이트리스트 도메인만 허용하세요.

인증: Bearer 토큰 검사 (간단 패턴)

무거운 OAuth 대신, 헤더에서 토큰을 읽어 검증하는 패턴입니다. 실제로는 JWT 검증·세션 조회 등을 여기에 넣습니다. Actix-web 4에서는 middleware::from_fn 으로 비교적 짧게 작성할 수 있습니다.

// 실행 예제
use actix_web::body::MessageBody;
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::http::header;
use actix_web::middleware::{from_fn, Next};
use actix_web::Error;
async fn bearer_guard(
    req: ServiceRequest,
    next: Next<impl MessageBody>,
) -> Result<ServiceResponse<impl MessageBody>, Error> {
    let token_ok = req
        .headers()
        .get(header::AUTHORIZATION)
        .and_then(|h| h.to_str().ok())
        .and_then(|s| s.strip_prefix("Bearer "))
        .map(|t| !t.is_empty())
        .unwrap_or(false);
    if token_ok {
        // 검증 성공 시: req.extensions_mut().insert(AuthUser { ....});
        next.call(req).await
    } else {
        Err(actix_web::error::ErrorUnauthorized("invalid or missing token"))
    }
}
// App::new().wrap(from_fn(bearer_guard))

표준화된 Bearer 추출이 필요하면 actix-web-httpauthHttpAuthentication::bearer() 와 검증 클로저를 쓰는 편이 깔끔합니다. 핵심은 검증 성공 시 req.extensions_mut()에 사용자 정보를 넣고, 핸들러에서는 req.extensions().get::<AuthUser>()로 꺼내 쓰는 패턴입니다.

에러 핸들링 미들웨어

전역으로 4xx/5xx 응답을 꾸미거나 로깅할 때 ErrorHandlers를 씁니다.

use actix_web::dev::ServiceResponse;
use actix_web::http::{header, StatusCode};
use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
fn on_internal_error<B>(
    mut res: ServiceResponse<B>,
) -> actix_web::Result<ErrorHandlerResponse<B>> {
    // 공통 JSON 스키마·헤더 보강 등
    res.response_mut().headers_mut().insert(
        header::CONTENT_TYPE,
        header::HeaderValue::from_static("application/json; charset=utf-8"),
    );
    Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}
App::new().wrap(
    ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, on_internal_error),
)

운영에서는 trace id를 헤더에 넣고, 5xx 시에만 상세 로그를 남기는 식으로 조합합니다.

7. 상태 관리: Arc, Mutex vs RwLock

web::Data<T>는 내부적으로 Arc 를 감쌉니다. 여러 워커 스레드가 같은 상태를 공유하므로, Send + Sync 가 보장되는 타입을 넣어야 합니다.

방식특징언제 쓰나
Mutex동시에 한 스레드만 T에 접근쓰기가 잦거나, 락 구간이 짧을 때
RwLock읽기는 여러 스레드, 쓰기는 하나조회 ≫ 수정 인 캐시·설정 등
CRUD 예제의 Mutex<Vec<User>>는 구현이 단순하지만, 읽기만 많은 엔드포인트에서는 RwLock이 경합을 줄일 수 있습니다.
use std::sync::{Arc, RwLock};
struct AppState {
    users: RwLock<Vec<User>>,
}
async fn get_users(data: web::Data<AppState>) -> Result<HttpResponse> {
    let users = data.users.read().map_err(|_| actix_web::error::ErrorInternalServerError("lock"))?;
    Ok(HttpResponse::Ok().json(&*users))
}
async fn create_user(
    data: web::Data<AppState>,
    user: web::Json<User>,
) -> Result<HttpResponse> {
    let mut users = data.users.write().map_err(|_| actix_web::error::ErrorInternalServerError("lock"))?;
    users.push(user.into_inner());
    Ok(HttpResponse::Created().finish())
}

실전 패턴

  • 인메모리만으로는 한계가 있으므로, 장기 저장은 DB(sqlx 등)로 넘기고 앱 상태에는 연결 풀이나 설정만 둡니다.
  • 락 안에서 await 하지 않기: 락을 잡은 채로 I/O를 하면 전체 처리량이 무너집니다. DB 작업은 락 밖에서 하고, 공유 구조체에는 필요한 최소 데이터만 보관합니다.
  • unwrap() 대신 map_err로 500 매핑하거나, 아래처럼 커스텀 에러 타입으로 일원화합니다.

8. 에러 처리: Result, ?, 커스텀 에러 타입

핸들러는 Result<impl Responder, E>를 반환할 수 있고, E: ResponseError 이면 프레임워크가 HTTP 응답으로 변환합니다.

?로 전파

아래 예시에서 anyhow 를 쓰려면 Cargo.tomlanyhow = "1" 을 추가합니다.

use actix_web::{error::ResponseError, http::StatusCode, HttpResponse};
use std::fmt;
#[derive(Debug)]
pub enum ApiError {
    NotFound,
    BadRequest(String),
    Internal(anyhow::Error),
}
impl fmt::Display for ApiError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ApiError::NotFound => write!(f, "not found"),
            ApiError::BadRequest(s) => write!(f, "{s}"),
            ApiError::Internal(e) => write!(f, "{e}"),
        }
    }
}
impl ResponseError for ApiError {
    fn error_response(&self) -> HttpResponse {
        match self {
            ApiError::NotFound => HttpResponse::new(StatusCode::NOT_FOUND),
            ApiError::BadRequest(msg) => HttpResponse::BadRequest().body(msg.clone()),
            ApiError::Internal(_) => HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR),
        }
    }
    fn status_code(&self) -> StatusCode {
        match self {
            ApiError::NotFound => StatusCode::NOT_FOUND,
            ApiError::BadRequest(_) => StatusCode::BAD_REQUEST,
            ApiError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

DB·외부 API에서 sqlx::Error 등을 받았을 때 From을 구현해 두면 ? 한 번ApiError로 올라갑니다.

impl From<sqlx::Error> for ApiError {
    fn from(e: sqlx::Error) -> Self {
        ApiError::Internal(e.into())
    }
}

anyhow는 애플리케이션 내부 편의용이고, HTTP로 노출할 메시지는 별도 필드로 제어하는 것이 안전합니다.

9. 데이터베이스 연동: sqlx 예제

sqlx는 컴파일 타임에 쿼리를 검사할 수 있고(선택), 비동기 런타임과 잘 맞습니다. 여기서는 파일 하나로 돌아가는 SQLite 예시를 둡니다.

Cargo.toml

FromRow 매크로를 쓰려면 macros 기능을 켭니다.

sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros"] }

연결 풀을 앱 상태에 넣기

use sqlx::{sqlite::SqlitePoolOptions, FromRow, Pool, Sqlite};
#[derive(Clone, FromRow)]
struct UserRow {
    id: i64,
    name: String,
}
#[derive(Clone)]
struct AppStateDb {
    pool: Pool<Sqlite>,
}
async fn get_user_db(
    data: web::Data<AppStateDb>,
    path: web::Path<i64>,
) -> Result<HttpResponse, actix_web::error::Error> {
    let row: Option<UserRow> = sqlx::query_as(
        "SELECT id, name FROM users WHERE id = ?",
    )
    .bind(*path)
    .fetch_optional(&data.pool)
    .await
    .map_err(actix_web::error::ErrorInternalServerError)?;
    match row {
        Some(u) => Ok(HttpResponse::Ok().json(serde_json::json!({ "id": u.id, "name": u.name }))),
        None => Err(actix_web::error::ErrorNotFound("not found")),
    }
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let pool = SqlitePoolOptions::new()
        .max_connections(5)
        .connect("sqlite://app.db") // 경로·버전에 맞게 조정 (sqlx 문서의 SQLite URL 참고)
        .await
        .expect("db");
    sqlx::query("CREATE TABLE IF NOT EXISTS users (id INTEGER PRIMARY KEY, name TEXT NOT NULL)")
        .execute(&pool)
        .await
        .expect("migrate");
    let state = web::Data::new(AppStateDb { pool });
    HttpServer::new(move || {
        App::new()
            .app_data(state.clone())
            .route("/users/{id}", web::get().to(get_user_db))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

Diesel은 동기 중심·강한 ORM 스타일이라, Tokio 액터 모델과 함께 쓸 때는 spawn_blocking 으로 풀 밖에서 실행하는 패턴이 흔합니다. 새 프로젝트는 sqlx + 마이그레이션 조합을 많이 택합니다.

10. 테스트: actix-web

actix_web::test실제 서비스 파이프라인에 요청을 보내 검증합니다.

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{http::StatusCode, test, web, App};
    #[actix_web::test]
    async fn get_users_returns_ok() {
        let state = web::Data::new(AppState {
            users: Mutex::new(vec![User {
                id: 1,
                name: "a".into(),
                email: "[email protected]".into(),
            }]),
        });
        let app = test::init_service(
            App::new()
                .app_data(state.clone())
                .route("/users", web::get().to(get_users)),
        )
        .await;
        let req = test::TestRequest::get().uri("/users").to_request();
        let resp = test::call_service(&app, req).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }
}
  • #[actix_web::test]: 내부적으로 런타임을 맞춰 줍니다.
  • JSON 본문 검증은 test::read_body_json 등으로 이어갈 수 있습니다.
  • DB가 있으면 테스트 전용 DB 파일이나 sqlx 테스트 트랜잭션 롤백으로 격리합니다.

11. 배포·프로덕션 설정

로깅

RUST_LOG로 모듈별 레벨을 줍니다.

RUST_LOG=actix_web=info,my_crate=debug

tracing + tracing-subscriber를 쓰면 JSON 로그·OpenTelemetry 연동에 유리합니다.

환경 변수

std::env::var 또는 dotenvy포트·DB URL·시크릿을 주입하고, 코드에 비밀을 넣지 않습니다.

let host = std::env::var("BIND_ADDR").unwrap_or_else(|_| "0.0.0.0".into());
let port: u16 = std::env::var("PORT")
    .ok()
    .and_then(|s| s.parse().ok())
    .unwrap_or(8080);

성능·운영

  • HttpServer::workers(n): CPU 코어 수에 맞게 조정(기본은 논리 코어 수).
  • 바인드 주소: 컨테이너·클라우드에서는 0.0.0.0으로 리슨해야 외부에서 접근 가능합니다.
  • 리버스 프록시(Nginx 등) 뒤에 두고 TLS는 프록시에서 종료하는 구성이 일반적입니다.
  • 타임아웃·바디 크기 제한HttpServer·App 설정 또는 프록시에서 제한합니다.

정리

핵심 요약

  1. Actix-web: 고성능 웹 프레임워크
  2. 라우팅: web::get(), web::post() 등
  3. 핸들러: async fn, impl Responder
  4. 상태 관리: web::Data (Arc 기반), Mutex/RwLock 선택
  5. JSON: serde, web::Json
  6. 미들웨어: CORS, 인증, ErrorHandlers 등으로 횡단 관심사 분리
  7. 에러: ResponseError 구현 + ? 전파로 핸들러 단순화
  8. DB: sqlx 풀을 Data에 두고 await (락 안에서 await 금지)
  9. 테스트: test::init_service, #[actix_web::test]
  10. 프로덕션: RUST_LOG, 환경 변수, workers·바인드·프록시

다음 단계


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「Rust 웹 개발 | Actix-web으로 REST API 만들기」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「Rust 웹 개발 | Actix-web으로 REST API 만들기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Rust 웹 개발 use actix_web::{web, App, HttpServer, Responder};. 실전 예제와 코드로 개념부터 활용까지 정리합니다. Rust·웹개발·Actix-web 중심으로 설명합니다. … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

Rust, 웹개발, Actix-web, REST API 등으로 검색하시면 이 글이 도움이 됩니다.