learn

Rust Module System — Package, Crate, Module, Path, use

4단계 위계

Package
  └─ Crate (1개 이상)
       └─ Module (중첩 가능)
            └─ Item (function, struct, enum, ...)
단계의미비유
PackageCargo가 다루는 단위. Cargo.toml 하나당 하나Maven/Gradle 프로젝트
Crate컴파일러가 한 번에 처리하는 단위jar 하나, .so/.dll 하나
Modulecrate 내부의 namespace + privacy 경계Java package
Item실제 코드 조각function, class

Crate — binary와 library

  • Binary cratemain 함수 보유, 실행 파일로 컴파일됨
  • Library cratemain 없음, 다른 crate가 의존성으로 가져다 씀

한 package는 binary crate 여러 개 + library crate 최대 하나를 가질 수 있다. Cargo 관습:

파일역할
src/main.rsbinary crate의 root, 패키지 이름과 동일
src/lib.rslibrary crate의 root
src/bin/*.rs추가 binary crate들

가장 흔한 패턴: library crate(lib.rs)가 진짜 코드를 다 들고, binary crate(main.rs)는 그 라이브러리를 호출하는 얇은 진입점. cargo 자체도 이 패턴이며 이렇게 하면 라이브러리를 다른 사람이 의존할 수 있다.

Module — namespace + privacy

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

mod 키워드로 정의. module은 두 역할을 동시에 한다.

namespace — 같은 이름이라도 다른 module 안에 있으면 충돌하지 않는다.

privacy 경계 — 기본 private. 부모는 자식 내부를 못 보고, 자식은 부모/조상을 볼 수 있다. OOP의 private/public과 비슷하지만 단위가 module이지 class가 아니다. 같은 module 안 struct들은 서로의 private 필드를 본다 — Rust의 "friend"는 module 단위로 자동 작동한다.

Module Tree

crate (lib.rs / main.rs)        ← 암시적 루트
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         └── serve_order

파일 시스템의 디렉토리 트리와 정확히 같은 비유. Rust 컴파일러가 path를 풀어내는 기준 구조다.

Path — module tree에서 item 가리키기

두 가지 형태.

// absolute (crate root부터)
crate::front_of_house::hosting::add_to_waitlist();

// relative (현재 module부터)
front_of_house::hosting::add_to_waitlist();

특수 키워드.

  • super::foo — 부모 module의 foo (filesystem의 ..)
  • self::foo — 현재 module의 foo

book 권장은 absolute path — 코드 이동이 잦으니 root부터 명시하는 게 안정적.

pub 키워드 — 공개 제어

기본 private이라 부모가 자식 내부에 접근하려면 pub 필요.

mod front_of_house {
    pub mod hosting {                    // module 자체 공개
        pub fn add_to_waitlist() {}      // 안의 item 공개
    }
}

module을 pub으로 한다고 그 안의 모든 게 public이 되진 않는다. 각 item에 따로 pub을 붙여야 한다.

struct vs enum의 pub 차이

mod back_of_house {
    pub struct Breakfast {
        pub toast: String,        // 공개
        seasonal_fruit: String,   // 비공개 (기본)
    }

    pub enum Appetizer {
        Soup,    // 자동 공개
        Salad,   // 자동 공개
    }
}
  • struct: 타입 자체와 각 필드의 공개 여부가 분리. 부분 공개가 자연스럽다.
  • enum: variant가 다 공개되거나 다 비공개거나. variant를 분기 처리하려면 다 보여야 의미가 있어서 default가 자동 공개.

use — path 단축

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

filesystem의 symbolic link와 같은 비유. 그 scope 안에서만 유효하며, 자식 module로 들어가면 다시 가져와야 한다.

관용 패턴 — function vs struct/enum

// 함수: 부모 module까지만 use → 호출 때 출처 힌트
use crate::front_of_house::hosting;
hosting::add_to_waitlist();

// struct/enum: 끝까지 use
use std::collections::HashMap;
let mut map = HashMap::new();

함수는 호출 시 출처가 안 보이면 헷갈리고, 타입은 매번 적게 되니 끝까지 가져와도 괜찮다는 컨벤션.

as — 별칭

이름 충돌 시.

use std::fmt::Result;
use std::io::Result as IoResult;

pub use — 재노출

내부 구조와 외부 노출 구조를 분리할 때.

pub use crate::front_of_house::hosting;
// 외부에서 restaurant::hosting::add_to_waitlist()로 호출 가능

라이브러리 작성 시 "내 코드 구조"와 "사용자가 보는 API"를 분리하는 강력한 도구.

외부 패키지

# Cargo.toml
[dependencies]
rand = "0.8.5"
use rand::Rng;

std는 자동으로 들어와서 Cargo.toml에 적지 않아도 되지만 use는 필요하다.

중첩 path와 glob

use std::{cmp::Ordering, io};
use std::io::{self, Write};
use std::collections::*;   // glob — 가독성 해치니 조심

파일 분리 — modincludeimport도 아니다

이게 다른 언어와 결정적으로 다른 지점.

src/
├── lib.rs
├── front_of_house.rs        ← front_of_house module 본체
└── front_of_house/
    └── hosting.rs            ← hosting module 본체
// src/lib.rs
mod front_of_house;          // 컴파일러야, front_of_house가 어딘가 있어!

mod foo;선언이다. 컴파일러가 다음에서 본체를 찾는다.

  1. src/foo.rs (권장)
  2. src/foo/mod.rs (옛 스타일)

자식 module인 hostingfront_of_house 디렉토리 안에 들어가야 한다 — module tree 구조와 디렉토리 구조가 일치.

다른 언어와의 매핑

언어가져오기 메커니즘
Rustmod (선언) + use (단축) — 두 단계 분리
Java디렉토리에서 자동 추론 + import
Pythonimport (모듈 로딩 + namespace 가져옴)
C++#include (텍스트 복붙, namespace는 별개)

Rust에서 한 module은 module tree에 정확히 한 번만 mod로 선언돼야 한다. 같은 파일을 여러 번 mod 하면 같은 코드가 중복 컴파일되어 충돌. mod는 "코드 포함" 명령이지 "import"가 아님을 기억할 것.

작은 프로젝트 흐름

calculator/
├── Cargo.toml
└── src/
    ├── lib.rs
    └── ops/
        ├── arithmetic.rs
        └── trig.rs
// src/lib.rs
pub mod ops;
// src/ops.rs           ← ops module 본체 (서브모듈 선언 위치)
pub mod arithmetic;
pub mod trig;
// src/ops/arithmetic.rs
pub fn add(a: f64, b: f64) -> f64 { a + b }
// src/ops/trig.rs
pub fn sin(x: f64) -> f64 { x.sin() }

외부 사용자.

use calculator::ops::arithmetic;
use calculator::ops::trig::sin;

Module tree와 디렉토리 구조의 일치가 한눈에 보인다.

결론

Package(Cargo.toml) > Crate(main/lib.rs) > Module(mod 선언, 디렉토리와 일치) > Item의 4단계 위계가 module system의 전부다. mod선언(C++ #include나 Java import와 다름), use는 path 단축. pub으로 공개 제어, pub use로 내부 구조와 외부 API 분리. 디렉토리 구조와 module tree가 1:1 대응되도록 파일을 배치하는 게 관용.