Rust 웹 개발 | Actix-web으로 REST API 만들기
이 글의 핵심
Rust로 웹 API를 만들 때 Actix-web을 고르는 경우가 많습니다. Tokio 기반 비동기 런타임 위에서 동작하며, 라우팅·JSON·미들웨어·테스트를 한 프레임워크 안에서 다룰 수 있습니다.
시리즈 안내
들어가며
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-httpauth 의 HttpAuthentication::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
| 방식 | 특징 | 언제 쓰나 |
|---|---|---|
| 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.toml에 anyhow = "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설정 또는 프록시에서 제한합니다.
정리
핵심 요약
- Actix-web: 고성능 웹 프레임워크
- 라우팅: web::get(), web::post() 등
- 핸들러: async fn, impl Responder
- 상태 관리: web::Data
( Arc기반),Mutex/RwLock선택 - JSON: serde, web::Json
- 미들웨어: CORS, 인증, ErrorHandlers 등으로 횡단 관심사 분리
- 에러:
ResponseError구현 +?전파로 핸들러 단순화 - DB: sqlx 풀을
Data에 두고 await (락 안에서 await 금지) - 테스트:
test::init_service,#[actix_web::test] - 프로덕션:
RUST_LOG, 환경 변수, workers·바인드·프록시
다음 단계
관련 글
- Flask 기초 | Python 웹 프레임워크 시작하기
- C++ HTTP 클라이언트 완벽 가이드 | REST API 호출·연결 풀·타임아웃·프로덕션 패턴
- C++ JSON 파싱 완벽 가이드 | nlohmann·RapidJSON·커스텀 타입·에러 처리·프로덕션 패턴
- C++ REST API 클라이언트 완벽 가이드 | CRUD·인증·에러 처리·프로덕션 패턴 [#21-3]
- C++와 Rust: 두 언어의 상호 운용성과 Memory Safety 논쟁의 실체 [#44-2]
심화 부록: 구현·운영 관점
이 부록은 앞선 본문에서 다룬 주제(「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 만들기」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.
- 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
- 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
- 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
- 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
- 부하 후 검증: 피크 대비 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 스냅샷 비교 |
| 빌드·배포만 실패 | 환경 변수, 권한, 플랫폼 차이, lockfile | CI 로그와 로컬 diff, 런타임·이미지 버전 핀 |
| 설정 불일치 | 프로필·시크릿·기본값, 리전 | 스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화 |
| 데이터 불일치 | 비멱등 재시도, 부분 쓰기, 캐시 무효화 누락 | 멱등 키·아웃박스·트랜잭션 경계 재검토 |
권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.
배포 전에는 git add → git commit → git push 후 npm run deploy 순서를 권장합니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Rust 웹 개발 use actix_web::{web, App, HttpServer, Responder};. 실전 예제와 코드로 개념부터 활용까지 정리합니다. Rust·웹개발·Actix-web 중심으로 설명합니다. … 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- Rust 비동기 프로그래밍 | async/await, Tokio
- Rust 테스팅 | 단위 테스트, 통합 테스트, 벤치마크
- Rust 동시성 | Thread, Channel, Arc, Mutex
이 글에서 다루는 키워드 (관련 검색어)
Rust, 웹개발, Actix-web, REST API 등으로 검색하시면 이 글이 도움이 됩니다.