본문 바로가기

파일 기반 모듈 분리

커지는 모듈을 별도 파일과 디렉터리로 분리하면서 mod 선언과 모듈 트리 경로가 유지되는 방식을 익힙니다.

지금까지 6장의 예제는 파일 하나에 모듈을 여럿 정의했습니다. 모듈이 커지면 정의를 다른 파일로 옮겨 코드를 찾기 쉽게 하고 싶어집니다. 이 글에서는 use 키워드로 경로를 스코프 안으로 가져오기의 마지막 레스토랑 코드에서 시작해, 크레이트 루트 파일에 모두 정의했던 모듈을 파일로 하나씩 옮겨 봅니다. 여기서는 크레이트 루트가 src/lib.rs이지만, 크레이트 루트가 src/main.rs인 바이너리 크레이트에서도 똑같이 할 수 있습니다.

모듈을 파일로 옮기기

먼저 front_of_house 모듈을 파일로 옮깁니다. front_of_house 모듈의 중괄호 안의 코드를 지우고 mod front_of_house; 선언만 남겨, src/lib.rs를 다음과 같이 만듭니다. 이 상태로는 src/front_of_house.rs를 만들기 전까지 컴파일되지 않습니다.

src/lib.rs
mod front_of_house;

pub use crate::front_of_house::hosting;

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

그다음 중괄호 안에 있던 코드를 새 파일 src/front_of_house.rs로 옮깁니다. 크레이트 루트에서 front_of_house라는 모듈 선언을 만났으므로, 컴파일러는 이 파일에서 모듈의 본문을 찾습니다.

src/front_of_house.rs
pub mod hosting {
    pub fn add_to_waitlist() {}
}

mod 선언으로 파일을 불러오는 일은 모듈 트리에서 한 번만 하면 됩니다. 그 파일이 프로젝트의 일부라는 것과 모듈 트리의 어디에 있는지를 컴파일러가 알게 되면, 다른 파일에서는 모듈 경로와 아이템 참조에서 본 것처럼 그 모듈이 선언된 곳의 경로로 그 안의 코드를 가리킵니다. 곧 mod는 다른 언어에서 보는 파일 포함(include) 연산이 아닙니다.

다음으로 hosting 모듈을 파일로 옮깁니다. hosting은 루트 모듈이 아니라 front_of_house의 자식이므로 과정이 조금 다릅니다. hosting의 파일은 모듈 트리에서 그 조상의 이름을 딴 새 폴더, 여기서는 src/front_of_house 폴더에 둡니다. 먼저 src/front_of_house.rs에는 hosting 모듈의 선언만 남깁니다.

src/front_of_house.rs
pub mod hosting;

그리고 src/front_of_house 폴더를 만들고, 그 안의 hosting.rs 파일에 hosting 모듈의 본문을 적습니다. 파일 자체가 모듈의 중괄호 역할을 하므로, 파일에는 mod hosting과 중괄호 없이 그 안에 있던 내용만 옮깁니다.

src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}

hosting.rs를 src 폴더에 바로 두면, 컴파일러는 그 코드를 크레이트 루트에 선언된 hosting 모듈로 여기고 front_of_house의 자식으로 보지 않습니다. 지금의 mod hosting; 선언은 front_of_house 안에 있으므로 컴파일러는 src/front_of_house 폴더에서 찾습니다. 어떤 모듈의 코드를 어느 파일에서 찾을지 정하는 컴파일러의 규칙 덕분에 폴더와 파일의 구조가 모듈 트리와 비슷하게 맞춰집니다.

대체 파일 경로

지금까지는 러스트 컴파일러가 쓰는 가장 관례적인 파일 경로를 보았지만, 러스트는 예전 방식의 파일 경로도 지원합니다. 크레이트 루트에 선언된 front_of_house 모듈의 본문은 다음 두 곳에서 찾습니다.

모듈지금 방식예전 방식
front_of_housesrc/front_of_house.rssrc/front_of_house/mod.rs
front_of_house의 자식 hostingsrc/front_of_house/hosting.rssrc/front_of_house/hosting/mod.rs

