Rust 모듈 시스템 큰 그림: mod, use, pub이 맞물리는 방식
Rust를 배우다 보면 어느 순간 코드가 한 파일에 다 들어가지 않게 됩니다.
처음에는 main.rs 하나로 충분하지만, 타입이 늘고 함수가 많아지면 자연스럽게 파일을 나누고 싶어지죠.
그때 등장하는 키워드가 mod, use, pub입니다.
문제는 이 셋이 비슷한 위치에 자주 나타난다는 점입니다.
파일 위쪽에 mod config;가 있고, 바로 아래에 use crate::config::Config;가 있으며, 다른 파일에는 pub struct Config가 있죠.
처음 보면 “이게 다 import인가?”, “왜 pub을 붙였는데 밖에서 안 보이지?”, “파일을 만들었는데 왜 컴파일러가 못 찾지?” 같은 질문이 생깁니다.
이번 글은 세부 문법을 깊게 파고드는 글이라기보다 Rust 모듈 시스템의 큰 지도를 그리는 글입니다.
mod는 모듈을 선언하고, use는 경로를 짧게 가져오며, pub은 공개 범위를 정합니다.
이 세 역할이 어떻게 맞물려 프로젝트 구조와 공개 API를 만드는지 큰 흐름부터 잡아보겠습니다.
모듈 시스템이 해결하는 문제
모듈 시스템은 단순히 파일을 여러 개로 나누기 위한 기능이 아닙니다. 물론 파일을 나누는 것도 중요하지만, Rust의 모듈은 그보다 조금 더 넓은 역할을 합니다.
우선 모듈은 이름 공간(namespace)을 만듭니다.
서로 다른 모듈 안에 같은 이름의 함수나 타입이 있어도 충돌하지 않습니다.
예를 들어 api::Error와 db::Error는 둘 다 Error라는 이름을 쓰지만 서로 다른 타입입니다.
mod api {
pub struct Error;
}
mod db {
pub struct Error;
}
또한 모듈은 코드의 경계를 만듭니다. 어떤 함수는 모듈 내부에서만 쓰고, 어떤 타입은 크레이트 전체에서 공유하고, 어떤 항목은 외부 사용자에게 공개할 수 있습니다. 이 경계를 잘 잡으면 내부 구현을 자유롭게 바꾸면서도 외부 API는 안정적으로 유지할 수 있습니다.
마지막으로 모듈은 프로젝트의 학습 비용을 줄입니다. 파일과 디렉터리 구조가 모듈 구조와 잘 맞아 있으면, 처음 보는 코드베이스에서도 “이 코드는 어디에 있고, 어디까지 공개되어 있나”를 빠르게 파악할 수 있습니다. Rust에서 모듈 시스템을 이해하는 건 문법 하나를 외우는 일이 아니라, 프로젝트를 읽고 설계하는 힘을 기르는 일에 가깝습니다.
mod, use, pub은 역할이 다릅니다
Rust 모듈 시스템을 처음 볼 때 가장 중요한 구분은 mod, use, pub의 역할을 섞지 않는 것입니다.
세 키워드는 자주 붙어 다니지만 서로 하는 일이 다릅니다.
mod는 모듈을 선언합니다.
“이 위치에 이런 이름의 자식 모듈이 있다”고 컴파일러에게 알려주는 역할입니다.
mod config;
fn main() {
let port = config::default_port();
println!("{port}");
}
pub fn default_port() -> u16 {
8080
}
여기서 mod config;는 src/config.rs 파일을 찾아 config 모듈의 본문으로 연결합니다.
더 자세한 파일 배치 규칙은 Rust mod 키워드에서 따로 다룹니다.
use는 이미 존재하는 경로를 현재 스코프로 가져옵니다.
긴 경로를 매번 쓰지 않고 짧은 이름으로 부르게 해주는 것이죠.
use std::collections::HashMap;
fn main() {
let mut scores = HashMap::new();
scores.insert("Alice", 90);
}
use는 모듈을 새로 만들지 않습니다.
use crate::config::default_port;라고 쓴다고 해서 config 모듈이 자동으로 생기지는 않습니다.
이미 모듈 트리에 존재하고 접근 가능한 항목만 가져올 수 있습니다.
이 차이는 Rust use 키워드에서 더 자세히 볼 수 있습니다.
pub은 공개 범위를 정합니다.
함수나 타입, 모듈을 다른 모듈에서 볼 수 있게 열어주는 키워드입니다.
pub struct Config {
pub port: u16,
}
다만 pub이 붙었다고 항상 외부 크레이트에서 바로 보이는 것은 아닙니다.
그 항목까지 가는 경로에 있는 모듈도 열려 있어야 하며, 필요하면 pub use로 공개 API를 따로 정리해야 합니다.
이 주제는 Rust 모듈 공개 범위에서 이어집니다.
크레이트 루트에서 모듈 트리가 시작됩니다
Rust의 모듈 구조는 크레이트 루트(crate root)에서 시작합니다.
바이너리 크레이트라면 보통 src/main.rs가 루트이고, 라이브러리 크레이트라면 src/lib.rs가 루트입니다.
예를 들어 이런 라이브러리 구조를 생각해보겠습니다.
src/
├── lib.rs
├── client.rs
├── error.rs
└── transport.rs
lib.rs가 크레이트 루트이므로 여기에서 자식 모듈을 선언합니다.
mod client;
mod error;
mod transport;
pub use client::Client;
pub use error::{Error, Result};
이 구조에서 client, error, transport는 모두 크레이트 내부 모듈입니다.
하지만 외부 사용자에게는 Client, Error, Result만 루트에서 보이도록 pub use로 재공개했습니다.
이게 Rust 모듈 시스템의 중요한 감각입니다. 파일 구조와 공개 API 구조가 반드시 같을 필요는 없습니다. 내부 파일은 유지보수하기 좋게 나누고, 외부 API는 사용자가 읽기 좋게 정리할 수 있습니다.
파일 구조와 공개 API는 분리할 수 있습니다
처음에는 pub mod client;처럼 모듈 자체를 공개하는 방식이 쉬워 보입니다.
pub mod client;
pub mod error;
이렇게 하면 외부 사용자는 my_crate::client::Client처럼 접근할 수 있습니다.
작은 프로젝트나 예제에서는 충분히 괜찮습니다.
하지만 라이브러리가 커지면 내부 파일 구조가 그대로 외부 API가 되어버립니다.
나중에 client.rs를 http/client.rs로 옮기고 싶어도 사용자 코드가 그 경로에 의존하고 있다면 변경이 어려워지죠.
그래서 라이브러리에서는 내부 모듈을 비공개로 두고, 필요한 타입만 재공개하는 방식을 자주 씁니다.
mod client;
mod error;
pub use client::Client;
pub use error::Error;
사용자는 더 단순한 경로를 씁니다.
use my_crate::{Client, Error};
내부에서는 파일을 어떻게 나누든 상관없습니다.
외부에 약속한 경로만 유지하면 됩니다.
이렇게 보면 pub use는 단순한 import 문법이 아니라 공개 API를 설계하는 도구입니다.
실무 구조를 한 번에 보면
조금 더 현실적인 예제를 보겠습니다. 간단한 HTTP 클라이언트 라이브러리를 만든다고 해볼게요.
src/
├── lib.rs
├── client.rs
├── error.rs
└── transport.rs
transport는 내부 구현입니다.
사용자는 HTTP 요청을 보내는 세부 함수를 몰라도 되고, Client만 쓰면 됩니다.
mod client;
mod error;
mod transport;
pub use client::Client;
pub use error::{Error, Result};
use crate::transport::send;
pub struct Client {
base_url: String,
}
impl Client {
pub fn new(base_url: impl Into<String>) -> Self {
Self {
base_url: base_url.into(),
}
}
pub fn get(&self, path: &str) -> crate::error::Result<String> {
send(&self.base_url, path)
}
}
pub(crate) fn send(base_url: &str, path: &str) -> crate::error::Result<String> {
Ok(format!("{base_url}/{path}"))
}
이 예제에는 모듈 시스템의 핵심이 모두 들어 있습니다.
lib.rs에서 mod로 내부 모듈을 선언하고, pub use로 외부 API를 정리합니다.
client.rs에서는 use crate::transport::send;로 내부 함수를 현재 스코프로 가져옵니다.
transport.rs의 send()는 pub(crate)라서 크레이트 내부에서는 쓸 수 있지만 외부 사용자에게는 보이지 않습니다.
이 구조를 이해하면 Rust 프로젝트를 볼 때 “파일이 어디 있나”와 “사용자에게 무엇을 공개하나”를 분리해서 읽을 수 있습니다. 그게 모듈 시스템을 제대로 쓰는 첫걸음입니다.
자주 헷갈리는 질문들
가장 흔한 질문은 “mod와 use가 둘 다 필요한가요?”입니다.
필요한 경우가 다릅니다.
내 크레이트 안의 파일을 모듈 트리에 연결하려면 부모 모듈에서 mod가 필요합니다.
그 모듈 안의 항목을 짧게 부르고 싶다면 use를 씁니다.
또 다른 질문은 “pub fn인데 왜 외부에서 안 보이나요?”입니다.
함수만 공개되어서는 부족할 수 있습니다.
외부에서 그 함수까지 도달하는 경로의 모듈도 공개되어야 합니다.
아니면 pub use로 루트에서 다시 공개해야 합니다.
“그럼 전부 pub mod로 열면 안 되나요?”라는 질문도 많습니다.
애플리케이션 내부 코드라면 큰 문제가 아닐 수 있습니다.
하지만 라이브러리라면 내부 구조를 API로 고정해버리는 결과가 됩니다.
외부 사용자가 의존할 경로는 의도적으로 작고 안정적으로 유지하는 편이 좋습니다.
마지막으로 “crate::와 super::는 언제 쓰나요?”도 자주 나옵니다.
crate::는 현재 크레이트 루트에서 시작하는 절대 경로라서, 중요한 내부 타입을 명확히 가져올 때 좋습니다.
super::는 부모 모듈에서 시작하므로 하위 모듈이 부모의 타입이나 함수를 참조할 때 자연스럽습니다.
테스트 모듈에서 use super::*;를 자주 보는 이유도 이 때문입니다.
마치며
Rust 모듈 시스템은 처음에는 낯설지만, 큰 그림은 생각보다 단순합니다.
mod는 모듈을 선언하고, use는 경로를 현재 스코프로 가져오고, pub은 공개 범위를 정합니다.
여기에 pub use를 더하면 내부 파일 구조와 외부 공개 API를 분리할 수 있습니다.
이 구분이 익숙해지면 컴파일 에러도 훨씬 읽기 쉬워집니다.
“모듈을 찾을 수 없다”면 mod 선언과 파일 위치를 확인하고, “이름을 찾을 수 없다”면 use 경로나 스코프를 확인하고, “비공개 항목”이라고 나오면 pub과 중간 모듈의 공개 여부를 확인하면 됩니다.
에러를 문법 전체의 실패로 보지 않고, 세 역할 중 어느 경계에서 막혔는지 좁혀갈 수 있는 거죠.
학습 순서는 Rust mod 키워드에서 파일과 모듈 선언을 먼저 잡고, Rust use 키워드에서 경로를 가져오는 방식을 익힌 뒤, Rust 모듈 공개 범위에서 pub, pub(crate), pub use를 정리하는 흐름이 가장 자연스럽습니다.
이 세 가지가 연결되면 Rust 프로젝트 구조가 훨씬 덜 막막하게 보일 거예요.
더 자세한 규칙은 Rust Reference의 items and modules 문서를 참고하세요.
This work is licensed under
CC BY 4.0