모듈 스코프와 공개 범위
mod로 관련 코드를 모듈 트리에 묶고 기본 비공개 규칙을 바탕으로 아이템의 스코프와 공개 경계를 설계합니다.
이번에는 모듈, 아이템의 이름을 지정하는 경로(path), 스코프에 경로를 가져오는 use 키워드, 아이템을 공개하는 데 사용하는 pub 키워드를 알아보겠습니다.
as 키워드, 외부 패키지, 글롭(glob) 연산자 등도 다룰 예정입니다.
우선은 여러분이 미래에 코드를 구조화할 때 쉽게 참조할 수 있는 규칙을 나열하는 것으로 시작해보겠습니다.
그다음 각각의 규칙에 대한 세부 사항을 설명해보겠습니다.
모듈 치트 시트
아래 다이어그램은 mod 선언, 경로, pub 공개 범위, use 가져오기 규칙을 정리한 것입니다.
MODULE DECLARATION · SOURCE DISCOVERY
파일을 만든 것만으로는 모듈 트리가 생기지 않는다
각 크레이트는 하나의 루트 소스 파일에서 시작합니다. 부모의 mod 선언이 새 모듈을 트리에 넣고, 선언 형태가 본문을 인라인에서 읽을지 외부 파일에서 읽을지 결정합니다.
-
크레이트 루트에서 시작
rustc는 크레이트마다 하나의 루트 소스를 입력받습니다. Cargo 기본 배치의src/lib.rs와src/main.rs는 각각 별도 library·binary 크레이트의 루트이며, target의path로 바꿀 수 있습니다. -
부모가 모듈을 선언
mod garden { /* ... */ }또는mod garden;이 논리적인crate::garden모듈을 만듭니다. 디렉터리나 파일이 존재하는 것만으로는 모듈이 되지 않습니다. -
선언 형태에 따라 본문 탐색
중괄호가 있으면 본문은 같은 소스에 인라인으로 있습니다. 세미콜론 선언은 기본적으로
garden.rs또는garden/mod.rs중 한 외부 파일을 사용하며, 둘을 동시에 둘 수 없습니다. -
본문이 모듈 트리에 합류
선택된 본문의 아이템과 서브모듈이
crate::garden::…경로 아래에 놓입니다. 소스 파일 경로는 본문 탐색 규칙이고, 코드에서 쓰는 모듈 경로는 논리적인 이름 경로입니다.
| 선언과 모듈 본문 위치 | 논리 경로 |
|---|---|
mod garden { … } · 선언의 중괄호 안, 별도 파일 없음 |
crate::garden |
mod garden; · src/garden.rs 또는 src/garden/mod.rs |
crate::garden |
mod vegetables; · src/garden/vegetables.rs 또는 src/garden/vegetables/mod.rs |
crate::garden::vegetables |
mod garden { /* ... */ }- 본문은 선언의 중괄호 안에 있습니다.
- 논리 경로:
crate::garden mod garden;src/garden.rs또는src/garden/mod.rs중 하나를 사용합니다.- 논리 경로:
crate::garden mod vegetables;src/garden/vegetables.rs또는src/garden/vegetables/mod.rs중 하나를 사용합니다.- 논리 경로:
crate::garden::vegetables
#[path = "..."]는 외부 모듈 파일 위치를 바꿀 수 있습니다. 따라서 기본 파일 배치는 모듈 트리를 찾는 관례이지, 파일 시스템과 모듈 트리가 항상 같은 구조라는 뜻은 아닙니다.
현재 cargo new가 만드는 패키지의 기본 에디션은 Rust 2024입니다. 이 장의
crate:: 경로는 Rust 2018 이후 에디션의 현재 경로 문법입니다. foo.rs 옆에
foo/bar.rs를 두는 방식은 현재 권장되는 모듈 파일 배치이고, foo/mod.rs
형식도 여전히 대안으로 사용할 수 있습니다.
아래에 모듈, 경로, use, pub 키워드가 컴파일러에서 동작하는 방법과 대부분의 개발자가 코드를 구성하는 방법에 대한 빠른 참고 자료가 있습니다.
이 장을 거치면서 각각의 규칙에 대한 예제를 살펴볼 것이지만, 이곳이 모듈의 작동 방법을 기억하는 데에 참조할 좋은 위치가 되겠습니다.
- 크레이트 루트부터 시작:
rustc는 크레이트마다 하나의 루트 소스 파일에서 컴파일을 시작합니다. Cargo의 기본 파일 배치에서는 라이브러리 타깃의 루트가 src/lib.rs, 기본 바이너리 타깃의 루트가 src/main.rs입니다.[lib]이나[[bin]]의path를 지정하면 다른 파일을 루트로 사용할 수도 있습니다. - 모듈 선언: 크레이트 루트 파일에는 새로운 모듈을 선언할 수 있습니다;
mod garden { /* ... */ }은 그 자리에서 인라인 모듈을 정의하고,mod garden;은 외부 파일에서 모듈 본문을 불러옵니다. 기본 파일 탐색 규칙에서는 src/garden.rs와 src/garden/mod.rs 중 하나를 사용할 수 있으며, 같은 모듈에 두 파일을 함께 둘 수는 없습니다. - 서브모듈 선언: 크레이트 루트가 아닌 다른 파일에서는 서브모듈(submodule)을
선언할 수 있습니다. 예를 들면 src/garden.rs 안에
mod vegetables;를 선언할 수도 있습니다. 이 외부 모듈 본문은 기본적으로 src/garden/vegetables.rs와 src/garden/vegetables/mod.rs 중 하나에 둡니다. 또는mod vegetables { /* ... */ }처럼 부모 모듈 안에 인라인으로 정의할 수 있습니다. - 모듈 내 코드로의 경로: 일단 모듈이 크레이트의 일부로서 구성되면, 공개 규칙이
허용하는 한도 내에서라면 해당 코드의 경로를 사용하여 동일한 크레이트의 어디에서든
이 모듈의 코드를 참조할 수 있게 됩니다. 예를 들면, garden vegetables 모듈 안에
있는
Asparagus타입은crate::garden::vegetables::Asparagus로 찾아 쓸 수 있습니다. - 비공개 vs 공개: 아이템은 기본적으로 부모 모듈에게 비공개(private)이며,
아이템을 정의한 모듈과 그 자손 모듈에서는 사용할 수 있습니다.
pub mod는 모듈 이름의 가시성만 넓히고 내부 아이템을 자동으로 공개하지 않습니다. 외부 크레이트가 원래 경로로 아이템을 사용하려면 경로의 조상 모듈과 최종 아이템이 모두 공개되어야 합니다. use키워드: 어떤 스코프 내에서use키워드는 긴 경로의 반복을 줄이기 위한 이름 바인딩을 만듭니다.crate::garden::vegetables::Asparagus를 참조할 수 있는 모든 스코프에서use crate::garden::vegetables::Asparagus;로 단축경로를 만들 수 있으며, 그 이후부터는 스코프에서 이 타입을 사용하려면Asparagus만 작성해주면 됩니다.use는 모듈을 크레이트에 추가하지 않으며,pub use는 이 바인딩을 공개하여 원래 내부 구조와 다른 재공개 경로를 만들 수 있습니다.
이 규칙들은 파일 위치, 모듈 선언, 공개 여부가 서로 맞물릴 때 제대로 작동합니다.
다만 파일 시스템은 모듈 본문을 찾는 기본 규칙일 뿐, 모듈 트리 그 자체는 아닙니다.
인라인 모듈은 별도 파일이 없고, 외부 파일도 mod 선언이 있어야 트리에 들어옵니다.
위의 규칙들을 보여주는 backyard라는 이름의 바이너리 크레이트를 만들어 보았습니다.
디렉터리명 또한 backyard로서, 아래의 파일들과 디렉터리들로 구성되어 있습니다.
지금의 경우 크레이트 루트 파일은 src/main.rs이고, 내용은 아래와 같습니다.
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
mod garden; 형태의 선언은 컴파일러에게 외부 파일에서 garden 모듈 본문을
불러오라고 지시합니다. 여기서는 pub mod garden;이므로 모듈 이름도 공개됩니다.
그러나 이 pub이 src/garden.rs 안의 아이템까지 자동으로 공개하지는 않습니다.
src/garden.rs는 아래와 같습니다.
pub mod vegetables;
여기 pub mod vegetables;은 외부 파일에서 vegetables 모듈 본문을 불러오고
모듈 이름을 공개합니다. 기본 탐색 규칙에 따라 이 예제에서는
src/garden/vegetables.rs를 사용합니다.
해당 파일의 코드는 아래와 같습니다.
#[derive(Debug)]
pub struct Asparagus {}
이제 위 규칙들의 세부 사항으로 넘어가서 실제로 해보면서 확인합시다!
모듈로 관련된 코드 묶기
모듈은 크레이트의 코드를 읽기 쉽고 재사용하기도 쉽게끔 구조화를 할 수 있게 해 줍니다.
모듈 내의 코드는 기본적으로 비공개이므로, 모듈은 아이템의 공개 여부(privacy) 를 제어하도록 해주기도 합니다.
비공개 아이템은 외부에서의 사용이 허용되지 않는 내부의 세부 구현입니다.
모듈과 모듈 내 아이템을 선택적으로 공개할 수 있는데, 이렇게 하여 외부의 코드가 모듈 및 아이템을 의존하고 사용할 수 있도록 노출해 줍니다.
예시로, 레스토랑 기능을 제공하는 라이브러리 크레이트를 작성한다고 가정해 보죠.
코드 구조에 집중할 수 있도록 레스토랑을 실제 코드로 구현하지는 않고, 본문은 비워둔 함수 시그니처만 정의하겠습니다.
레스토랑 업계에서는 레스토랑을 크게 접객 부서(front of house) 와 지원 부서(back of house) 로 나눕니다.
접객 부서는 호스트가 고객을 안내하고, 웨이터가 주문 접수 및 결제를 담당하고, 바텐더가 음료를 만들어 주는 곳입니다.
지원 부서는 셰프, 요리사, 주방보조가 일하는 주방과 매니저가 행정 업무를 하는 곳입니다.
중첩(nested) 모듈 안에 함수를 집어넣어 구성하면 크레이트 구조를 실제 레스토랑이 일하는 방식과 동일하게 구성할 수 있습니다.
cargo new --lib restaurant 명령어를 실행하여 restaurant이라는 새 라이브러리를 생성하고, 예제 6-1 코드를 src/lib.rs에 작성하여 모듈, 함수 시그니처를 정의합시다.
아래는 접객 부서 쪽 코드입니다.
예제 6-1: 함수를 포함하는 별도의 모듈을 포함한front_of_house 모듈
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
fn seat_at_table() {}
}
mod serving {
fn take_order() {}
fn serve_order() {}
fn take_payment() {}
}
}
mod 키워드와 모듈 이름(위의 경우 front_of_house)을 지정하여 모듈을 정의합니다.
모듈의 본문은 중괄호로 감싸져 있습니다.
hosting, serving 모듈처럼, 모듈 내에는 다른 모듈을 넣을 수 있습니다.
모듈에는 구조체, 열거형, 상수, 트레이트, 함수(예제 6-1처럼) 등의 아이템 정의 또한 가질 수 있습니다.
모듈을 사용함으로써 관련된 정의들을 하나로 묶고 어떤 연관성이 있는지 이름을 지어줄 수 있습니다.
모듈화된 코드를 사용하는 프로그래머가 자신에게 필요한 어떠한 정의를 찾을 때, 모든 정의를 읽어 내릴 필요 없이 그룹 기반으로 탐색할 수 있으므로 훨씬 쉽게 찾아낼 수 있죠.
코드에 새로운 기능을 추가하려는 프로그래머도 자신이 어디에 코드를 작성해야 프로그램 구조가 그대로 유지되는지 파악할 수 있습니다.
Cargo의 기본 배치에서 src/main.rs와 src/lib.rs는 각각 바이너리 타깃과 라이브러리 타깃의 크레이트 루트입니다. 두 파일이 함께 있다면 하나의 루트를 공유하는 것이 아니라 서로 다른 두 크레이트가 됩니다.
각 크레이트 루트는 모듈 트리(module tree) 라고 불리는 논리 구조의 최상위
모듈을 형성합니다. 현재 크레이트 안의 절대 경로에서는 이 루트를 crate로
가리킵니다.
예제 6-2는 예제 6-1의 구조를 모듈 트리로 나타낸 모습입니다.
예제 6-2: 예제 6-1 코드를 모듈 트리로 나타낸 모습트리는 모듈이 서로 어떻게 중첩되어 있는지 보여줍니다; 예를 들어 hosting 모듈은 front_of_house 내에 위치합니다.
이 트리는 또한 어떤 모듈이 서로 형제(sibling) 관계에 있는지 나타내기도 하는데, 이는 동일한 모듈 내에 정의되어 있음을 말합니다; hosting과 serving은 front_of_house 모듈 내에 정의된 형제입니다.
모듈 A가 모듈 B 안에 있으면, 모듈 A는 모듈 B의 자식이며, 모듈 B는 모듈 A의 부모라고 말합니다.
전체 모듈 트리 최상위에 crate라는 모듈이 암묵적으로 위치한다는 점을 기억해 두세요.
모듈 트리는 디렉터리 트리처럼 부모·자식 관계를 갖지만 둘은 동일한 구조가
아닙니다. 한 파일에 여러 인라인 모듈을 둘 수도 있고, 외부 모듈 파일의 위치는
기본 탐색 규칙이나 #[path] 속성으로 정할 수 있습니다.
다음 표는 호출 위치가 같은 크레이트 안인지 외부 크레이트인지에 따라 privacy를
판단하는 방법과 use, pub use의 역할을 함께 정리합니다.
PRIVACY · NAME BINDING
호출 위치와 공개 경로를 나눠야 접근 가능성을 정확히 판단한다
private 아이템은 정의한 모듈과 그 자손이 사용할 수 있습니다. 외부 크레이트에서 원래 경로로 접근하려면 조상 모듈과 최종 아이템이 모두 공개되어야 하며, 공개 재공개 경로는 예외가 됩니다.
같은 크레이트 안
private 아이템은 그 아이템을 정의한 모듈과 자손 모듈에서 접근할 수 있습니다. 부모나 다른 형제 모듈에서는 해당 private 자식 아이템을 직접 사용할 수 없습니다.
외부 크레이트에서
원래 경로를 쓰려면 경로의 모든 조상 모듈과 최종 아이템이 공개되어야 합니다. pub use가 만든 공개 경로를 사용하면 private 내부 모듈을 API에 노출하지 않을 수 있습니다.
| 선언 | 무엇을 허용하는가 | 놓치기 쉬운 경계 |
|---|---|---|
| 기본 private | 정의 모듈과 그 자손에서 접근 | 부모·형제에게 자동 공개되지 않음 |
pub mod api |
호출 위치에서 조상 경로가 허용하는 범위까지 모듈 이름 공개 | 모듈 안의 함수·타입·필드는 각자 visibility를 유지 |
pub fn run |
접근 가능한 조상 경로 또는 재공개 경로를 통해 함수 공개 | 최종 아이템의 pub만으로 private 조상 경로를 통과하지 못함 |
pub (crate) |
최대 현재 크레이트 범위까지 함수 공개 | 호출 위치에서 함수까지 이어지는 조상 경로도 접근 가능해야 함 |
use 바인딩 |
privacy를 확인한 뒤 현재 스코프에 이름 바인딩 생성 | 모듈을 선언하거나 소스 파일을 불러오는 기능이 아님 |
pub use |
바인딩을 공개해 새 공개 API 경로 생성 | 내부 모듈의 원래 경로와 외부 사용 경로가 달라질 수 있음 |
- 기본 private
- 정의 모듈과 그 자손에서 접근합니다.
- 부모·형제에게 자동 공개되지 않습니다.
pub mod api- 모듈 이름의 visibility만 넓힙니다.
- 내부 함수·타입·필드는 각자 visibility를 유지합니다.
pub fn run- 접근 가능한 조상 경로나 재공개 경로를 통해 함수를 공개합니다.
- 최종 함수만
pub로 두어 private 조상을 통과할 수는 없습니다. pub(crate)- 최대 현재 크레이트 범위까지 함수를 공개합니다.
- 호출 위치에서 조상 경로도 접근 가능해야 합니다.
use바인딩- privacy를 확인하고 현재 스코프에 이름을 바인딩합니다.
- 모듈 선언이나 파일 로딩은 하지 않습니다.
pub use재공개- 바인딩을 공개해 새 공개 API 경로를 만듭니다.
- private 내부 구조와 외부 사용 경로를 분리할 수 있습니다.
오류는 경로 기준점과 철자 → 부모의 mod 선언과 소스 위치 → 호출 위치에서 조상·최종 아이템의 visibility → use 바인딩 또는 pub use 재공개 순서로 확인합니다.