이 시리즈는 Rust 공식문서를 통해 공부한 흔적임을 밝힙니다.
지금까지 우리는 하나의 파일에 하나의 모듈로 작성했다.
모듈이 뭐지, 싶을 수도 있지만 그것을 배우지 않았다는 점에서 우린 모듈을 나눈 적 없다는 건 알 것이다.
아무튼 이렇게 하나로 모든 것을 구현하는 방식은 지금처럼 작은 프로젝트에서는 문제 없지만
프로젝트가 크고 복잡해질수록 이해하기 어렵고 추적하기 힘들어 문제를 야기할 수도 있다.
프로젝트를 여러 개의 파일과 모듈로 나누어 관리하면 보다 효율적으로 관리할 수 있다.
하나의 패키지는 여러 개의 바이너리 크레이트를 포함할 수 있으며
필요에 따라 하나의 라이브러리 크레이트를 포함할 수 있다.
그리고 어떤 부분은 패키지 내에 구현하는 게 아니라 외부의 의존 패키지로 추가할 수도 있다.
이와 같은 패키지 관리에 필요한 개념들을 알아보도록 하겠다.
우리가 앞으로 배울 모듈 시스템에는 다음과 같은 개념들이 포함된다.
- 패키지 Packages
크레이트를 빌드, 테스트, 공유할 수 있는 Cargo의 기능
A Cargo feature that lets you build, test, and share crates- 크레이트 Crates
라이브러리나 실행파일을 생성하는 모듈의 트리
A tree of modules that produces a library or executable- 모듈 Modules과
use
구조, 범위, 경로의 접근성을 제어하는 것
Let you control the organization, scope, and privacy of paths- 경로 Paths
구조체, 함수, 모듈 등의 이름을 결정하는 방식
A way of naming an item, such as a struct, function, or module
패키지는 하나 이상의 크레이트와 그것을 빌드하는 방법을 서술하는 Cargo.toml
파일로 이루어져 있다.
크레이트는 하나의 바이너리 혹은 라이브러리로, 모듈로 이루어져 있다.
Rust 컴파일러가 컴파일을 시작하는 소스 파일을 크레이트 루트라고 부른다.
그 구성을 알아보기 위해 cargo new
명령어를 통해 새 프로젝트,
그러니까 새 패키지를 생성해보자.
peter@hp-laptop:~/rust-practice$ mkdir chapter07
peter@hp-laptop:~/rust-practice$ cd chapter07
peter@hp-laptop:~/rust-practice/chapter07$ cargo new my-project
Created binary (application) `my-project` package
peter@hp-laptop:~/rust-practice/chapter07$ tree my-project/
my-project/
├── Cargo.toml
└── src
└── main.rs
1 directory, 2 files
peter@hp-laptop:~/rust-practice/chapter07$
src/main.rs
이 패키지와 같은 이름을 갖는 바이너리 크레이트의 크레이트 루트라는 것이 약속되어 있어
따로 명시해주지 않고 src/main.rs
가 크레이트 루트로 사용된다.
마찬가지로 라이브러리 크레이트의 경우 src/lib.rs
가 존재한다면
그것이 패키지와 같은 이름을 갖는 라이브러리 크레이트의 크레이트 루트가 된다.
여러 개의 바이너리 크레이트를 포함하고 싶다면 src/bin
디렉토리를 만들어
그 디렉토리 안에 작성하면 각 파일은 별개의 바이너리 크레이트가 된다.
기능을 크레이트 단위로 관리하면 필요한 기능 및 그것과 연관된 기능을 원활하게 가져다 사용할 수 있다.
그리고 이름이 겹치는 구조체, 함수 등이 존재하더라도 크레이트를 통해 구분할 수 있다.
크레이트의 코드는 모듈 단위로 그룹화된다.
이로서 가독성과 재사용성을 높일 수 있고, 접근성을 제어할 수 있다.
모듈에 대한 이해를 돕기 위해 레스토랑 패키지를 생성하여 예제를 작성해보자.
이것은 기능적인 것보다는 구조적인 부분에 집중할 것이다.
cargo new
명령어는 기본적으로 바이너리 크레이트를 하나 가진 패키지를 생성하는데
라이브러리 크레이트를 하나 가진 패키지를 생성하려면 --lib
옵션을 추가하면 된다.
자동으로 생성되는 녀석에 대해서는 나중에 다루게 되겠지만 일단 지우고 예제를 작성하자.
peter@hp-laptop:~/rust-practice/chapter07$ cargo new --lib restaurant
Created library `restaurant` package
peter@hp-laptop:~/rust-practice/chapter07$ cd restaurant/
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
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
키워드로 시작되는 부분이 모듈이며 모듈 안에 다른 모듈이 정의될 수 있다.
모듈 안에는 구조체, 열거형, 상수, 트레이트 함수 등이 들어갈 수 있다.
이것을 통해 코드를 그룹화하면 그 이름으로 그것이 무엇에 쓰이는 녀석인지 알 수 있어
이해하기도 쉽고 필요한 코드를 찾기도 쉬우며
새로운 코드를 추가할 때도 적절한 위치를 파악할 수 있다.
크레이트는 모듈로 구성되어 있다고 했는데, 그 구조를 모듈 트리라고 한다.
모듈 트리의 루트는 crate
라는 이름의 모듈이며
크레이트 루트인 src/main.rs
또는 src/lib.rs
가 이에 해당한다.
우리가 작성한 restaurant
크레이트의 모듈 트리는 개념적으로 다음과 같이 구성된다.
restaurant
크레이트의 모듈 트리crate └── front_of_house ├── hosting │ ├── add_to_waitlist │ └── seat_at_table └── serving ├── table_order ├── serve_order └── take_payment
그리고 이러한 모듈 트리의 구조는 모듈의 경로를 찾을 때도 이용된다.
경로는 절대 경로와 상대 경로로 구분할 수 있다.
- 절대 경로
크레이트 루트로부터 시작하여 크레이트 이름이나crate
를 이용하여 나타낸 경로
An absolute path starts from a crate root by using a crate name or a literal crate.- 상대 경로
현재 모델로부터 시작하여self
,super
, 또는 현재 모듈의 식별자를 이용하여 나타낸 경로
A relative path starts from the current module and uses self, super, or an identifier in the current module.
우리가 작성한 모듈 아래에 새 함수를 만들고
모듈 내 함수를 절대 경로 및 상대 경로를 통해 호출하는 코드를 작성해보자.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
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() {} } } pub fn eat_at_restaurant() { crate::front_of_house::hosting::add_to_waitlist(); front_of_house::hosting::add_to_waitlist(); }
eat_at_restaurant
함수의 두 함수 호출은 동일한 함수를 호출하는 것이며
첫번째 것이 절대 경로, 두번째 것이 상대 경로로 나타낸 것이다.
둘 다 add_to_waitlist
에 도달하기까지의 경로를 ::
를 통해 작성했다.
절대 경로를 사용할지 상대 경로를 사용할지는 프로젝트를 관리하기 나름이다.
공식 문서를 작성한 필자들은 절대 경로를 선호한다는 건 여담.
pub
키워드그런데 방금 작성한 코드를 cargo build
로 컴파일하면 오류를 만나게 된다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ cargo build
Compiling restaurant v0.1.0 (/home/peter/rust-practice/chapter07/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:18:28
|
18 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error[E0603]: module `hosting` is private
--> src/lib.rs:20:21
|
20 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
error: aborting due to 2 previous errors
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant`.
To learn more, run the command again with --verbose.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$
hosting
모듈이 private인데 우리가 외부에서 접근하려고 했다는 것이다.
모듈은 코드의 효율적인 관리만을 위한 게 아니고 캡슐화의 기능도 있다.
그리고 Rust는 변수를 기본적으로 불변성을 띄게 만드는 것과 같이
모듈도 따로 명시하지 않는다면 private으로 정의된다.
모듈 내부의 구조체, 함수 등 모든 것이 그렇다.
위 예제에서 front_of_house
는 eat_at_restaurant
와 같은 범위에 있어 접근 가능하지만
front_of_house
내부에 있는 hosting
같은 경우에는 그렇지 않아 접근할 수 없다.
만약 외부에서 접근하고 싶다면 따로 public을 명시해주어야 하는데
pub
키워드를 사용하면 된다.
이 때, 경로 상의 모든 모듈, 함수 등이 public이어야 한다는 점을 유의하자.
즉, 다음과 같이 수정해야 컴파일을 할 수 있다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} fn seat_at_table() {} } mod serving { fn take_order() {} fn serve_order() {} fn take_payment() {} } } pub fn eat_at_restaurant() { crate::front_of_house::hosting::add_to_waitlist(); front_of_house::hosting::add_to_waitlist(); }
상대 경로 중에는 부모 모듈을 나타내는 super
로부터 시작하는 경우도 있다.
그 예시로, 잘못된 주문을 받은 경우 요리사가 요리 후 직접 음식을 가져다준다고 하자.
이 때, 음식을 가져다주는 것은 front_of_house
모듈에 정의되어 있고
요리사의 작업은 back_of_house
모듈에 정의된다고 하면
이는 다음과 같이 작성될 수 있다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
mod front_of_house { // snip pub mod serving { fn take_order() {} pub fn serve_order() {} fn take_payment() {} } } mod back_of_house { fn fix_incorrect_order() { cook_order(); super::front_of_house::serving::serve_order(); } fn cook_order() {} } pub fn eat_at_restaurant() { crate::front_of_house::hosting::add_to_waitlist(); front_of_house::hosting::add_to_waitlist(); }
back_of_house
모듈의 fix_incorrect_order
함수에서
그것이 정의된 모듈 바깥의 함수에 접근하고자 하여 super
를 사용한 것을 확인할 수 있다.
이렇게 super
를 사용하면 이후에 모듈 트리를 재구성하게 되더라도
이들끼리의 상대적인 경로가 달라지지 않는다면 코드 수정을 최소화 할 수 있다.
함수와 모듈뿐만 아니라 구조체나 열거형도
pub
키워드를 통해 외부에서 접근 가능하도록 공개할 수 있는데
이와 관련하여 몇 가지 알아두어야 하는 것이 있다.
구조체는 가변성이 필드가 아닌 구조체 전체를 대상으로 설정되었던 것과 달리
pub
키워드는 각각의 필드 단위로 적용된다.
따라서 공개하고자 하는 필드만 공개하고
나머지는 비공개로 놔둔 채 메서드를 통해서만 접근할 수 있도록 한다.
그리고 열거형의 경우 열거형만 공개하면 그 안의 열것값들은 자동으로 공개된다.
생각해보면 열것값은 공개 안하고 열거형만 공개한다거나 열것값 중 일부만 공개하는 건
그닥 의미 있는 일은 아니긴 하다.
restaurant
예제에 구조체와 열거형을 사용하는 코드를 추가해보자.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
mod front_of_house { // snip } mod back_of_house { pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } pub enum Appetizer { Soup, Salad, } fn fix_incorrect_order() { cook_order(); super::front_of_house::serving::serve_order(); } fn cook_order() {} } pub fn eat_at_restaurant() { front_of_house::hosting::add_to_waitlist(); let appetizer = back_of_house::Appetizer::Salad; let mut meal = back_of_house::Breakfast::summer("Rye"); meal.toast = String::from("Wheet"); println!("I'd like {} toast please", meal.toast); }
use
키워드우리가 작성한 코드를 보면 모듈이 중첩될수록 그 길이가 길어지는 것을 볼 수 있다.
이것이 길어질수록 우리 코드는 복잡해보인다.
따라서 우리는 필요에 따라 다른 범위의 코드를 현재 범위로 가져와 사용할 수 있는데
이 때 사용하는 것이 use
키워드다.
use
키워드를 통해 어떤 구문을 현재 범위로 가져오면
그것을 마치 현재 범위에서 정의한 것처럼 사용할 수 있다.
예를 들어, 다음과 같이 crate::front_of_house::hosting
을 루트 범위로 가져오면
크레이트 루트에 정의된 eat_at_restaurant
함수에서 그것을 hosting
으로 접근할 수 있다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
mod front_of_house { pub mod hosting { pub fn add_to_waitlist() {} fn seat_at_table() {} } // snip } mod back_of_house { // snip } use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); let appetizer = back_of_house::Appetizer::Salad; let mut meal = back_of_house::Breakfast::summer("Rye"); meal.toast = String::from("Wheet"); println!("I'd like {} toast please", meal.toast); }
상대경로를 사용할 경우 현재 범위의 이름 대신 self
를 넣어
use self::front_of_house::hosting;
과 같이 작성한다.
물론 use crate::front_of_house::hosting::add_to_waitlist();
처럼 가져와서
add_to_waitlist();
만으로 호출하는 것도 가능하지만
그럴 경우 이게 현재 범위에서 정의된 것인지 누구의 것인지 알기 어려워
구조체, 열거형 등을 가져올 땐 그 메서드가 아닌 그 아이템 단위로 가져오는 것이 관용적이다.
특히 서로 다른 경로에서부터 같은 이름의 아이템을 가져올 경우
Rust 컴파일러는 그 이름만으로 그것을 구분할 수 없어 이를 허용하지 않는다.
굳이 같은 이름의 아이템을 가져오고 싶다면 as
명령어를 사용할 수 있는데
use A as B;
와 같은 형식으로 작성하면 A
대신 B
라는 이름으로 사용할 수 있다.
이와 같이 명시적으로 구분지으면 원래 이름이 같아도 새 이름으로 구분해서 사용할 수 있다.
pub use
키워드pub use
키워드는 현재 범위로 가져온 것을 다시 내보내기까지 한다.
즉, 현재 범위 밖에서도 그것을 pub use
가 사용된 위치에 정의된 것처럼 사용할 수 있게 된다.
이러한 기법은 크레이트의 구조는 관리하기 좋은 상태로 둔 채
pub use
를 통해 외부에서 사용하기 좋은 형태로 접근 구조를 바꿔준다.
외부 크레이트 또한 use
키워드를 통해 가져올 수 있는데,
표준 라이브러리처럼 Prelude에 포함된 크레이트라면 그냥 use
만으로 사용 가능하지만
일반적으로는 Cargo.toml
에 패키지와 버전을 명시해주어야 한다.
숫자 맞추기 게임을 만들었을 때 했던 것처러 말이다.
Cargo.toml
의 의존성 목록에 추가하고 나면 그것의 하위 모듈 및 아이템은
use
를 통해 적절한 범위에 가져올 수 있다.
만약 둘 이상의 아이템을 use
로 가져오는데 그들의 경로에 겹치는 부분이 있다면
우리는 중첩 경로를 통해 코드의 양을 조금 줄일 수 있다.
예를 들어 다음과 같이 작성했던 건
use std::io;
use std::cmp::Ordering;
다음과 같이 작성할 수 있다.
use std::{io, Ordering};
만약 그 중 중첩된 부분과 완전히 일치하는 경로가 있다면 그것은 self
를 통해 나타낼 수 있다.
예를 들어 다음과 같이 작성했던 건
use std::io;
use std::io::Write;
다음과 같이 작성할 수 있다.
use std::io::{self, Write};
그리고 해당 범위의 모든 것을 가져오고자 한다면 다음과 같은 것도 가능한데
use std::collections::*;
어떤 것들이 가져와졌는지 알기 어려워 주의해서 사용해야 한다.
지금까지의 예제는 하나의 src/main.rs
파일, 또는 하나의 src/lib.rs
파일로 작성되었다.
하지만 전체 패키지가 커질수록 이것은 모듈 단위로 분할하는 쪽이 관리하기 좋다.
src/
아래에 front_of_house.rs
, back_of_house.rs
파일을 생성하고
해당 이름의 모듈의 본문을 각각 파일로 옮겨 보도록 하자.
그리고 src/lib.rs
에서는 코드블록을 지우고 세미콜론을 찍어
그런 이름을 가진 모듈이 존재한다는 사실만을 남긴다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house.rs
src/front_of_house.rs
pub mod hosting { pub fn add_to_waitlist() {} fn seat_at_table() {} } pub mod serving { fn take_order() {} pub fn serve_order() {} fn take_payment() {} }
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/back_of_house.rs
src/back_of_house.rs
pub struct Breakfast { pub toast: String, seasonal_fruit: String, } impl Breakfast { pub fn summer(toast: &str) -> Breakfast { Breakfast { toast: String::from(toast), seasonal_fruit: String::from("peaches"), } } } pub enum Appetizer { Soup, Salad, } fn fix_incorrect_order() { cook_order(); super::front_of_house::serving::serve_order(); } fn cook_order() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/lib.rs
src/lib.rs
mod front_of_house; mod back_of_house; use crate::front_of_house::hosting; pub fn eat_at_restaurant() { hosting::add_to_waitlist(); let appetizer = back_of_house::Appetizer::Salad; let mut meal = back_of_house::Breakfast::summer("Rye"); meal.toast = String::from("Wheet"); println!("I'd like {} toast please", meal.toast); }
이렇게 하면 Rust 컴파일러가 src/lib.rs
를 컴파일하다가
mod front_of_house;
를 만나면 같은 이름을 가진 .rs
파일을 찾아
그곳에서 front_of_house
모듈의 정의를 확인한다.
back_of_house
도 마찬가지다.
그리고 하위 모듈들도 같은 방식으로 분할할 수 있는데
하위 모듈은 상위 모듈과 같은 이름을 가진 디렉토리를 생성하여 그 안에 작성한다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ mkdir src/front_of_house
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house/hosting.rs
src/front_of_house/hosting.rs
pub fn add_to_waitlist() {} fn seat_at_table() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house/serving.rs
src/front_of_house/serving.rs
fn take_order() {} pub fn serve_order() {} fn take_payment() {}
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ vi src/front_of_house.rs
src/front_of_house.rs
pub mod hosting; pub mod serving;
그리고 이렇게 하면 front_of_table
이 파일 이름이기도 하고 폴더 이름이기도 한데
src/front_of_table.rs
를 scr/front_of_table/mod.rs
로 옮길 수도 있다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ mv src/front_of_house.rs src/front_of_house/mod.rs
자, 이로서 모듈 트리는 유지한 채 관리하기 수월한 파일 단위로 분할하였다.
개념 적인 모듈 트리는 다음과 같고
crate
├── front_of_house
│ ├── hosting
│ │ ├── add_to_waitlist
│ │ └── seat_at_table
│ └── serving
│ ├── table_order
│ ├── serve_order
│ └── take_payment
└── back_of_house
├── fix_incorrect_order
└── cook_order
실제 파일 구조는 다음과 같다.
peter@hp-laptop:~/rust-practice/chapter07/restaurant$ tree src
src
├── back_of_house.rs
├── front_of_house
│ ├── hosting.rs
│ ├── mod.rs
│ └── serving.rs
└── lib.rs
1 directory, 5 files
peter@hp-laptop:~/rust-practice/chapter07/restaurant$
모듈의 경로를 알면 그것이 실제 경로도 쉽게 찾을 수 있을 것이다.
이 포스트의 내용은 공식문서의 7장 Managing Growing Projects with Packages, Crates, and Modules에 해당합니다.