본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 10. 30. · 13 Views
Actix-web으로 REST API 서버 개발 완벽 가이드
Rust의 강력한 웹 프레임워크 Actix-web을 활용하여 안전하고 빠른 REST API 서버를 구축하는 방법을 배웁니다. 초급 개발자도 쉽게 따라할 수 있도록 기본 개념부터 실전 활용까지 단계별로 설명합니다.
목차
1. Actix-web 기본 설정
시작하며
여러분이 처음 웹 서버를 만들려고 할 때 이런 고민을 해본 적 있나요? "어떤 프레임워크를 선택해야 할까?
성능도 좋으면서 배우기 쉬운 게 있을까?" 많은 개발자들이 Node.js나 Django를 먼저 떠올리지만, 최근 실무에서는 성능과 안정성이 중요한 프로젝트에서 Rust와 Actix-web을 선택하는 경우가 많아졌습니다. 이런 선택은 단순히 유행을 따르는 게 아닙니다.
Actix-web은 벤치마크에서 Node.js보다 10배 이상 빠른 처리량을 보여주며, Rust의 메모리 안정성 덕분에 런타임 에러로부터 자유롭습니다. 특히 동시 접속자가 많은 서비스나 마이크로서비스 아키텍처에서 그 진가를 발휘합니다.
바로 이럴 때 필요한 것이 Actix-web의 기본 설정입니다. 복잡해 보이지만 실제로는 몇 줄의 코드만으로 프로덕션 레벨의 웹 서버를 시작할 수 있습니다.
개요
간단히 말해서, Actix-web은 Rust로 작성된 매우 빠르고 안전한 웹 프레임워크입니다. Actor 모델을 기반으로 하여 높은 동시성을 자연스럽게 처리할 수 있습니다.
실무에서 이 프레임워크가 필요한 이유는 명확합니다. 첫째, 타입 안정성 덕분에 컴파일 시점에 대부분의 버그를 잡아낼 수 있습니다.
둘째, 제로 코스트 추상화로 성능 손실 없이 고수준 API를 사용할 수 있습니다. 예를 들어, 대용량 트래픽을 처리하는 API 게이트웨이나 실시간 데이터 처리 서버 같은 경우에 매우 유용합니다.
기존에는 성능을 위해 C++로 작성하거나 안정성을 위해 Java를 선택해야 했다면, 이제는 Actix-web으로 둘 다 얻을 수 있습니다. 핵심 특징으로는 첫째, 비동기 처리를 기본으로 지원하여 적은 자원으로 많은 연결을 처리합니다.
둘째, 강력한 타입 시스템으로 런타임 에러를 방지합니다. 셋째, 풍부한 미들웨어 생태계로 인증, 로깅, CORS 등을 쉽게 추가할 수 있습니다.
이러한 특징들이 장기적으로 유지보수 비용을 크게 줄여주고 서비스의 안정성을 보장합니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result};
// 간단한 핸들러 함수
async fn hello() -> Result<HttpResponse> {
// JSON 응답 반환
Ok(HttpResponse::Ok().json("Hello, Actix-web!"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
println!("서버 시작: http://localhost:8080");
// HTTP 서버 생성 및 설정
HttpServer::new(|| {
App::new()
// 라우트 등록
.route("/hello", web::get().to(hello))
})
.bind(("127.0.0.1", 8080))? // 주소와 포트 바인딩
.run() // 서버 실행
.await
}
설명
이것이 하는 일: 위 코드는 8080 포트에서 HTTP 요청을 받아 처리하는 기본 웹 서버를 생성합니다. /hello 엔드포인트에 GET 요청이 오면 JSON 응답을 반환합니다.
첫 번째로, use 구문으로 필요한 모듈들을 가져옵니다. HttpServer는 서버 자체를 관리하고, App은 애플리케이션 설정을 담당하며, web은 라우팅 관련 기능을 제공합니다.
이렇게 모듈을 명시적으로 가져오는 것은 Rust의 특징으로, 어떤 기능을 사용하는지 코드만 봐도 명확히 알 수 있습니다. 그 다음으로, hello 함수가 실행되면서 비동기로 응답을 생성합니다.
async 키워드 덕분에 이 함수는 다른 요청을 블록하지 않고 동시에 여러 요청을 처리할 수 있습니다. HttpResponse::Ok()는 200 상태 코드를 의미하며, .json()은 자동으로 Content-Type 헤더를 설정하고 데이터를 JSON으로 직렬화합니다.
마지막으로, HttpServer::new()에서 클로저를 통해 애플리케이션을 설정하고, .bind()로 네트워크 주소를 지정한 뒤 .run()으로 서버를 시작합니다. #[actix_web::main] 매크로는 비동기 런타임을 자동으로 설정해주어 복잡한 설정 없이 async 코드를 실행할 수 있게 합니다.
여러분이 이 코드를 사용하면 즉시 작동하는 웹 서버를 얻을 수 있습니다. 실무에서의 이점으로는 첫째, 컴파일 타임에 대부분의 에러를 잡을 수 있어 런타임 크래시가 거의 없습니다.
둘째, 메모리 누수 걱정 없이 안전하게 장시간 운영할 수 있습니다. 셋째, 작은 바이너리 사이즈로 컨테이너 이미지가 가벼워져 배포가 빠릅니다.
실전 팁
💡 Cargo.toml에 actix-web = "4" 의존성을 추가하는 것을 잊지 마세요. 버전을 명시하면 호환성 문제를 예방할 수 있습니다.
💡 개발 중에는 .bind(("0.0.0.0", 8080))을 사용하면 외부에서도 접근할 수 있어 모바일 테스트나 팀원과의 공유가 편리합니다.
💡 프로덕션 환경에서는 HttpServer::new()에 .workers(4)를 추가하여 워커 스레드 수를 CPU 코어 수에 맞게 조정하면 성능이 크게 향상됩니다.
💡 로그를 보려면 env_logger 크레이트를 추가하고 env_logger::init()을 호출하세요. 디버깅 시간을 절반으로 줄여줍니다.
💡 cargo watch -x run을 사용하면 코드 변경 시 자동으로 재시작되어 개발 속도가 빨라집니다.
2. HTTP 라우팅
시작하며
여러분이 API를 설계할 때 이런 고민을 해본 적 있나요? "사용자 정보는 /users로 하고, 상품 정보는 /products로 해야 하는데, 이걸 어떻게 깔끔하게 관리하지?" REST API에서 URL 구조는 서비스의 직관성을 결정하는 핵심 요소입니다.
이런 문제는 프로젝트가 커질수록 더 복잡해집니다. 엔드포인트가 수십 개로 늘어나면 코드가 스파게티처럼 엉키고, 어떤 경로가 어떤 기능을 하는지 파악하기 어려워집니다.
특히 팀 프로젝트에서는 각자 만든 라우트가 충돌하는 경우도 빈번합니다. 바로 이럴 때 필요한 것이 Actix-web의 체계적인 라우팅 시스템입니다.
직관적인 매크로와 스코프 기능으로 수백 개의 엔드포인트도 깔끔하게 관리할 수 있습니다.
개요
간단히 말해서, 라우팅은 HTTP 요청이 어떤 핸들러 함수로 전달될지 결정하는 메커니즘입니다. URL 경로와 HTTP 메서드를 기준으로 적절한 처리기를 찾아줍니다.
실무에서 이 기능이 중요한 이유는 API의 가독성과 유지보수성을 결정하기 때문입니다. 예를 들어, RESTful 원칙에 따라 /api/users에 GET 요청은 목록 조회, POST 요청은 생성, /api/users/{id}에 GET은 단일 조회, PUT은 수정, DELETE는 삭제로 명확히 구분할 수 있습니다.
이러한 일관된 패턴은 프론트엔드 개발자나 API 소비자가 문서 없이도 직관적으로 사용할 수 있게 합니다. 기존에는 if-else 문으로 URL을 파싱하고 메서드를 확인했다면, 이제는 선언적으로 라우트를 정의하여 코드의 의도가 명확해집니다.
핵심 특징으로는 첫째, 경로 매개변수를 통해 동적 URL을 처리할 수 있습니다. 둘째, 스코프를 사용하여 라우트를 논리적으로 그룹화할 수 있습니다.
셋째, 가드를 통해 요청 전처리 로직을 추가할 수 있습니다. 이러한 특징들이 대규모 API 서버를 체계적으로 구성하는 데 필수적입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result};
// 사용자 목록 조회
async fn get_users() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json(vec!["user1", "user2"]))
}
// 특정 사용자 조회 (경로 매개변수 사용)
async fn get_user(path: web::Path<u32>) -> Result<HttpResponse> {
let user_id = path.into_inner();
Ok(HttpResponse::Ok().json(format!("User ID: {}", user_id)))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
// API 버전별로 스코프 그룹화
.service(
web::scope("/api/v1")
.route("/users", web::get().to(get_users))
.route("/users/{id}", web::get().to(get_user))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 /api/v1/users와 /api/v1/users/{id} 두 개의 엔드포인트를 생성하여 사용자 목록 조회와 단일 사용자 조회 기능을 제공합니다. 첫 번째로, web::Path<u32>는 URL의 {id} 부분을 자동으로 추출하여 u32 타입으로 변환합니다.
만약 숫자가 아닌 값이 들어오면 Actix-web이 자동으로 400 Bad Request를 반환하므로, 별도의 검증 코드가 필요 없습니다. .into_inner()는 Path 래퍼를 벗겨내어 실제 값을 얻습니다.
그 다음으로, web::scope("/api/v1")이 실행되면서 하위의 모든 라우트에 /api/v1 접두사가 자동으로 붙습니다. 이는 API 버전 관리에 매우 유용합니다.
v2를 만들 때는 새로운 스코프를 추가하기만 하면 되고, 기존 v1은 그대로 유지할 수 있어 하위 호환성을 보장합니다. 마지막으로, .route() 메서드가 경로와 HTTP 메서드를 핸들러에 연결합니다.
web::get()은 GET 요청만 받아들이며, POST나 PUT 등 다른 메서드는 자동으로 405 Method Not Allowed로 거부됩니다. 이러한 자동 처리 덕분에 보안 관련 실수를 줄일 수 있습니다.
여러분이 이 코드를 사용하면 RESTful API의 표준 구조를 자연스럽게 따르게 됩니다. 실무에서의 이점으로는 첫째, 새로운 팀원이 코드를 봤을 때 즉시 API 구조를 파악할 수 있습니다.
둘째, 스코프별로 미들웨어를 적용할 수 있어 인증이 필요한 엔드포인트를 쉽게 보호할 수 있습니다. 셋째, 경로 매개변수 타입 검증이 자동으로 이루어져 잘못된 요청을 조기에 차단합니다.
실전 팁
💡 복잡한 경로 매개변수는 web::Path<(String, u32)>처럼 튜플로 받을 수 있습니다. 예를 들어 /users/{name}/posts/{id}의 경우 유용합니다.
💡 web::resource()를 사용하면 같은 경로에 여러 HTTP 메서드를 한 번에 등록할 수 있어 RESTful 리소스 관리가 편해집니다.
💡 라우트 순서가 중요합니다. /users/me를 /users/{id} 보다 먼저 등록해야 "me"가 ID로 인식되지 않습니다.
💡 web::scope()는 중첩할 수 있어 /api/v1/admin/users 같은 깊은 구조도 깔끔하게 표현됩니다.
💡 개발 중에는 라우트 목록을 출력하는 미들웨어를 만들어 등록된 엔드포인트를 한눈에 확인하세요.
3. JSON 요청 처리
시작하며
여러분이 회원가입 API를 만들 때 이런 상황을 겪어본 적 있나요? 클라이언트가 보낸 JSON 데이터의 이메일 형식이 올바른지, 비밀번호 길이가 충분한지 일일이 확인하는 코드를 작성하다 보면 핵심 로직보다 검증 코드가 더 길어지는 경우가 많습니다.
이런 문제는 API 서버의 안정성을 해칩니다. 잘못된 데이터가 데이터베이스에 저장되거나, 예상치 못한 타입으로 인해 런타임 에러가 발생하면 서비스 전체가 중단될 수 있습니다.
특히 프론트엔드와 백엔드 개발자가 다를 때 데이터 형식에 대한 오해로 많은 시간을 낭비하게 됩니다. 바로 이럴 때 필요한 것이 Actix-web의 JSON 자동 직렬화와 검증 시스템입니다.
Rust의 강력한 타입 시스템과 serde 라이브러리를 활용하여 안전하게 데이터를 처리할 수 있습니다.
개요
간단히 말해서, JSON 요청 처리는 클라이언트가 보낸 JSON 데이터를 Rust의 구조체로 자동 변환하고 검증하는 과정입니다. 이를 통해 타입 안정성을 보장하면서도 간결한 코드를 작성할 수 있습니다.
실무에서 이 기능이 필수적인 이유는 명확합니다. 첫째, 잘못된 데이터 형식을 컴파일 시점에 잡아낼 수 있습니다.
둘째, 수동으로 JSON을 파싱하고 검증하는 보일러플레이트 코드를 제거할 수 있습니다. 예를 들어, 사용자 등록, 상품 주문, 결제 요청 같은 중요한 트랜잭션에서 데이터 무결성을 보장하는 것이 매우 중요합니다.
기존에는 JavaScript 객체를 일일이 확인하고 타입을 체크했다면, 이제는 구조체 정의만으로 모든 검증이 자동으로 이루어집니다. 핵심 특징으로는 첫째, web::Json<T> 추출자가 자동으로 역직렬화와 검증을 수행합니다.
둘째, serde의 derive 매크로로 보일러플레이트를 최소화합니다. 셋째, validator 크레이트와 결합하면 복잡한 비즈니스 규칙도 선언적으로 표현할 수 있습니다.
이러한 특징들이 안전하고 유지보수하기 쉬운 API를 만드는 핵심입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result};
use serde::{Deserialize, Serialize};
// 요청 데이터 구조체 정의
#[derive(Deserialize)]
struct CreateUser {
username: String,
email: String,
age: u32,
}
// 응답 데이터 구조체
#[derive(Serialize)]
struct UserResponse {
id: u32,
username: String,
message: String,
}
// JSON 요청을 받아 처리하는 핸들러
async fn create_user(user: web::Json<CreateUser>) -> Result<HttpResponse> {
// 자동으로 역직렬화된 데이터 사용
let new_user = UserResponse {
id: 1,
username: user.username.clone(),
message: format!("{}님이 생성되었습니다", user.username),
};
// 구조체를 자동으로 JSON으로 직렬화
Ok(HttpResponse::Created().json(new_user))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/users", web::post().to(create_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 POST 요청으로 받은 JSON 데이터를 CreateUser 구조체로 변환하고, 처리 후 UserResponse 구조체를 JSON으로 응답합니다. 첫 번째로, #[derive(Deserialize)]와 #[derive(Serialize)] 매크로가 컴파일 타임에 JSON 변환 코드를 자동 생성합니다.
이는 런타임 오버헤드가 전혀 없으며, 잘못된 필드명이나 타입은 컴파일 시점에 즉시 발견됩니다. 예를 들어 클라이언트가 age를 문자열로 보내면 자동으로 400 Bad Request가 반환됩니다.
그 다음으로, web::Json<CreateUser> 추출자가 요청 본문을 읽어 역직렬화를 시도합니다. 이 과정에서 Content-Type이 application/json인지 확인하고, JSON 파싱이 실패하면 적절한 에러 메시지와 함께 400 응답을 보냅니다.
모든 필드가 구조체 정의와 정확히 일치해야 하므로, 선택적 필드는 Option<T>로 표시해야 합니다. 마지막으로, HttpResponse::Created().json(new_user)가 구조체를 JSON으로 직렬화하고 201 상태 코드와 함께 응답합니다.
Content-Type 헤더도 자동으로 설정되어 클라이언트가 JSON으로 인식할 수 있습니다. 직렬화 역시 컴파일 타임에 최적화되어 매우 빠릅니다.
여러분이 이 코드를 사용하면 데이터 검증에 신경 쓰지 않고 비즈니스 로직에 집중할 수 있습니다. 실무에서의 이점으로는 첫째, 타입 불일치로 인한 런타임 에러가 사라집니다.
둘째, API 문서를 작성할 때 구조체 정의가 곧 스키마가 되어 정확한 문서를 유지할 수 있습니다. 셋째, 프론트엔드와 TypeScript를 쓴다면 Rust 구조체에서 타입 정의를 자동 생성하여 완벽한 타입 안정성을 달성할 수 있습니다.
실전 팁
💡 선택적 필드는 Option<String>으로 정의하세요. 클라이언트가 보내지 않아도 에러가 발생하지 않습니다.
💡 #[serde(rename = "userName")]을 사용하면 Rust의 snake_case와 JSON의 camelCase를 자동 변환할 수 있습니다.
💡 validator 크레이트의 #[validate(email)]나 #[validate(length(min = 8))] 같은 애트리뷰트로 복잡한 검증을 선언적으로 추가하세요.
💡 큰 JSON 페이로드는 web::JsonConfig::default().limit(4096)으로 크기 제한을 두어 DoS 공격을 방지하세요.
💡 #[serde(default)]를 사용하면 필드가 없을 때 기본값을 자동으로 채워주어 편리합니다.
4. 응답 생성
시작하며
여러분이 API를 개발하다 보면 이런 상황을 자주 마주칩니다. 성공했을 때는 200 OK와 데이터를 보내고, 찾지 못했을 때는 404 Not Found를, 권한이 없을 때는 403 Forbidden을 보내야 합니다.
그런데 매번 상태 코드와 헤더, 본문을 수동으로 설정하다 보면 실수하기 쉽고 코드도 지저분해집니다. 이런 문제는 특히 대규모 프로젝트에서 일관성을 해칩니다.
어떤 엔드포인트는 에러를 JSON으로 반환하고, 다른 곳은 plain text로 반환하면 클라이언트 개발자가 혼란스러워합니다. 또한 캐시 헤더나 CORS 헤더를 빠뜨리면 성능 문제나 보안 이슈가 발생할 수 있습니다.
바로 이럴 때 필요한 것이 Actix-web의 풍부한 응답 생성 API입니다. 다양한 상태 코드와 헤더를 타입 안전하게 설정할 수 있으며, 빌더 패턴으로 읽기 쉬운 코드를 작성할 수 있습니다.
개요
간단히 말해서, 응답 생성은 HTTP 상태 코드, 헤더, 본문을 조합하여 클라이언트에게 보낼 응답을 만드는 과정입니다. Actix-web은 이를 위한 다양한 헬퍼 메서드를 제공합니다.
실무에서 올바른 응답 생성이 중요한 이유는 API의 사용성과 디버깅 효율성을 결정하기 때문입니다. 명확한 상태 코드는 클라이언트가 문제를 즉시 파악하게 해주고, 적절한 헤더는 브라우저 캐싱이나 보안을 최적화합니다.
예를 들어, 인증 실패 시 401 Unauthorized를 보내면 클라이언트가 자동으로 로그인 페이지로 리다이렉트할 수 있습니다. 기존에는 응답 객체를 만들고 일일이 필드를 설정했다면, 이제는 HttpResponse::Ok(), HttpResponse::NotFound() 같은 메서드로 의도를 명확히 표현할 수 있습니다.
핵심 특징으로는 첫째, 200번대부터 500번대까지 모든 표준 상태 코드에 대한 메서드가 있습니다. 둘째, 체이닝 패턴으로 헤더를 직관적으로 추가할 수 있습니다.
셋째, .json(), .body(), .streaming() 등 다양한 본문 형식을 지원합니다. 이러한 특징들이 견고하고 사용자 친화적인 API를 만드는 데 필수적입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result, http::header};
use serde::Serialize;
#[derive(Serialize)]
struct ErrorResponse {
error: String,
code: u16,
}
// 다양한 응답 생성 예제
async fn get_resource(path: web::Path<u32>) -> Result<HttpResponse> {
let id = path.into_inner();
if id == 0 {
// 400 Bad Request - 잘못된 요청
return Ok(HttpResponse::BadRequest().json(ErrorResponse {
error: "ID는 0보다 커야 합니다".to_string(),
code: 400,
}));
}
if id > 100 {
// 404 Not Found - 리소스 없음
return Ok(HttpResponse::NotFound().json(ErrorResponse {
error: "리소스를 찾을 수 없습니다".to_string(),
code: 404,
}));
}
// 200 OK - 성공 응답 (커스텀 헤더 포함)
Ok(HttpResponse::Ok()
.insert_header((header::CACHE_CONTROL, "max-age=3600"))
.insert_header(("X-Custom-Header", "value"))
.json(serde_json::json!({
"id": id,
"name": "Resource Name",
"status": "active"
})))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/resource/{id}", web::get().to(get_resource))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 ID 값에 따라 다른 HTTP 상태 코드와 본문을 반환하며, 성공 시 캐시 헤더와 커스텀 헤더를 포함합니다. 첫 번째로, early return 패턴을 사용하여 에러 케이스를 먼저 처리합니다.
HttpResponse::BadRequest()는 400 상태 코드를 설정하고, .json()으로 구조화된 에러 메시지를 반환합니다. 이렇게 일관된 에러 형식을 유지하면 클라이언트가 에러 처리 로직을 한 번만 작성하면 되므로 매우 편리합니다.
그 다음으로, HttpResponse::NotFound()가 404 상태를 생성합니다. Actix-web은 각 상태 코드에 맞는 기본 이유 구문(Reason Phrase)을 자동으로 설정합니다.
예를 들어 404는 "Not Found", 500은 "Internal Server Error"가 자동으로 포함됩니다. 마지막으로, 성공 케이스에서 .insert_header()로 여러 헤더를 추가합니다.
CACHE_CONTROL 헤더는 브라우저가 1시간 동안 응답을 캐시하도록 지시하여 서버 부하를 줄입니다. 커스텀 헤더는 API 버전이나 요청 추적 ID를 전달하는 데 유용합니다.
체이닝 패턴 덕분에 여러 헤더를 가독성 좋게 추가할 수 있습니다. 여러분이 이 코드를 사용하면 HTTP 표준을 올바르게 따르는 전문적인 API를 만들 수 있습니다.
실무에서의 이점으로는 첫째, 명확한 상태 코드로 클라이언트가 에러를 즉시 분류할 수 있습니다. 둘째, 캐시 헤더로 불필요한 요청을 줄여 성능과 비용을 절감합니다.
셋째, 일관된 에러 형식으로 프론트엔드 에러 핸들링이 단순해집니다.
실전 팁
💡 HttpResponse::Created().json(...)은 201 상태와 함께 Location 헤더를 추가하면 RESTful 표준에 완벽히 부합합니다.
💡 대용량 파일 응답은 .streaming()을 사용하여 메모리에 전체를 로드하지 않고 스트리밍하세요.
💡 HttpResponse::build(StatusCode::IM_A_TEAPOT)처럼 비표준 상태 코드도 자유롭게 사용할 수 있습니다.
💡 CORS 헤더는 매번 추가하지 말고 미들웨어로 전역 설정하면 코드가 깔끔해집니다.
💡 content_type("text/html; charset=utf-8")로 명시적으로 인코딩을 설정하면 한글 깨짐을 방지할 수 있습니다.
5. 미들웨어 활용
시작하며
여러분이 API 서버를 운영하다 보면 이런 요구사항을 자주 듣게 됩니다. "모든 요청에 대해 로그를 남겨주세요", "인증된 사용자만 접근할 수 있게 해주세요", "응답 시간을 측정해주세요".
이런 기능을 각 핸들러마다 직접 구현하면 코드 중복이 심해지고 유지보수가 악몽이 됩니다. 이런 문제는 횡단 관심사(Cross-Cutting Concerns)라고 불리며, 모든 엔터프라이즈 애플리케이션이 직면하는 과제입니다.
로깅, 인증, 에러 처리, 성능 모니터링 같은 기능은 비즈니스 로직과 분리되어야 하지만, 모든 요청에 적용되어야 합니다. 바로 이럴 때 필요한 것이 미들웨어입니다.
Actix-web의 미들웨어 시스템은 요청-응답 파이프라인에 끼어들어 공통 로직을 재사용 가능한 컴포넌트로 만들 수 있게 해줍니다.
개요
간단히 말해서, 미들웨어는 요청이 핸들러에 도달하기 전이나 응답이 클라이언트에게 가기 전에 실행되는 코드입니다. 파이프라인 패턴으로 여러 미들웨어를 순차적으로 적용할 수 있습니다.
실무에서 미들웨어가 필수적인 이유는 관심사의 분리와 코드 재사용성 때문입니다. 로깅 미들웨어 하나로 모든 엔드포인트의 요청/응답을 기록할 수 있고, 인증 미들웨어로 보호된 라우트를 일관되게 관리할 수 있습니다.
예를 들어, API 게이트웨이에서 rate limiting, 요청 검증, 변환 등을 미들웨어로 구현하면 각 마이크로서비스는 비즈니스 로직에만 집중할 수 있습니다. 기존에는 데코레이터나 AOP(Aspect-Oriented Programming)를 사용했다면, Actix-web에서는 미들웨어로 더 직관적이고 타입 안전하게 같은 효과를 얻을 수 있습니다.
핵심 특징으로는 첫째, wrap() 메서드로 미들웨어를 체이닝할 수 있습니다. 둘째, 전역 또는 스코프별로 선택적으로 적용할 수 있습니다.
셋째, 커스텀 미들웨어를 만들어 프로젝트 특화 로직을 추가할 수 있습니다. 이러한 특징들이 깨끗하고 유지보수하기 쉬운 아키텍처를 만드는 핵심입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result, middleware};
use actix_web::middleware::Logger;
async fn protected_handler() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("보호된 리소스에 접근했습니다"))
}
async fn public_handler() -> Result<HttpResponse> {
Ok(HttpResponse::Ok().json("공개 리소스입니다"))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 환경 변수로 로그 레벨 설정
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
HttpServer::new(|| {
App::new()
// 전역 미들웨어: 모든 요청에 적용
.wrap(Logger::default()) // 요청/응답 로깅
.wrap(middleware::Compress::default()) // Gzip 압축
// 공개 엔드포인트
.route("/public", web::get().to(public_handler))
// 보호된 엔드포인트 (스코프별 미들웨어)
.service(
web::scope("/api")
.wrap(middleware::NormalizePath::trim_trailing_slash())
.route("/protected", web::get().to(protected_handler))
)
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 모든 요청에 로깅과 압축을 적용하고, /api 경로에만 URL 정규화를 추가로 적용하는 미들웨어 파이프라인을 구성합니다. 첫 번째로, Logger::default()가 각 요청의 메서드, 경로, 상태 코드, 처리 시간을 자동으로 로그에 기록합니다.
env_logger::init_from_env()는 RUST_LOG 환경 변수로 로그 레벨을 동적으로 조절할 수 있게 해줍니다. 프로덕션에서는 info, 개발 중에는 debug로 설정하면 필요한 만큼만 로그를 볼 수 있어 성능 영향을 최소화합니다.
그 다음으로, Compress::default()가 응답 본문을 자동으로 Gzip으로 압축합니다. 클라이언트가 Accept-Encoding 헤더를 보내면 압축을 활성화하고, 그렇지 않으면 원본 그대로 보냅니다.
이는 대역폭을 크게 줄여주며, 특히 JSON 응답은 70-80% 압축되어 모바일 사용자에게 큰 도움이 됩니다. 마지막으로, NormalizePath::trim_trailing_slash()가 /api/protected와 /api/protected/를 동일하게 처리합니다.
이는 클라이언트가 실수로 슬래시를 추가해도 404가 발생하지 않게 해주어 사용자 경험을 개선합니다. 스코프별로 적용되므로 /api 하위에만 영향을 미칩니다.
여러분이 이 코드를 사용하면 프로덕션 레벨의 기능을 몇 줄로 추가할 수 있습니다. 실무에서의 이점으로는 첫째, 로그 분석으로 병목 지점을 빠르게 파악할 수 있습니다.
둘째, 압축으로 트래픽 비용을 크게 절감합니다. 셋째, 커스텀 미들웨어로 회사의 보안 정책이나 규정 준수를 자동화할 수 있습니다.
실전 팁
💡 actix-web-httpauth 크레이트의 HttpAuthentication 미들웨어로 JWT나 Basic Auth를 쉽게 구현할 수 있습니다.
💡 미들웨어 순서가 중요합니다. 로깅은 가장 먼저, 압축은 가장 나중에 적용하세요.
💡 DefaultHeaders 미들웨어로 보안 헤더(X-Frame-Options, X-Content-Type-Options)를 전역으로 추가하세요.
💡 커스텀 미들웨어는 Transform과 Service 트레이트를 구현하면 됩니다. 예제는 공식 문서에 잘 나와 있습니다.
💡 개발 중에는 middleware::Logger::new("%a %r %s %Dms")로 커스텀 로그 포맷을 만들어 필요한 정보만 출력하세요.
6. 상태 관리
시작하며
여러분이 API 서버에서 데이터베이스 연결 풀을 사용하려고 할 때 이런 고민을 해본 적 있나요? "각 핸들러마다 연결을 새로 만들면 비효율적인데, 어떻게 공유하지?" 전역 변수를 쓰자니 Rust의 소유권 시스템이 허락하지 않고, 매개변수로 전달하자니 코드가 복잡해집니다.
이런 문제는 실제 애플리케이션에서 매우 흔합니다. 데이터베이스 커넥션 풀, 캐시 클라이언트, 설정 객체, 외부 API 클라이언트 등은 모든 핸들러에서 접근해야 하지만, 매번 생성하면 성능이 크게 저하됩니다.
특히 데이터베이스 연결은 생성 비용이 크므로 재사용이 필수적입니다. 바로 이럴 때 필요한 것이 Actix-web의 애플리케이션 상태입니다.
스레드 안전하게 데이터를 공유하면서도 타입 안정성을 유지할 수 있는 우아한 솔루션을 제공합니다.
개요
간단히 말해서, 애플리케이션 상태는 모든 핸들러에서 접근할 수 있는 공유 데이터입니다. web::Data<T>로 감싸면 여러 스레드에서 안전하게 읽을 수 있습니다.
실무에서 상태 관리가 중요한 이유는 성능과 리소스 효율성 때문입니다. 데이터베이스 연결 풀을 한 번만 만들고 재사용하면 연결 생성 오버헤드가 사라집니다.
또한 설정 파일을 한 번 읽어 메모리에 유지하면 매 요청마다 파일을 읽지 않아도 됩니다. 예를 들어, Redis 클라이언트를 상태로 관리하면 모든 엔드포인트에서 캐싱을 일관되게 적용할 수 있습니다.
기존에는 싱글톤 패턴이나 의존성 주입 컨테이너를 사용했다면, Actix-web에서는 web::Data로 더 간단하고 타입 안전하게 같은 효과를 얻습니다. 핵심 특징으로는 첫째, Arc(Atomic Reference Counting)로 자동 메모리 관리가 됩니다.
둘째, 여러 타입의 상태를 동시에 관리할 수 있습니다. 셋째, 추출자 패턴으로 핸들러에서 간편하게 접근합니다.
이러한 특징들이 확장 가능하고 효율적인 서버를 만드는 핵심입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result};
use serde::Serialize;
use std::sync::Mutex;
// 공유할 애플리케이션 상태
struct AppState {
app_name: String,
request_count: Mutex<u64>, // 가변 데이터는 Mutex로 보호
}
#[derive(Serialize)]
struct StatsResponse {
app_name: String,
total_requests: u64,
}
// 상태를 사용하는 핸들러
async fn index(data: web::Data<AppState>) -> Result<HttpResponse> {
// 요청 카운터 증가 (Mutex로 동기화)
let mut counter = data.request_count.lock().unwrap();
*counter += 1;
let count = *counter;
drop(counter); // 락 해제
Ok(HttpResponse::Ok().json(StatsResponse {
app_name: data.app_name.clone(),
total_requests: count,
}))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
// 애플리케이션 상태 초기화
let app_state = web::Data::new(AppState {
app_name: String::from("My API Server"),
request_count: Mutex::new(0),
});
HttpServer::new(move || {
App::new()
.app_data(app_state.clone()) // 상태 등록
.route("/", web::get().to(index))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 애플리케이션 이름과 요청 카운터를 모든 워커 스레드에서 공유하고, 각 요청마다 카운터를 증가시켜 통계를 제공합니다. 첫 번째로, web::Data::new()가 상태를 Arc로 감싸서 생성합니다.
Arc는 원자적 참조 카운팅으로 여러 스레드가 동시에 접근해도 안전합니다. .clone()은 포인터만 복사하므로 오버헤드가 거의 없으며, 실제 데이터는 한 번만 메모리에 존재합니다.
move 클로저는 상태를 각 워커 스레드로 이동시킵니다. 그 다음으로, 핸들러에서 web::Data<AppState>를 매개변수로 받으면 Actix-web이 자동으로 상태를 주입합니다.
data.app_name처럼 불변 데이터는 락 없이 바로 접근할 수 있어 매우 빠릅니다. 반면 request_count는 Mutex로 보호되므로 .lock()을 호출해야 합니다.
마지막으로, Mutex가 한 번에 한 스레드만 값을 수정할 수 있게 보장합니다. drop(counter)로 명시적으로 락을 해제하면 다른 스레드가 기다리는 시간을 최소화할 수 있습니다.
만약 읽기만 한다면 RwLock을 사용하여 여러 스레드가 동시에 읽을 수 있게 최적화할 수 있습니다. 여러분이 이 코드를 사용하면 전역 상태를 타입 안전하게 관리할 수 있습니다.
실무에서의 이점으로는 첫째, 데이터베이스 커넥션 풀을 공유하여 연결 생성 비용을 제거합니다. 둘째, 설정 변경 시 파일만 수정하고 재시작하면 모든 핸들러에 즉시 반영됩니다.
셋째, Redis나 메시지 큐 클라이언트를 상태로 관리하면 일관된 인터페이스로 접근할 수 있습니다.
실전 팁
💡 읽기가 많고 쓰기가 적으면 Mutex 대신 RwLock을 사용하여 동시 읽기를 허용하세요.
💡 데이터베이스 연결은 sqlx::PgPool이나 deadpool을 상태로 등록하면 자동으로 연결을 관리합니다.
💡 여러 타입의 상태가 필요하면 .app_data()를 여러 번 호출하여 각각 등록할 수 있습니다.
💡 Mutex::lock()이 실패하면 다른 스레드가 패닉했다는 의미이므로, .expect()로 명확히 처리하세요.
💡 상태가 복잡하면 구조체 대신 트레이트를 사용하여 테스트 시 목(mock) 구현을 주입할 수 있습니다.
7. 에러 핸들링
시작하며
여러분이 데이터베이스 쿼리나 외부 API 호출을 할 때 이런 경험을 해본 적 있나요? 네트워크가 끊기거나 쿼리가 실패했을 때 서버가 500 에러만 뱉고 아무 정보도 주지 않아서 디버깅에 몇 시간을 허비한 적이요.
이런 상황은 프로덕션 환경에서 치명적입니다. 이런 문제는 에러 처리가 일관되지 않을 때 발생합니다.
어떤 함수는 panic!을 호출하고, 어떤 함수는 Result를 반환하지만 제대로 처리하지 않으면 사용자는 의미 없는 에러만 보게 됩니다. 특히 민감한 정보가 에러 메시지에 노출되면 보안 사고로 이어질 수 있습니다.
바로 이럴 때 필요한 것이 Actix-web의 체계적인 에러 핸들링입니다. ResponseError 트레이트와 커스텀 에러 타입을 활용하여 안전하고 디버깅하기 쉬운 에러 처리를 구현할 수 있습니다.
개요
간단히 말해서, 에러 핸들링은 예상치 못한 상황을 안전하게 처리하고 적절한 HTTP 응답으로 변환하는 과정입니다. Rust의 Result 타입과 결합하여 컴파일 타임에 에러 처리를 강제합니다.
실무에서 올바른 에러 핸들링이 필수적인 이유는 명확합니다. 첫째, 사용자에게 명확한 피드백을 제공하여 문제 해결을 돕습니다.
둘째, 개발자가 로그를 통해 원인을 빠르게 파악할 수 있습니다. 셋째, 민감한 정보를 숨기면서도 유용한 정보를 제공하는 균형을 맞출 수 있습니다.
예를 들어, 결제 실패 시 사용자에게는 "결제 처리 중 오류가 발생했습니다"라고 보여주고, 로그에는 실제 에러 코드와 스택 트레이스를 남길 수 있습니다. 기존에는 try-catch로 모든 에러를 잡아야 했다면, Rust에서는 ? 연산자로 에러를 자동으로 전파하고 한 곳에서 처리할 수 있습니다.
핵심 특징으로는 첫째, ResponseError 트레이트로 에러를 HTTP 응답으로 자동 변환합니다. 둘째, 커스텀 에러 타입으로 비즈니스 도메인의 에러를 명확히 표현합니다.
셋째, thiserror 크레이트로 보일러플레이트를 최소화합니다. 이러한 특징들이 안정적이고 유지보수하기 쉬운 서버를 만드는 기반입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result, error, http::StatusCode};
use serde::Serialize;
use thiserror::Error;
// 커스텀 에러 타입 정의
#[derive(Error, Debug)]
enum ApiError {
#[error("사용자를 찾을 수 없습니다: {0}")]
NotFound(String),
#[error("데이터베이스 오류: {0}")]
DatabaseError(String),
#[error("권한이 없습니다")]
Unauthorized,
}
// 에러를 HTTP 응답으로 변환
impl error::ResponseError for ApiError {
fn error_response(&self) -> HttpResponse {
let status = self.status_code();
HttpResponse::build(status).json(ErrorResponse {
error: self.to_string(),
code: status.as_u16(),
})
}
fn status_code(&self) -> StatusCode {
match self {
ApiError::NotFound(_) => StatusCode::NOT_FOUND,
ApiError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ApiError::Unauthorized => StatusCode::UNAUTHORIZED,
}
}
}
#[derive(Serialize)]
struct ErrorResponse {
error: String,
code: u16,
}
// 에러를 반환하는 핸들러
async fn get_user(path: web::Path<u32>) -> Result<HttpResponse, ApiError> {
let user_id = path.into_inner();
if user_id == 999 {
// 커스텀 에러 반환
return Err(ApiError::NotFound(format!("ID {}", user_id)));
}
Ok(HttpResponse::Ok().json(serde_json::json!({
"id": user_id,
"name": "John Doe"
})))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/users/{id}", web::get().to(get_user))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 도메인별 에러를 정의하고, 각 에러를 적절한 HTTP 상태 코드와 JSON 응답으로 변환하는 체계를 구축합니다. 첫 번째로, #[derive(Error)] 매크로가 thiserror 크레이트를 통해 Error 트레이트를 자동 구현합니다.
#[error("...")]는 에러 메시지 템플릿을 정의하며, {0}은 에러에 포함된 데이터를 포맷합니다. 이렇게 하면 to_string()을 호출할 때 자동으로 읽기 쉬운 메시지가 생성됩니다.
그 다음으로, ResponseError 트레이트를 구현하여 Actix-web에게 이 에러를 어떻게 HTTP 응답으로 변환할지 알려줍니다. status_code() 메서드는 에러 종류에 따라 적절한 상태 코드를 선택합니다.
error_response()는 실제 응답 본문을 만들며, 일관된 JSON 형식으로 에러 정보를 제공합니다. 마지막으로, 핸들러에서 Result<HttpResponse, ApiError>를 반환하면 ? 연산자로 에러를 간편하게 전파할 수 있습니다.
Err(ApiError::NotFound(...))를 반환하면 Actix-web이 자동으로 error_response()를 호출하여 클라이언트에게 적절한 응답을 보냅니다. 이 패턴 덕분에 핸들러 코드는 비즈니스 로직에만 집중할 수 있습니다.
여러분이 이 코드를 사용하면 에러 처리가 일관되고 예측 가능해집니다. 실무에서의 이점으로는 첫째, 에러 타입만 봐도 어떤 문제인지 즉시 파악할 수 있습니다.
둘째, 컴파일러가 모든 에러 케이스를 처리했는지 검증하므로 놓치는 경우가 없습니다. 셋째, 프로덕션에서는 상세한 에러를 로그에만 남기고 사용자에게는 일반화된 메시지를 보여 보안을 강화할 수 있습니다.
실전 팁
💡 anyhow 크레이트를 사용하면 서드파티 라이브러리 에러를 쉽게 변환할 수 있습니다: map_err(|e| ApiError::DatabaseError(e.to_string()))
💡 #[error(transparent)]를 사용하면 다른 에러를 래핑하면서 원본 에러를 보존할 수 있습니다.
💡 개발 환경에서는 스택 트레이스를 포함하고, 프로덕션에서는 숨기는 로직을 cfg!(debug_assertions)로 구현하세요.
💡 sentry 크레이트와 연동하면 모든 에러를 자동으로 모니터링 서비스로 전송할 수 있습니다.
💡 에러 메시지에 민감한 정보(비밀번호, 토큰 등)가 포함되지 않도록 주의하세요. 로그를 검토하는 습관을 들이세요.
8. 비동기 처리
시작하며
여러분이 외부 API를 호출하거나 데이터베이스 쿼리를 실행할 때 이런 문제를 겪어본 적 있나요? 한 요청이 느린 쿼리를 실행하는 동안 다른 모든 요청이 멈춰버려서 사용자들이 타임아웃을 경험하는 상황 말이죠.
이는 동기식 처리의 치명적인 약점입니다. 이런 문제는 I/O 바운드 작업이 많은 웹 서버에서 특히 심각합니다.
CPU는 대부분의 시간을 네트워크나 디스크 응답을 기다리며 놀고 있지만, 전통적인 스레드 모델에서는 스레드가 블록되어 다른 일을 할 수 없습니다. 수천 개의 동시 연결을 처리하려면 수천 개의 스레드가 필요해지고, 메모리가 부족해집니다.
바로 이럴 때 필요한 것이 비동기 프로그래밍입니다. Actix-web은 Tokio 런타임을 기반으로 하여 적은 스레드로 수만 개의 동시 연결을 효율적으로 처리할 수 있습니다.
개요
간단히 말해서, 비동기 처리는 I/O 작업이 완료될 때까지 기다리는 동안 다른 작업을 수행할 수 있게 하는 프로그래밍 패러다임입니다. async/await 키워드로 동기 코드처럼 작성하면서도 논블로킹으로 동작합니다.
실무에서 비동기 처리가 필수적인 이유는 확장성과 성능 때문입니다. 같은 하드웨어로 10배 이상의 동시 사용자를 처리할 수 있으며, 응답 시간도 크게 개선됩니다.
예를 들어, 마이크로서비스 아키텍처에서 여러 서비스를 동시에 호출하여 결과를 모으는 경우, 순차적으로 호출하면 1초씩 걸려 총 3초가 걸리지만, 비동기로 병렬 호출하면 1초 만에 끝낼 수 있습니다. 기존에는 콜백 지옥이나 복잡한 이벤트 루프를 직접 관리했다면, Rust의 async/await는 코드를 읽기 쉽게 유지하면서도 고성능을 제공합니다.
핵심 특징으로는 첫째, async fn으로 선언한 함수는 자동으로 Future를 반환합니다. 둘째, .await는 Future가 완료될 때까지 현재 작업을 양보하고 다른 작업을 처리합니다.
셋째, Tokio의 작업 스케줄러가 CPU 코어를 최대한 활용하도록 작업을 분배합니다. 이러한 특징들이 현대 웹 서버의 기반입니다.
코드 예제
use actix_web::{web, App, HttpServer, HttpResponse, Result};
use serde::Serialize;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Serialize)]
struct ApiResponse {
user: String,
orders: Vec<String>,
recommendations: Vec<String>,
}
// 비동기 함수들 (외부 서비스 호출을 시뮬레이션)
async fn fetch_user(id: u32) -> String {
sleep(Duration::from_millis(100)).await; // 네트워크 지연 시뮬레이션
format!("User {}", id)
}
async fn fetch_orders(id: u32) -> Vec<String> {
sleep(Duration::from_millis(150)).await;
vec![format!("Order 1 for user {}", id)]
}
async fn fetch_recommendations(id: u32) -> Vec<String> {
sleep(Duration::from_millis(120)).await;
vec![format!("Recommendation 1 for user {}", id)]
}
// 여러 비동기 작업을 병렬로 실행
async fn get_dashboard(path: web::Path<u32>) -> Result<HttpResponse> {
let user_id = path.into_inner();
// tokio::join!으로 세 작업을 동시에 실행
let (user, orders, recommendations) = tokio::join!(
fetch_user(user_id),
fetch_orders(user_id),
fetch_recommendations(user_id)
);
Ok(HttpResponse::Ok().json(ApiResponse {
user,
orders,
recommendations,
}))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/dashboard/{id}", web::get().to(get_dashboard))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
설명
이것이 하는 일: 위 코드는 사용자 정보, 주문 목록, 추천 항목을 세 개의 외부 서비스에서 동시에 가져와 하나의 대시보드 응답으로 합칩니다. 첫 번째로, 각 함수의 async fn은 함수가 비동기 작업을 수행한다는 것을 선언합니다.
sleep().await는 실제 네트워크 요청을 시뮬레이션하며, .await를 만나면 현재 작업을 일시 중단하고 런타임에게 제어를 넘깁니다. 이 동안 다른 요청들이 처리되므로 스레드가 블록되지 않습니다.
그 다음으로, tokio::join! 매크로가 세 함수를 동시에 시작합니다. 순차적으로 호출하면 100+150+120=370ms가 걸리지만, 병렬로 실행하면 가장 긴 150ms만 걸립니다.
이는 2배 이상의 성능 향상입니다. join!은 모든 작업이 완료될 때까지 기다리며, 하나라도 실패하면 에러를 반환합니다.
마지막으로, Actix-web의 런타임이 이 모든 것을 투명하게 관리합니다. #[actix_web::main] 매크로가 Tokio 런타임을 초기화하고, 각 워커 스레드는 수천 개의 Future를 동시에 처리할 수 있습니다.
CPU 코어가 4개라면 4개의 워커 스레드가 수만 개의 동시 연결을 처리하는 것이 가능합니다. 여러분이 이 코드를 사용하면 복잡한 의존성을 가진 API도 효율적으로 구현할 수 있습니다.
실무에서의 이점으로는 첫째, 서버 비용을 크게 절감할 수 있습니다. 둘째, 사용자 경험이 개선되어 이탈률이 감소합니다.
셋째, 마이크로서비스 간 통신이 많은 현대 아키텍처에 최적화되어 있습니다.
실전 팁
💡 tokio::try_join!을 사용하면 에러가 발생한 즉시 중단하고 나머지 작업을 취소할 수 있습니다.
💡 CPU 집약적인 작업은 tokio::task::spawn_blocking()으로 별도 스레드 풀에서 실행하세요. 비동기 런타임을 블록하지 않습니다.
💡 futures::future::join_all()로 동적인 개수의 Future를 병렬 실행할 수 있습니다.
💡 타임아웃이 필요하면 tokio::time::timeout(Duration::from_secs(5), my_async_fn()).await로 감싸세요.
💡 async-stream 크레이트로 비동기 스트림을 만들면 대용량 데이터를 청크 단위로 처리할 수 있습니다.
댓글 (0)
함께 보면 좋은 카드 뉴스
OpenCode 기여와 확장 개발 완벽 가이드
오픈소스 프로젝트 OpenCode에 기여하고 확장 기능을 개발하는 방법을 알아봅니다. CONTRIBUTING.md 가이드부터 PR 제출까지 실무에서 바로 활용할 수 있는 내용을 담았습니다.
Permission과 보안 시스템 완벽 가이드
Claude Code의 Permission 시스템을 통해 보안을 관리하는 방법을 알아봅니다. 권한 모델, 파일 보호, 명령어 제어 등 실무에서 필요한 보안 개념을 쉽게 설명합니다.
Plugin 시스템과 확장성 완벽 가이드
AI Agent 개발에서 핵심이 되는 플러그인 아키텍처를 배웁니다. 플러그인 구조 설계부터 인증, 도구 플러그인 개발, 그리고 실제 GitHub Copilot 사례까지 단계별로 살펴봅니다.
TUI 터미널 사용자 인터페이스 구현 완벽 가이드
SolidJS 기반의 OpenTUI 프레임워크를 활용하여 터미널에서 동작하는 현대적인 사용자 인터페이스를 구축하는 방법을 알아봅니다. CLI 도구를 넘어 인터랙티브한 TUI 애플리케이션 개발의 핵심을 다룹니다.
Hono 기반 서버 API 완벽 가이드
Hono 프레임워크를 활용한 서버 API 구축 방법을 알아봅니다. 클라이언트/서버 아키텍처부터 OpenAPI 스펙 자동 생성, SSE 이벤트 스트리밍까지 실무에서 바로 활용할 수 있는 핵심 개념을 다룹니다.