이 문서는 로드맵 8단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 가시성(
pub), 경로(use), 파일↔모듈 트리 대응, 워크스페이스, feature/cfg를 다룹니다. axum 프로젝트를handlers/models/errors로 나누는 감각이 목표입니다.
선수: 앞 단계 문법. 이 단계는 코드 배치와 공개 API에 초점.
// lib.rs 또는 main.rs
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {} // 모듈 안에서만
}
mod serving {
fn take_order() {}
}
}
pub fn eat_at_restaurant() {
// 절대 경로
crate::front_of_house::hosting::add_to_waitlist();
// 상대 경로
front_of_house::hosting::add_to_waitlist();
}crate::— 크레이트 루트부터self::— 현재 모듈super::— 부모 모듈
| 키워드 | 의미 |
|---|---|
| (없음) | 부모 모듈까지 private |
pub |
바깥 크레이트에도 공개 |
pub(crate) |
이 크레이트 안에서만 공개 |
pub(super) |
부모 모듈에만 |
pub(in path) |
특정 경로 아래에서만 |
mod shop {
pub struct Coffee {
pub name: String, // 필드는 각각 pub 필요
price_cents: u32, // private 필드
}
impl Coffee {
pub fn new(name: impl Into<String>, price_cents: u32) -> Self {
Self {
name: name.into(),
price_cents,
}
}
pub(crate) fn price_cents(&self) -> u32 {
self.price_cents
}
}
}구조체를 pub으로 해도 필드는 기본 private입니다. 생성자·게터로 API를 통제하는 패턴이 흔합니다.
use crate::front_of_house::hosting;
pub fn eat() {
hosting::add_to_waitlist();
}
// 재수출 — 내부 구조를 숨기고 루트 API만 노출
pub use crate::front_of_house::hosting::add_to_waitlist;
// 이름 충돌 시
use std::fmt::Result as FmtResult;
use std::io::Result as IoResult;관례:
- 구조체·enum·트레잇 →
use로 타입 이름까지 - 함수 → 모듈까지만
use하고module::func()(출처가 보임) - 예외:
use std::collections::HashMap;처럼 너무 흔한 것
- nested
mod에서pub을 빼먹고 호출해 에러 확인 -
pub(crate)함수를 같은 크레이트 / (가상으로) 외부에서 쓰는 차이 설명
src/
main.rs // 또는 lib.rs
models.rs // mod models;
models/
user.rs // models.rs 안에서 mod user;
post.rs
handlers.rs
handlers/
auth.rs
posts.rs
errors.rs
// main.rs
mod models;
mod handlers;
mod errors;
fn main() {
// ...
}// models.rs
pub mod user;
pub mod post;
pub use user::User; // 재수출로 crate::models::User 가능// models/user.rs
pub struct User {
pub id: u64,
pub name: String,
}src/
models/
mod.rs // 예전엔 폴더 = mod.rs
user.rs
둘 다 유효합니다. 한 프로젝트에선 한 스타일로 통일하세요. 새 코드는 models.rs + models/ 조합이 읽기 쉽습니다.
- 바이너리만:
main.rs - 라이브러리:
lib.rs가 루트 - 둘 다 있으면:
main.rs가use my_crate::...로 라이브러리를 호출 — 로직은 lib에, 진입점만 main이 이상적
-
mod foo;+foo.rs파일을 실제로 만들어 컴파일 - 서브모듈
foo/bar.rs를foo.rs에서mod bar;로 연결
여러 크레이트를 한 저장소에서 공통 lockfile로 관리합니다.
# 루트 Cargo.toml
[workspace]
resolver = "2"
members = [
"crates/api",
"crates/core",
"crates/macros",
]my-project/
Cargo.toml # workspace
Cargo.lock
crates/
api/Cargo.toml
api/src/main.rs
core/Cargo.toml
core/src/lib.rs
# crates/api/Cargo.toml
[dependencies]
core = { path = "../core" }
tokio = { version = "1", features = ["full"] }이점: 의존성 버전 통일, cargo test --workspace, 공통 프로파일.
- (개념) axum 앱과 도메인 로직을
api/core로 나눈다고 가정하고 members 목록 스케치
# Cargo.toml
[features]
default = ["json"]
json = ["dep:serde_json"]
metrics = []
[dependencies]
serde_json = { version = "1", optional = true }#[cfg(feature = "json")]
pub fn to_json<T: serde::Serialize>(v: &T) -> String {
serde_json::to_string(v).unwrap()
}
#[cfg(not(feature = "json"))]
pub fn to_json<T>(_v: &T) -> String {
String::from("{}")
}빌드: cargo build --features metrics / --no-default-features
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
#[cfg(target_os = "linux")]
fn os_specific() {}
#[cfg(debug_assertions)]
fn only_debug() {}- optional 의존성 하나를 feature 뒤에 두고
cfg로 감싸 보기 -
#[cfg(test)]모듈에 테스트 하나 작성
실제 파일로 나누는 목표 구조:
src/
main.rs
lib.rs // 재사용·테스트용 루트
errors.rs
models/
mod.rs 또는 models.rs + …
user.rs
handlers/
mod.rs
health.rs
users.rs
state.rs
// errors.rs
use std::fmt;
#[derive(Debug)]
pub enum AppError {
NotFound,
Internal(String),
}
impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
AppError::NotFound => write!(f, "not found"),
AppError::Internal(msg) => write!(f, "internal: {msg}"),
}
}
}
impl std::error::Error for AppError {}// models/user.rs
#[derive(Debug, Clone)]
pub struct User {
pub id: u64,
pub name: String,
}// handlers/users.rs (axum 없이도 시그니처 감각)
use crate::errors::AppError;
use crate::models::User;
pub fn get_user(id: u64) -> Result<User, AppError> {
if id == 0 {
return Err(AppError::NotFound);
}
Ok(User {
id,
name: String::from("async"),
})
}// lib.rs
pub mod errors;
pub mod handlers;
pub mod models;
pub mod state;
pub use errors::AppError;// main.rs
use my_crate::handlers;
fn main() {
match handlers::users::get_user(1) {
Ok(u) => println!("{u:?}"),
Err(e) => eprintln!("{e}"),
}
}실습 순서:
- 빈 바이너리/라이브러리에 위 모듈 파일 생성
cargo check로 경로·가시성 에러를 하나씩 해결- (선택) axum
Router를handlers에 모으고State(Arc<AppState>)는state.rs
-
pub use로crate::User처럼 flat re-export API 만들기 - workspace에
core(순수 로직) /api(axum) 분리 -
metricsfeature로 로그 한 줄 조건부 컴파일
mod 트리 + pub / pub(crate)
use / pub use (재수출)
파일: foo.rs + foo/ 또는 foo/mod.rs
workspace members
features + #[cfg(...)]
실전: handlers / models / errors / state
9단계 매크로에서는 macro_rules!, derive, proc macro를 “읽고 쓰는” 수준으로 다룹니다. #[derive(Debug)]와 axum/serde 매크로가 덜 마법처럼 보이게 됩니다.