반응형
    
    
    
  이번 섹션에서는 Rust와 Axum을 활용하여 간단한 가계부 웹 애플리케이션을 구축합니다. 프로젝트 요구사항을 분석하고, CRUD 엔드포인트를 구현하며, 요청 파라미터와 경로 변수를 처리하는 방법을 배워보겠습니다. 또한 JSON 데이터의 직렬화 및 역직렬화, 에러 처리와 커스텀 에러 응답까지 다루어 실제 서비스 개발에 필요한 기본기를 익힙니다.
2-1. 프로젝트 요구사항 분석 및 설계
1. 프로젝트 개요
- 목적: 간단한 가계부 애플리케이션을 구축하여 수입과 지출을 관리할 수 있도록 합니다.
 - 주요 기능:
- 가계부(Book) 생성, 조회, 수정, 삭제 기능
 - 수입/지출 기록(Record) 추가, 조회, 수정, 삭제 기능
 - 카테고리(Category) 관리 (선택 사항)
 
 
2. 데이터 모델 설계
간단하게 하기 위해 데이터 모델을 다음과 같이 설계합니다.
테이블 목록
- tb_book: 가계부 정보를 저장합니다.
 - tb_record: 수입/지출 기록을 저장합니다.
 
테이블 구조
1. tb_book
| 컬럼 이름 | 데이터 타입 | 제약 조건 | 설명 | 
|---|---|---|---|
| id | SERIAL | PRIMARY KEY | 가계부 ID | 
| name | VARCHAR(50) | NOT NULL | 가계부 이름 | 
2. tb_record
| 컬럼 이름 | 데이터 타입 | 제약 조건 | 설명 | 
|---|---|---|---|
| id | SERIAL | PRIMARY KEY | 기록 ID | 
| book_id | INTEGER | FOREIGN KEY(tb_book.id) | 가계부 ID | 
| amount | INTEGER | NOT NULL | 금액 | 
| memo | VARCHAR(32) | 메모 | |
| target_dt | DATETIME | NOT NULL | 거래 일시 | 
| is_income | BOOLEAN | NOT NULL | income - true expense - false  | 
3. 엔드포인트 설계
가계부(Book) 엔드포인트
- POST /books: 새로운 가계부 생성
 - GET /books: 모든 가계부 조회
 - GET /books/{id}: 특정 가계부 조회
 - PUT /books/{id}: 가계부 정보 수정
 - DELETE /books/{id}: 가계부 삭제
 
기록(Record) 엔드포인트
- POST /books/{book_id}/records: 특정 가계부에 새로운 기록 추가
 - GET /books/{book_id}/records: 특정 가계부의 모든 기록 조회
 - GET /books/{book_id}/records/{id}: 특정 기록 조회
 - PUT /books/{book_id}/records/{id}: 기록 수정
 - DELETE /books/{book_id}/records/{id}: 기록 삭제
 