같은 모듈에 두 방식을 함께 쓰면 컴파일 오류가 납니다. 한 프로젝트에서 모듈마다 다른 방식을 섞어 쓰는 것은 허용되지만, 프로젝트를 읽는 사람이 헷갈릴 수 있습니다. mod.rs 방식의 가장 큰 단점은 프로젝트에 mod.rs라는 이름의 파일이 많아져, 편집기에서 여러 개를 함께 열었을 때 구분하기 어렵다는 것입니다. 기본 경로 대신 다른 파일을 쓰려면 #[path = "..."] 속성으로 직접 지정할 수도 있습니다.

파일을 나눠도 같은 모듈 트리

이제 각 모듈의 코드를 다른 파일로 옮겼지만 모듈 트리는 그대로입니다. 아래 그림처럼 한 파일에 모두 있던 때와 파일을 나눈 뒤의 모듈 트리는 crate에서 front_of_house, hosting, add_to_waitlist로 이어지는 같은 모양이며, 달라진 것은 각 모듈의 본문이 놓인 파일뿐입니다.

mod 선언이 논리 모듈 트리와 파일 탐색을 연결하고 use와 pub use가 각각 스코프 이름과 공개 API를 설계하는 경계를 정리한 표

MOD TREE · FILE LOOKUP · PUBLIC API

mod가 트리를 만들고, 파일은 그 모듈의 본문을 담는다

모듈의 정체성은 선언 위치가 정하고 파일 위치는 관용적인 본문 탐색 규칙을 따릅니다. use는 이름을 바인딩하고 pub use는 공개 경로를 설계하므로 파일 분리와 공개 API 변경은 별개입니다.

파일을 나눠도 모듈의 부모·자식 관계는 같다서로독립인두4노드3간선모듈트리를비교한다. 한파일의crate→front_of_house→hosting→add_to_waitlist와외부파일분리후동일이름의같은트리를보여준다. 노드소스파일라벨만달라지며두트리사이의실행간선은없다. 이그림은논리모듈트리이며파일디렉터리트리나pubuse의공개경로트리가아니다. 재공개APIrestaurant::hosting은아래규칙표로따로읽는다. 모바일도8노드6소속관계를유지한다.한 파일 안cratesrc/lib.rsfront_of_housesrc/lib.rshostingsrc/lib.rsadd_to_waitlistsrc/lib.rs파일 분리 후cratesrc/lib.rsfront_of_housesrc/front_of_house.rshostingsrc/front_of_house/hosting.rsadd_to_waitlist같은 hosting.rs
파일을 나눠도 모듈의 부모·자식 관계는 같다서로독립인두4노드3간선모듈트리를비교한다. 한파일의crate→front_of_house→hosting→add_to_waitlist와외부파일분리후동일이름의같은트리를보여준다. 노드소스파일라벨만달라지며두트리사이의실행간선은없다. 이그림은논리모듈트리이며파일디렉터리트리나pubuse의공개경로트리가아니다. 재공개APIrestaurant::hosting은아래규칙표로따로읽는다. 모바일도8노드6소속관계를유지한다.한 파일 안cratesrc/lib.rsfront_of_housesrc/lib.rshostingsrc/lib.rsadd_to_waitlistsrc/lib.rs파일 분리 후cratesrc/lib.rsfront_of_housesrc/front_of_house.rshostingsrc/front_of_house/hosting.rsadd_to_waitlist같은 hosting.rs

1 · 부모가 자식 모듈을 선언

// src/lib.rs
mod front_of_house;

크레이트 루트의 선언이 crate::front_of_house 노드를 만듭니다. 파일을 디렉터리에 놓는 것만으로 모듈이 생기지는 않습니다.

2 · 그 모듈이 다시 자식을 선언

// src/front_of_house.rs
pub mod hosting;

선언 위치 때문에 hosting은 루트가 아니라 front_of_house의 자식입니다. pub는 이름의 가시성을 정할 뿐 파일 탐색 규칙을 바꾸지 않습니다.

선언에서 모듈 트리, 본문 파일, 스코프 이름, 공개 API로 이어지는 계약
구문과 위치 만드는 계약과 지켜야 할 경계
루트의 부모 모듈 선언
mod front_of_house;

crate::front_of_house를 선언하고 관용적으로 src/front_of_house.rs 또는 예전 형식인 src/front_of_house/mod.rs에서 본문을 찾습니다. 같은 모듈에 두 파일을 함께 두면 오류입니다.

