Rust의 모듈 시스템으로 대규모 웹 프로젝트 구조화하기
Ethan Miller
Product Engineer · Leapcell

소개
대규모 웹 애플리케이션 개발은 고유한 과제를 안겨주며, 특히 끊임없이 증가하는 코드베이스를 관리하는 것이 그중 하나입니다. 프로젝트가 확장됨에 따라 부실한 구성은 빠르게 뒤얽힌 종속성, 어려운 탐색, 신규 팀원에게 가파른 학습 곡선으로 이어질 수 있습니다. Rust 생태계에서 성능과 안전이 가장 중요하므로, 코드 구조에 대한 동등하게 강력한 접근 방식이 필수적입니다. 이 글은 Rust의 강력한 모듈 시스템, 특히 mod와 use의 실용적인 적용에 대해 살펴보고, 이를 활용하여 상당한 웹 프로젝트를 위한 깨끗하고 유지보수 가능하며 확장 가능한 아키텍처를 만드는 방법을 보여줍니다. 이처럼 간단해 보이는 키워드가 복잡한 로직을 구성하고, 코드 재사용성을 촉진하며, 생산적인 개발 환경을 조성하는 데 필수적인 도구가 되는 방법을 살펴봅니다.
Rust 모듈 및 적용 이해하기
실용적인 예시를 살펴보기 전에, Rust 모듈 시스템의 기반이 되는 핵심 개념을 간략하게 정의해 보겠습니다.
- 모듈 (
mod): 모듈은 라이브러리 또는 바이너리 크레이트 내에서 코드를 구성하는 방법입니다. 함수, 구조체, 열거형, 트레이트 및 다른 모듈까지 포함할 수 있습니다. 모듈은 명확한 계층 구조를 만들고 항목의 가시성을 제어합니다. 기본적으로 모듈 내의 항목은 해당 모듈에 비공개입니다. - 가시성 키워드 (
pub,pub(crate),pub(super),pub(in super::super)): 이러한 키워드는 항목에 액세스할 수 있는 위치를 결정합니다.pub는 항목을 모듈 외부의 모든 코드에 공개적으로 보이게 합니다.pub(crate)는 가시성을 현재 크레이트로 제한합니다.pub(super)는 항목을 상위 모듈에 보이게 합니다.pub(in path)는 특정 경로에 대한 가시성을 정밀하게 제어할 수 있게 합니다. - 사용 선언 (
use):use키워드는 모듈의 항목을 현재 범위로 가져와 더 짧은 이름으로 참조할 수 있게 합니다. 이렇게 하면 완전한 경로를 반복적으로 입력할 필요 없이 가독성이 향상됩니다.
이해를 바탕으로, 가상의 대규모 웹 애플리케이션, 예를 들어 전자상거래 플랫폼을 고려하여 어떻게 구성할 수 있는지 살펴보겠습니다.
핵심 아키텍처 원칙
대규모 웹 프로젝트의 경우 일반적으로 계층형 아키텍처를 목표로 합니다. 일반적인 패턴은 다음과 같습니다.
- 애플리케이션 진입점:
main.rs(라이브러리의 경우lib.rs) - 구성: 환경 변수, 데이터베이스 연결 등을 처리합니다.
- 라우트/컨트롤러: API 엔드포인트를 정의하고 들어오는 요청을 처리합니다.
- 서비스/비즈니스 로직: 핵심 비즈니스 규칙을 캡슐화하고 데이터 액세스를 오케스트레이션합니다.
- 모델/엔티티: 데이터 구조(예: 사용자, 제품, 주문)를 나타냅니다.
- 데이터베이스 액세스/리포지토리: 데이터베이스와 상호 작용합니다.
- 유틸리티/공유: 일반적인 헬퍼 함수, 오류 처리 등
mod 및 use를 사용한 구조 구현
우리의 전자상거래 프로젝트가 단일 바이너리 크레이트로 구성되어 있다고 상상해 보겠습니다. src 디렉토리는 다음과 같이 구성될 수 있습니다.
src/
├── main.rs
├── config.rs
├── db/
│ ├── mod.rs
│ └── schema.rs
│ └── models.rs
│ └── products.rs
│ └── users.rs
│ └── orders.rs
├── routes/
│ ├── mod.rs
│ ├── auth.rs
│ ├── products.rs
│ └── users.rs
├── services/
│ ├── mod.rs
│ ├── auth.rs
│ ├── products.rs
│ └── users.rs
└── utils/
├── mod.rs
└── error.rs
└── helpers.rs
main.rs 진입점
main.rs는 애플리케이션의 여러 부분을 통합하는 오케스트레이터 역할을 합니다.
// src/main.rs mod config; mod db; mod routes; mod services; mod utils; // 이름 충돌을 피하고 경로를 짧게 유지하기 위해 일반적으로 특정 항목을 `use`합니다. use crate::config::app_config; use crate::db::establish_connection; use crate::routes::create_router; #[tokio::main] async fn main() { // 구성 로드 let config = app_config::load().expect("Failed to load configuration"); // 데이터베이스 연결 설정 let db_pool = establish_connection(&config.database_url) .await .expect("Failed to connect to database"); // 애플리케이션 전체 상태 초기화 (예: 공유 리소스용 Arc) let app_state = todo!(); // 실제 애플리케이션 상태 자리 표시자 // 웹 서버 생성 및 실행 let app = create_router(app_state); let listener = tokio::net::TcpListener::bind(&config.server_address) .await .expect("Failed to bind server address"); println!("Server running on {}", config.server_address); axum::serve(listener, app) .await .expect("Server failed to start"); }
main.rs 상단의 mod 선언은 최상위 모듈을 도입합니다. 그런 다음 use crate::module::item은 특정 항목을 범위 안으로 가져와 전체 경로 없이 직접 액세스할 수 있게 합니다.
하위 모듈 구성
예시로 db 모듈의 중첩된 구성을 살펴보겠습니다.
// src/db/mod.rs pub mod schema; // 데이터베이스 스키마 정의 (예: Diesel 매크로 사용) pub mod models; // 데이터베이스 테이블에 매핑되는 Rust 구조체 정의 pub mod products; // 제품 데이터 액세스와 관련된 함수 포함 pub mod users; // 사용자 데이터 액세스와 관련된 함수 포함 pub mod orders; // 주문 데이터 액세스와 관련된 함수 포함 use diesel::PgConnection; use diesel::r2d2::{ConnectionManager, Pool}; use std::env; pub type DbPool = Pool<ConnectionManager<PgConnection>>; pub async fn establish_connection(database_url: &str) -> Result<DbPool, String> { let manager = ConnectionManager::<PgConnection>::new(database_url); Pool::builder() .test_on_check_out(true) .build(manager) .map_err(|e| format!("Failed to create pool: {}", e)) }
여기서 pub mod는 schema, models, products 등을 crate::db 경로 내의 다른 모듈에서 사용할 수 있게 합니다. establish_connection 함수도 pub이므로 main.rs에서 호출할 수 있습니다. db/mod.rs 내의 use 선언은 db 모듈 자체에 국한됩니다.
이제 src/db/products.rs를 살펴보겠습니다.
// src/db/products.rs use diesel::prelude::*; use crate::db::{DbPool, models::Product}; // 상위 및 형제 모듈의 항목 사용 pub async fn find_all_products(pool: &DbPool) -> Result<Vec<Product>, String> { todo!() // 실제 제품 가져오기 로직 } pub async fn find_product_by_id(pool: &DbPool, product_id: i32) -> Result<Option<Product>, String> { todo!() // 실제 제품 가져오기 로직 } pub async fn create_product(pool: &DbPool, new_product: NewProduct) -> Result<Product, String> { todo!() // 실제 제품 생성 로직 } // ... 기타 제품 관련 DB 작업
src/db/products.rs 내에서 use crate::db::{DbPool, models::Product}를 사용합니다. DbPool은 src/db/mod.rs에서 노출되고, models::Product는 db 상위 모듈 내의 products 모듈과 형제 관계인 models 모듈에서 옵니다. 이것은 모듈 계층 구조를 탐색하는 방법을 보여줍니다.
라우트 및 서비스
routes 모듈은 일반적으로 Axum 또는 Actix-web과 같은 웹 프레임워크를 사용하여 API 엔드포인트를 정의합니다. services 모듈은 비즈니스 로직을 캡슐화하여 라우트와 데이터베이스 액세스 사이의 중개자 역할을 합니다.
// src/routes/mod.rs pub mod auth; pub mod products; pub mod users; use axum::{routing::get, Router}; use std::sync::Arc; use crate::utils::error::AppError; // 사용자 정의 오류 유형 가져오기 예시 pub struct AppState { // 공유 애플리케이션 상태 예시 pub db_pool: crate::db::DbPool, // 기타 공유 리소스 } // 메인 애플리케이션 라우터 생성 함수 pub fn create_router(app_state: Arc<AppState>) -> Router { Router::new() .route("/", get(|| async { "Hello, world!" })) .nest("/auth", auth::auth_routes(app_state.clone())) .nest("/products", products::product_routes(app_state.clone())) .nest("/users", users::user_routes(app_state.clone())) // 여기에 더 많은 라우트를 추가하세요 }
// src/routes/products.rs use axum::{ extract::{Path, State}, Json, Router, routing::{get, post}, }; use serde::{Deserialize, Serialize}; use std::sync::Arc; use crate::routes::AppState; // routes/mod.rs에 정의된 AppState 가져오기 use crate::services; // services 모듈 액세스 use crate::utils::error::AppError; // 응답에 사용자 정의 오류 유형 사용 #[derive(Serialize)] struct ProductResponse { id: i32, name: String, price: f64, } #[derive(Deserialize)] struct CreateProductRequest { name: String, price: f64, } pub fn product_routes(app_state: Arc<AppState>) -> Router { Router::new() .route("/", get(get_all_products).post(create_product)) .route("/:id", get(get_product_by_id)) .with_state(app_state) } async fn get_all_products(State(app_state): State<Arc<AppState>>) -> Result<Json<Vec<ProductResponse>>, AppError> { let products = services::products::get_all_products(&app_state.db_pool) .await? .into_iter() .map(|p| ProductResponse { id: p.id, name: p.name, price: p.price as f64 }) .collect(); Ok(Json(products)) } async fn get_product_by_id( State(app_state): State<Arc<AppState>>, Path(product_id): Path<i32>, ) -> Result<Json<ProductResponse>, AppError> { let product = services::products::get_product_by_id(&app_state.db_pool, product_id) .await? .map(|p| ProductResponse { id: p.id, name: p.name, price: p.price as f64 }) .ok_or(AppError::NotFound)?; Ok(Json(product)) } async fn create_product( State(app_state): State<Arc<AppState>>, Json(payload): Json<CreateProductRequest>, ) -> Result<Json<ProductResponse>, AppError> { let new_product = services::products::create_product(&app_state.db_pool, payload.name, payload.price) .await? .map(|p| ProductResponse { id: p.id, name: p.name, price: p.price as f64 }) .ok_or(AppError::InternalServerError)?; // 오류 처리 예시 Ok(Json(new_product)) }
services 모듈에는 get_all_products 등의 실제 구현이 포함되며, db 모듈의 함수를 호출합니다.
// src/services/products.rs use crate::db; // 데이터베이스 관련 기능 액세스 use crate::db::DbPool; use crate::db::models::{Product, NewProduct}; use crate::utils::error::AppError; pub async fn get_all_products(pool: &DbPool) -> Result<Vec<Product>, AppError> { db::products::find_all_products(pool) .await .map_err(|e| AppError::InternalServerErrorDetail(e.to_string())) } pub async fn get_product_by_id(pool: &DbPool, product_id: i32) -> Result<Option<Product>, AppError> { db::products::find_product_by_id(pool, product_id) .await .map_err(|e| AppError::InternalServerErrorDetail(e.to_string())) } pub async fn create_product(pool: &DbPool, name: String, price: f64) -> Result<Option<Product>, AppError> { let new_product = NewProduct { name, price: price as i32 }; // 단순화를 위해 가격을 DB에서 i32로 가정 db::products::create_product(pool, new_product) .await .map_err(|e| AppError::InternalServerErrorDetail(e.to_string())) }
src/services/products.rs에서 use crate::db;를 사용하여 데이터베이스 관련 함수에 액세스합니다. 그런 다음 db::products::find_all_products 및 유사한 함수를 호출합니다. 이러한 명확한 관심사 분리는 라우트가 씬(thin)하게 유지되고, 요청/응답 구문 분석만 처리하며, 비즈니스 로직을 서비스에 위임하고, 서비스는 다시 데이터 액세스를 데이터베이스 계층에 위임하도록 보장합니다.
이 접근 방식의 이점
- 명확성과 가독성: 애플리케이션을 논리적 모듈로 분할하면 코드베이스를 탐색하고 이해하기가 훨씬 쉬워집니다.
- 유지보수성: 데이터베이스 스키마와 같은 한 영역의 변경 사항이 애플리케이션의 관련 없는 부분으로 전파될 가능성이 줄어듭니다.
- 테스트 용이성: 개별 모듈(예: 서비스, 데이터베이스 작업)을 독립적으로 단위 테스트하여 더 강력한 소프트웨어를 만들 수 있습니다.
- 협업: 여러 개발자가 충돌을 줄이고 책임에 대한 명확한 이해를 바탕으로 다양한 모듈에서 동시에 작업할 수 있습니다.
- 캡슐화: 모듈은 무엇을 노출(
pub)하고 무엇을 내부에 유지할지 제어하여 최소 권한 원칙을 따르고 의도하지 않은 액세스를 방지합니다.
결론
mod와 use로 지원되는 Rust의 모듈 시스템은 가장 복잡한 웹 애플리케이션조차도 구조화할 수 있는 강력하고 유연한 기반을 제공합니다. 코드를 논리적 모듈로 신중하게 구성하고 명시적인 가시성 규칙을 사용함으로써 개발자는 매우 유지보수 가능하고, 확장 가능하며, 이해하기 쉬운 프로젝트를 구축할 수 있습니다. 이 체계적인 접근 방식은 개발 프로세스를 향상시킬 뿐만 아니라 애플리케이션이 성장하고 발전함에 따라 아키텍처가 견고하게 유지되도록 보장합니다. 모듈 시스템을 마스터하는 것은 대규모 웹 개발에서 Rust의 잠재력을 최대한 발휘하는 열쇠입니다.

