패키지, 크레이트와 모듈
이 포스트는 Rust를 처음 공부하면서 정리한 내용입니다. 해당 포스트는 개인적으로 기억하기 위한 메모 성격에 가깝습니다. “러스트 프로그래밍 공식 가이드 (2018, 제이펍)” 서적을 참고하였으며, 러스트 공식 사이트 에서 책과 동일한 내용을 찾을 수 있습니다. 따라서 더 자세한 내용을 찾으시면 위의 링크를 참고하시면 좋습니다.
오랫동안 C와 C++ 위주로 사용하다 보니 Rust가 가진 다양한 기능에 익숙해지는 것이 쉽지가 않습니다. 그 중 하나가 모듈화에 관련된 부분입니다. C++에서는 헤더 파일을 포함 방법 또는 클래스에서 가시성을 설정하는 등의 과정과는 많이 달라 다른 사람이 작성한 코드를 보는 것이 힘들더군요. 이 포스트에서는 cargo를 이용한 모듈화 관련 개념에 대해 정리해보려 합니다.
Rust의 모듈 시스템과 프로젝트 관리는 중요한 부분이므로 아래의 추가적인 자료를 참고하였습니다. (링크)
Cargo
먼저 크레이트와 모듈이라는 개념부터 정리하고 넘어가야 합니다. 크레이트(crate)는 다른 프로그래밍 언어에서 라이브러리 또는 패키지에 대응되는 개념입니다. cargo는 Rust에서 패키지 관리 툴(Package Management Tool)으로, 쉽게 바이너리나 라이브러리 프로젝트를 관리할 수 있게 해줍니다.
- 패키지(package): 크레이트를 빌드, 테스트, 공유할 수 있는 cargo의 기능
- 크레이트(crate): 라이브러리나 실행 파일을 생성하는 모듈의 트리
- 모듈(module)과 use: 코드의 구조와 범위, 그리고 경로의 접근성을 제어하는 기능
- 경로(path): 구조체, 함수, 혹은 모듈 등의 이름을 결정하는 방식
패키지와 크레이트
우선, 크레이트는 하나의 바이너리 혹은 라이브러리 입니다. 모든 크레이트는 트리 구조를 띄고 있는데, 암묵적으로 루트 모듈(root module)은 최상위 모듈이 됩니다.
$ cargo new phrases
$ cd phrases
cargo new 명령어를 통해 phrases 라는 모듈을 생성하면 아래와 같은 파일 구조를 확인할 수 있습니다. 이때 src/lib.rs이 crate root가 됩니다.
$ tree .
.
├── Cargo.toml
└── src
└── lib.rs
1 directory, 2 files
위의 명령을 실행하게 되면 Cargo.toml 파일을 생성합니다. 이 파일은 크레이트를 빌드하는 방법을 서술하는 사양서와 같은 역할을 합니다. Cargo.toml의 내용을 보면 src/main.rs 또는 src/lib.rs에 대해서는 언급되어 있지 않습니다. 이는 cargo는 패키지 디렉터리에 src/main.rs 가 있다면 이 패키지와 같은 이름의 바이너리 크레이트를 포함한다고 판단하고 자동적으로 src/main.src 파일을 크레이트 루트로 인식합니다. 마찬가지로 패키지 디렉터리에 src/lib.rs 가 있다면 이 패키지와 같은 이름의 라이브러리 크레이트를 포함한다고 판단하고 자동적으로 src/lib.src 파일을 크레이트 루트로 인식합니다. cargo는 라이브러리나 바이너리를 빌드할 때 rustc 컴파일러에게 크레이트 루트 파일을 전달합니다.
패키지는 하나 이상의 라이브러리 크레이트를 포함하거나 포함하지 않을 수 있고, 바이너리 크레이트는 최소한 하나는 포함해야 한다는 규칙이 존재합니다.
모듈 (Module)
모듈(module)은 크레이트의 코드를 그룹화하여 가독성과 재사용성을 향상시키는 방법입니다. 또한 외부 영역에서 이 코드를 사용할 수 있는지 범위(공개, pub)를 정하여 접근성을 결정하기도 합니다.
모듈에서 기억해야 할 중요 키워드는 pub
입니다.
모듈 정의하기
우선 english
모듈과 japanese
모듈을 결정하는 예제를 사용하겠습니다. src/lib.rs에 아래와 같이 작성합니다.
// src/lib.rs
mod english {
mod greetings {
fn bark() {}
}
mod farewells {
}
}
mod japanese {
mod greetings {
}
mod farewells {
}
}
Rust에서는 모듈을 정의할 때 lower snake case 네이밍 기법(Naming Conventions)을 사용합니다. 모듈은 위의 예시에서와 같이 mod 모듈명으로 모듈의 이름을 지정하고 중괄호로 모듈의 본문을 감싸고 있습니다. 모듈에는 구조체, 열거자, 상수, 트레이트(Trait), 함수, 그리고 하위 모듈(sub-modules)를 추가할 수 있습니다.
$ cargo build
이 예제에서는 메인 함수가 없고 파일 이름이 lib.rs 이므로 라이브러리로 빌드합니다.
파일 분리
위의 예제는 english와 japanese 모듈이 하나의 파일에 작성되어 있는 것을 나타냈습니다. 그러나 위의 예제를 아래와 같이 수정하면 다른 파일로 분리하여 모듈의 크기가 비약적으로 커지는 것을 방지할 수 있습니다.
// src/lib.rs
// 방법 1. 모든 파일안에 우겨넣기
mod english {
mod greetings {
fn bark() {}
}
mod farewells {
}
}
모듈을 src/lib.rs에 선언 후 별도의 파일로 분리하겠습니다.
// src/lib.rs
// 방법 2. 선언만 src/lib.rs에 하기
mod english;
이렇게 두면, Rust는 자동적으로 src/english.rs 파일이나 src/english/mod.rs 파일을 찾기 시작합니다. 이때 주의할 점은 lib.rs에 선언되어 있으므로 모듈을 다시 선언할 필요 없이 본문만 작성하면 된다는 점입니다. 즉,
// src/english.rs 또는 src/english/mod.rs 내용
// mod english 블록을 재작성 할 필요가 없다.
mod greetings {
fn bark() {}
}
mod farewells {
}
크레이트 예시
따라서 전체적인 프로젝트 파일의 구조는 아래와 같이 구성한다고 하면,
$ tree .
.
├── Cargo.lock
├── Cargo.toml
├── src
│ ├── english
│ │ ├── farewells.rs
│ │ ├── greetings.rs
│ │ └── mod.rs
│ ├── japanese
│ │ ├── farewells.rs
│ │ ├── greetings.rs
│ │ └── mod.rs
│ └── lib.rs
└── target
└── debug
├── build
├── deps
├── examples
├── libphrases-a7448e02a0468eaa.rlib
└── native
src/lib.rs
src/lib.rs는 모듈의 root crate가 되므로 아래와 같이 작성합니다.
mod english;
mod japanese;
src/english/mod.rs
그리고 각 english
와 japanese
는 하위 모듈을 가지고 있으므로 mod.rs에 아래와 같이 작성합니다.
mod farewells;
mod greetings;
접근성 제어에 관련된 내용은 다음 포스트에서 다루겠습니다.