Skip to content

Latest commit

 

History

History
382 lines (295 loc) · 7.82 KB

08.md

File metadata and controls

382 lines (295 loc) · 7.82 KB

Rust 문법 로드맵 — 8단계: 모듈 시스템 & 프로젝트 구조 (상세 교재)

이 문서는 로드맵 8단계를 코드 예제와 함께 상세히 풀어놓은 교재입니다. 가시성(pub), 경로(use), 파일↔모듈 트리 대응, 워크스페이스, feature/cfg를 다룹니다. axum 프로젝트를 handlers / models / errors로 나누는 감각이 목표입니다.

선수: 앞 단계 문법. 이 단계는 코드 배치와 공개 API에 초점.


8.1 mod / pub / pub(crate) / use

모듈은 트리다

// 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로 경로 단축

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) 함수를 같은 크레이트 / (가상으로) 외부에서 쓰는 차이 설명

8.2 파일 시스템 ↔ 모듈 트리

2018+ 스타일 (권장)

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,
}

구식 mod.rs 스타일 (아직 많이 봄)

src/
  models/
    mod.rs      // 예전엔 폴더 = mod.rs
    user.rs

둘 다 유효합니다. 한 프로젝트에선 한 스타일로 통일하세요. 새 코드는 models.rs + models/ 조합이 읽기 쉽습니다.

main.rs vs lib.rs

  • 바이너리만: main.rs
  • 라이브러리: lib.rs가 루트
  • 둘 다 있으면: main.rs가 use my_crate::...로 라이브러리를 호출 — 로직은 lib에, 진입점만 main이 이상적

체크포인트

  • mod foo; + foo.rs 파일을 실제로 만들어 컴파일
  • 서브모듈 foo/bar.rs를 foo.rs에서 mod bar;로 연결

8.3 워크스페이스

여러 크레이트를 한 저장소에서 공통 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 목록 스케치

8.4 [features]와 #[cfg]

Cargo features

# 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

#[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)] 모듈에 테스트 하나 작성

8.5 종합 실습 — axum 스타일 모듈 분리 (스케치)

실제 파일로 나누는 목표 구조:

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}"),
    }
}

실습 순서:

  1. 빈 바이너리/라이브러리에 위 모듈 파일 생성
  2. cargo check로 경로·가시성 에러를 하나씩 해결
  3. (선택) axum Router를 handlers에 모으고 State(Arc<AppState>)는 state.rs

확장 과제

  • pub use로 crate::User처럼 flat re-export API 만들기
  • workspace에 core(순수 로직) / api(axum) 분리
  • metrics feature로 로그 한 줄 조건부 컴파일

이 단계 한 장 요약

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 매크로가 덜 마법처럼 보이게 됩니다.