2-2. CRUD 엔드포인트 구현 (예: 가계부 관리)
1. 프로젝트 생성 및 설정
Cargo.toml에 필요한 의존성을 추가합니다.
[dependencies]
axum = "0.6"
tokio = { version = "1", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
2. 데이터 구조 정의
src/main.rs
use axum::{
    routing::{get, post, put, delete},
    Router, Json, extract::{Path, Query},
    http::StatusCode,
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
// 가계부 구조체
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}
// 애플리케이션 상태: 가계부 목록을 저장
type AppState = Arc<Mutex<Vec<Book>>>;
#[tokio::main]
async fn main() {
    let state = Arc::new(Mutex::new(Vec::new()));
    let app = Router::new()
        .route("/books", post(create_book).get(get_books))
        .route("/books/:id", get(get_book).put(update_book).delete(delete_book))
        .with_state(state);
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("서버가 {}에서 시작됩니다...", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}
3. 가계부 엔드포인트 핸들러 구현
3.1 가계부 생성 (POST /books)
async fn create_book(
    state: axum::extract::State<AppState>,
    Json(payload): Json<BookInput>,
) -> (StatusCode, Json<Book>) {
    let mut books = state.lock().unwrap();
    let new_id = (books.len() + 1) as u32;
    let book = Book {
        id: new_id,
        name: payload.name,
    };
    books.push(book.clone());
    (StatusCode::CREATED, Json(book))
}
#[derive(Debug, Deserialize)]
struct BookInput {
    name: String,
}
3.2 가계부 목록 조회 (GET /books)
async fn get_books(state: axum::extract::State<AppState>) -> Json<Vec<Book>> {
    let books = state.lock().unwrap();
    Json(books.clone())
}
3.3 특정 가계부 조회 (GET /books/:id)
async fn get_book(
    state: axum::extract::State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<Book>, StatusCode> {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(StatusCode::NOT_FOUND)
}
3.4 가계부 수정 (PUT /books/:id)
async fn update_book(
    state: axum::extract::State<AppState>,
    Path(id): Path<u32>,
    Json(payload): Json<BookInput>,
) -> Result<Json<Book>, StatusCode> {
    let mut books = state.lock().unwrap();
    if let Some(book) = books.iter_mut().find(|book| book.id == id) {
        book.name = payload.name.clone();
        Ok(Json(book.clone()))
    } else {
        Err(StatusCode::NOT_FOUND)
    }
}
3.5 가계부 삭제 (DELETE /books/:id)
async fn delete_book(
    state: axum::extract::State<AppState>,
    Path(id): Path<u32>,
) -> StatusCode {
    let mut books = state.lock().unwrap();
    if books.iter().any(|book| book.id == id) {
        books.retain(|book| book.id != id);
        StatusCode::NO_CONTENT
    } else {
        StatusCode::NOT_FOUND
    }
}
2-3. 요청 파라미터와 경로 변수 처리 방법
1. 경로 변수 추출 (Path)
- 경로 변수는 URL 경로에서 값을 추출합니다.
 Path<T>를 사용하여 경로 변수를 추출할 수 있습니다.
예시:
async fn get_book(Path(id): Path<u32>) { /* ... */ }
2. 쿼리 파라미터 추출 (Query)
- 쿼리 파라미터는 URL의 쿼리 문자열에서 값을 추출합니다.
 Query<T>를 사용하며,T는Deserialize를 구현해야 합니다.
예시:
async fn search_books(Query(params): Query<SearchParams>) { /* ... */ }
#[derive(Deserialize)]
struct SearchParams {
    name: Option<String>,
}
2-4. JSON 데이터 직렬화 및 역직렬화
serde크레이트를 사용하여 JSON 데이터를 직렬화(Serialize) 및 역직렬화(Deserialize)할 수 있습니다.- 구조체에 
#[derive(Serialize, Deserialize)]를 추가합니다. 
예시:
use serde::{Serialize, Deserialize};
#[derive(Debug, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}
- 핸들러 함수에서 
Json<T>를 사용하여 JSON 데이터를 자동으로 처리합니다. 
예시:
async fn create_book(Json(payload): Json<BookInput>) { /* ... */ }
2-5. 에러 처리와 커스텀 에러 응답
1. 기본 에러 처리
- 핸들러 함수의 반환 타입에 
Result<T, E>를 사용하여 에러를 처리할 수 있습니다. E로는axum::response::IntoResponse를 구현하는 타입을 사용해야 합니다.
예시:
async fn get_book(
    Path(id): Path<u32>,
) -> Result<Json<Book>, StatusCode> {
    // ...
}
2. 커스텀 에러 타입 정의
- 에러를 더 자세히 처리하기 위해 커스텀 에러 타입을 정의할 수 있습니다.
 thiserror크레이트를 사용하면 편리하게 에러 타입을 정의할 수 있습니다.
Cargo.toml에 의존성 추가:
[dependencies]
thiserror = "1.0"
에러 타입 정의:
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
    #[error("가계부를 찾을 수 없습니다.")]
    NotFound,
    #[error("내부 서버 오류가 발생했습니다.")]
    InternalServerError,
}
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status_code = match self {
            AppError::NotFound => StatusCode::NOT_FOUND,
            AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
        };
        let body = Json(json!({ "error": self.to_string() }));
        (status_code, body).into_response()
    }
}
핸들러 함수에서 사용:
async fn get_book(
    state: axum::extract::State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<Book>, AppError> {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound)
}
3. 에러 응답 커스터마이징
- 에러 메시지를 JSON 형태로 반환하여 클라이언트가 이해하기 쉽게 만듭니다.
 IntoResponse를 구현하여 에러 응답을 커스터마이징할 수 있습니다.