부모 안의 자식 모듈 선언
mod hosting;

hosting을 부모의 자식으로 선언하고 src/front_of_house/hosting.rs 또는 src/front_of_house/hosting/mod.rs를 찾습니다. src/hosting.rs로 옮기기만 해서는 루트 모듈이 되지 않습니다.

사용자 지정 파일 경로
// src/lib.rs
#[path = "front.rs"]
mod front_of_house;

이 예제는 src/front.rs를 찾습니다. 인라인 모듈 밖에서는 선언 소스의 디렉터리가 기준입니다. 인라인 모듈 안에서는 인라인 경로가 붙고, lib.rs·main.rs·mod.rs가 아닌 소스라면 그 파일의 모듈 이름도 디렉터리로 붙습니다. 파일 경로를 바꿔도 논리 모듈 이름과 트리 위치는 선언이 정합니다.

현재 스코프의 이름 바인딩
use module::item;

이미 선언된 아이템을 현재 스코프의 짧은 이름으로 바인딩합니다. 어떤 파일이 크레이트에 포함되는지 결정하거나 새 모듈을 선언하지 않습니다.

공개 이름 재내보내기
pub use module::item;

pub use가 놓인 모듈까지 접근 가능한 코드에 새 경로를 제공합니다. 공개 아이템의 비공개 정의 경로는 이 새 경로로 우회할 수 있지만, 대상 자체의 가시성보다 더 넓게 다시 내보낼 수는 없습니다.

  1. 루트 선언이 부모 모듈을 만든다

    mod front_of_house;는 논리 노드를 만들고 현대식 front_of_house.rs 또는 예전식 front_of_house/mod.rs에서 본문을 찾습니다. 둘을 함께 둘 수는 없습니다.

  2. 부모 선언이 자식 경로를 정한다

    부모 안의 mod hosting; 때문에 본문 후보는 front_of_house/hosting.rs 또는 front_of_house/hosting/mod.rs입니다. 파일 이동만으로 루트 모듈이 되지 않습니다.

  3. 파일 탐색 경로를 명시적으로 바꾼다

    src/lib.rs의 #[path = "front.rs"]는 src/front.rs를 찾습니다. 인라인 모듈 밖에서는 선언 소스의 디렉터리가 기준이며, 인라인 모듈 안에서는 인라인 경로와 소스 파일 형식에 따른 경로가 더 붙습니다. 논리 트리는 여전히 mod 선언이 정합니다.

  4. use는 현재 스코프의 이름이다

    이미 선언된 아이템을 짧게 부를 바인딩이며 파일을 로드하거나 새 모듈을 만들지 않습니다.

  5. pub use는 사용자가 볼 경로다

    pub use가 놓인 모듈까지 접근 가능한 코드에 새 경로를 제공합니다. 공개 아이템의 비공개 정의 경로를 우회할 수 있지만 대상 자체의 가시성보다 더 넓게 다시 내보내지는 못합니다.

점검 순서: 선언한 부모 스코프 → 논리 모듈 경로 → 관용 파일 후보 또는 #[path] → 아이템 가시성 → use와 pub use의 사용자 경로. 일반 경로는 대상까지의 모든 조상 모듈이 접근 가능해야 하며, 비공개 아이템은 정의 모듈과 그 자손에서 접근할 수 있습니다. 파일 분리만을 이유로 모두 pub로 만들 필요는 없습니다.

그래서 정의가 다른 파일에 있어도 eat_at_restaurant 안의 함수 호출은 고치지 않고 그대로 동작합니다. 이 방법으로 모듈이 커질 때마다 새 파일로 옮길 수 있습니다. src/lib.rs의 pub use crate::front_of_house::hosting 문장도 바꾸지 않았다는 점에 주목합니다. 본문 없는 mod는 모듈을 선언하고 관례적인 경로에서 본문을 찾는 반면, use는 이미 선언된 아이템의 이름을 지금의 스코프에 묶고, pub use는 그 이름을 새 공개 경로로 내보낼 뿐입니다. use는 크레이트의 일부로 컴파일될 파일에 아무 영향도 주지 않습니다.

세 파일 src/lib.rs, src/front_of_house.rs, src/front_of_house/hosting.rs를 저장하고 cargo check를 실행하면, 컴파일러는 mod 선언을 따라 두 파일을 읽어 같은 모듈 트리를 만듭니다. 호출까지 확인하려면 같은 패키지에 다음 src/main.rs를 더합니다.