전체 코드 예시
src/main.rs
use axum::{
    routing::{get, post, put, delete},
    Router, Json, extract::{Path, State},
    http::StatusCode,
    response::{IntoResponse, Response},
};
use serde::{Deserialize, Serialize};
use std::sync::{Arc, Mutex};
use std::net::SocketAddr;
use thiserror::Error;
use serde_json::json;
// 데이터 구조체 정의
#[derive(Debug, Clone, Serialize, Deserialize)]
struct Book {
    id: u32,
    name: String,
}
#[derive(Debug, Deserialize)]
struct BookInput {
    name: String,
}
// 애플리케이션 상태
type AppState = Arc<Mutex<Vec<Book>>>;
// 에러 타입 정의
#[derive(Error, Debug)]
pub enum AppError {
    #[error("가계부를 찾을 수 없습니다.")]
    NotFound,
    #[error("내부 서버 오류가 발생했습니다.")]
    InternalServerError,
}
impl IntoResponse for AppError {
    fn into_response(self) -> Response {
        let status_code = match self {
            AppError::NotFound => StatusCode::NOT_FOUND,
            AppError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
        };
        let body = Json(json!({ "error": self.to_string() }));
        (status_code, body).into_response()
    }
}
// 핸들러 함수 구현
async fn create_book(
    State(state): State<AppState>,
    Json(payload): Json<BookInput>,
) -> (StatusCode, Json<Book>) {
    let mut books = state.lock().unwrap();
    let new_id = (books.len() + 1) as u32;
    let book = Book {
        id: new_id,
        name: payload.name,
    };
    books.push(book.clone());
    (StatusCode::CREATED, Json(book))
}
async fn get_books(State(state): State<AppState>) -> Json<Vec<Book>> {
    let books = state.lock().unwrap();
    Json(books.clone())
}
async fn get_book(
    State(state): State<AppState>,
    Path(id): Path<u32>,
) -> Result<Json<Book>, AppError> {
    let books = state.lock().unwrap();
    books
        .iter()
        .find(|book| book.id == id)
        .cloned()
        .map(Json)
        .ok_or(AppError::NotFound)
}
async fn update_book(
    State(state): State<AppState>,
    Path(id): Path<u32>,
    Json(payload): Json<BookInput>,
) -> Result<Json<Book>, AppError> {
    let mut books = state.lock().unwrap();
    if let Some(book) = books.iter_mut().find(|book| book.id == id) {
        book.name = payload.name.clone();
        Ok(Json(book.clone()))
    } else {
        Err(AppError::NotFound)
    }
}
async fn delete_book(
    State(state): State<AppState>,
    Path(id): Path<u32>,
) -> Result<StatusCode, AppError> {
    let mut books = state.lock().unwrap();
    if books.iter().any(|book| book.id == id) {
        books.retain(|book| book.id != id);
        Ok(StatusCode::NO_CONTENT)
    } else {
        Err(AppError::NotFound)
    }
}
#[tokio::main]
async fn main() {
    let state = Arc::new(Mutex::new(Vec::new()));
    let app = Router::new()
        .route("/books", post(create_book).get(get_books))
        .route("/books/:id", get(get_book).put(update_book).delete(delete_book))
        .with_state(state);
    let addr = SocketAddr::from(([127, 0, 0, 1], 3000));
    println!("서버가 {}에서 시작됩니다...", addr);
    axum::Server::bind(&addr)
        .serve(app.into_make_service())
        .await
        .unwrap();
}
테스트 방법
- 서버 실행
 cargo run- POST /books: 가계부 생성
 curl -X POST -H "Content-Type: application/json" -d '{"name": "내 가계부"}' http://localhost:3000/books- GET /books: 가계부 목록 조회
 curl http://localhost:3000/books- GET /books/1: 특정 가계부 조회
 curl http://localhost:3000/books/1- PUT /books/1: 가계부 수정
 curl -X PUT -H "Content-Type: application/json" -d '{"name": "업데이트된 가계부"}' http://localhost:3000/books/1- DELETE /books/1: 가계부 삭제
 curl -X DELETE http://localhost:3000/books/1
결론
이번 섹션에서는 Rust와 Axum을 활용하여 간단한 가계부 웹 애플리케이션을 구축하였습니다. 프로젝트 요구사항 분석부터 시작하여, CRUD 엔드포인트를 구현하고, 요청 파라미터와 경로 변수 처리, JSON 데이터의 직렬화 및 역직렬화, 에러 처리와 커스텀 에러 응답까지 다루었습니다.
참고 자료
Note: 이 글은 학습 목적으로 작성되었으며, 실제 서비스 개발 시에는 보안, 성능, 에러 처리 등 다양한 측면을 고려해야 합니다.
반응형
    
    
    
  '슬기로운 개발자생활 > Rust' 카테고리의 다른 글
| &Axum SQLx를 활용한 데이터베이스 중급 - Docker를 활용한 PostgreSQL 환경 설정 (0) | 2024.09.24 | 
|---|---|
| Rust와 Axum을 활용한 웹 백엔드 개발 - Axum 프레임워크 소개 (0) | 2024.09.24 | 
| Rust와 Axum을 활용한 웹 백엔드 개발 - 비동기 프로그래밍 (0) | 2024.09.23 |