src/main.rs
fn main() {
    println!("before");
    restaurant::eat_at_restaurant();
    restaurant::hosting::add_to_waitlist();
    println!("after");
}
before
after

add_to_waitlist의 본문은 비어 있으므로, 호출을 둘러싼 before와 after 두 줄만 출력됩니다. 바이너리는 바깥 크레이트이므로 pub use가 만든 restaurant::hosting 경로로 함수를 부릅니다. 모듈의 소스 파일을 찾는 규칙은 Reference의 모듈 소스 파일에 정리되어 있습니다.

모듈 요약

러스트에서는 패키지를 여러 크레이트로, 크레이트를 여러 모듈로 나누고, 한 모듈에 정의한 아이템을 절대 경로나 상대 경로로 다른 모듈에서 가리킬 수 있습니다. 이런 경로를 use로 스코프에 가져오면, 그 스코프에서 아이템을 여러 번 쓸 때 짧은 이름을 쓸 수 있습니다. 모듈의 코드는 기본으로 비공개이지만, pub을 붙여 정의를 공개할 수 있습니다.

정리

  • mod 이름;만 남기고 본문을 이름.rs로 옮기면, 컴파일러가 그 파일에서 모듈의 본문을 찾습니다.
  • 자식 모듈의 파일은 부모 모듈의 이름을 딴 폴더에 두며, src/front_of_house/hosting.rs처럼 모듈 트리를 따라 놓입니다.
  • mod는 파일을 포함하는 연산이 아니며, 한 모듈은 트리에서 한 번만 선언합니다.
  • 예전 방식의 mod.rs도 지원하지만 같은 모듈에 두 방식을 함께 쓸 수는 없고, #[path]로 경로를 직접 정할 수도 있습니다.
  • 파일을 나눠도 모듈 트리와 경로는 그대로이므로 호출 코드와 use, pub use를 고칠 필요가 없습니다.

연습 문제

1. 파일 문제인가 공개 문제인가

세 파일 배치로 되돌린 뒤 다음 변경을 하나씩 적용합니다. 각각 어떻게 되나요? (가) hosting.rs를 src/hosting.rs로 옮기고 선언은 그대로 둔다. (나) src/front_of_house.rs를 그대로 두고 src/front_of_house/mod.rs도 만든다. (다) src/lib.rs의 pub use crate::front_of_house::hosting;을 use crate::front_of_house::hosting;으로 바꾼다.

해설 보기

(가)는 파일을 찾는 문제입니다. front_of_house 안의 mod hosting;은 여전히 src/front_of_house/hosting.rs나 src/front_of_house/hosting/mod.rs를 찾으므로, 그 파일이 없다는 오류가 나고 src/hosting.rs는 읽지 않습니다. (나)도 파일 문제로, 같은 front_of_house 모듈의 본문이 두 곳에 있으므로 컴파일러가 어느 쪽을 쓸지 몰라 오류를 냅니다. (다)는 공개 경로의 문제입니다. 라이브러리 안의 eat_at_restaurant는 루트 스코프의 hosting을 그대로 쓸 수 있어 라이브러리는 컴파일되지만, 바이너리의 restaurant::hosting::add_to_waitlist()는 hosting이 더 이상 공개되지 않으므로 비공개라는 E0603 오류가 납니다.

2. 새 모듈을 파일로 만들기

front_of_house에 serving 모듈을 더해 그 안에 pub fn take_order() {}를 두려 합니다. 지금의 파일 배치를 따른다면 무엇을 어디에 적어야 하나요?

해설 보기

부모인 src/front_of_house.rs에 pub mod serving; 선언을 더하고, 본문은 src/front_of_house/serving.rs에 pub fn take_order() {}로 적습니다. 파일만 만들고 선언을 빠뜨리면 serving은 모듈 트리에 들어오지 않으며, 바깥에서 쓰려면 pub use로 공개 경로를 만들거나 조상 모듈을 모두 공개해야 합니다.

이것으로 6장을 마칩니다. 다음 장의 벡터와 값 목록에서는 이렇게 정리한 코드에서 쓸 표준 라이브러리의 컬렉션 자료 구조를 살펴봅니다.