Chap.7 crates and modules
거대한 프로그램을 작성할 때에는 코드 관리가 무척 중요합니다.
- 어느 시점부터는 머릿속에서 생각하는 것만으로는 전체 프로그램의 변화를 쫓아갈 수 없기 때문입니다.
- 코드에서 연관된 기능은 묶고 서로 다른 기능은 분리해두 어야,
- 이후 특정 기능을 구현하는 코드를 찾거나 변경할 때 헤맬 필요 없습니다.
- 따라서 프로젝트 규모가 커지면 코드를 여러 모듈, 여러 파일로 나누어 관리할 필요가 있습니다.
한 패키지 내에는 여러 바이너리 크레이트와 (원할 경우) 라이브러리 크레이트를 포함시킬 수 있으므로, 커진 프로젝트의 각 부분을 크레이트로 나눠서 외부 라이브러리처럼 쓸 수 있습니다.
이번 장에서 배워 볼 것은 이러한 기술들입니다. 상호연관된 패키지들로 이루어진 대규모 프로젝트의 경우, 14장 “Cargo Workspaces” 절에서 다룰 예정인, Cargo에서 제공하는 Workspace 기능을 이용합니다.
그룹화 외에도, 세부 구현을 캡슐화하면 더 고수준으로 코드를 재사용할 수 있습니다. 어떤 작업을 구현해두고 다른 코드에서 해당 코드의 공개 인터페이스를 통해 호출하면, 세부적인 작동은 알 필요 없죠. 여러분은 어떤 부분을 다른 코드에서 사용할 수 있도록 공개하고, 어떤 부분을 비공개된 세부 구현으로 만들어 자유롭게 수정할 수 있도록 할지를 정의하는 방식으로 코드를 작성합니다. 머릿속에 담아두어야 하는 정보의 양을 줄이는 또 다른 방법이기도 합니다.
스코프 개념도 관련되어 있습니다. 중첩된 컨텍스트에 작성한 코드는 "스코프 내" 정의된 다양한 이름들이 사용됩니다. 프로그래머나 컴파일러가 코드를 읽고, 쓰고, 컴파일할 때는 어떤 위치의 어떤 이름이 무엇을 의미하는지 알아야 합니다. 해당 이름이 변수인지, 함수인지, 열거형인지, 모듈인지, 상수인지, 그 외 요소인지 말이죠. 동일한 스코프 내에는 같은 이름을 가진 요소가 둘 이상 존재할 수 없기 때문에, 스코프를 의도적으로 생성해 어떤 이름은 스코프 내에 위치하고 어떤 이름은 스코프 밖에 위치하도록 조정하기도 합니다. (이름 충돌을 해결하는 도구도 존재합니다)
러스트에는 코드 조직화에 필요한 기능이 여럿 있습니다. 어떤 세부 정보를 외부에 노출할지, 비공개로 둘지, 프로그램의 스코프 내 항목 이름 등 다양합니다. 이를 통틀어 모듈 시스템 이라 하며, 다음 기능들이 포함됩니다.
- 패키지 크레이트를 빌드하고, 테스트하고, 공유하는데 사용하는 Cargo 기능입니다.
- 크레이트 라이브러리나 실행 가능한 모듈로 구성된 트리 구조입니다.
- 모듈, use: 구조, 스코프를 제어하고, 조직 세부 경로를 감추는 데 사용합니다.
- 경로 구조체, 함수, 모듈 등의 이름을 지정합니다.
이번 장에서는 이 기능들을 모두 다뤄보면서 어떻게 작동하고, 어떻게 사용해서 스코프를 관리하는지 등을 배워보겠습니다. 이번 장을 마치고 나면, 모듈 시스템을 확실히 이해하고 스코프를 자유자재로 다룰 수 있을 거랍니다!
7.1 패키지, 크레이트
모듈 시스템
에서 처음 다뤄볼 내용은 패키지
와 크레이트
입니다.
- 크레 이트는 바이너리일 수도 있고, 라이브러리일 수도 있습니다.
- 러스트 컴파일러는 크레이트 루트 라는 소스 파일부터 컴파일을 시작해서 여러분이 작성한 크레이트의 루트 모듈을 구성합니다. (모듈은 "모듈을 정의하여 스코프 및 공개 여부 제어하기"에서 알아볼 예정입니다) 패키지 는 하나 이상의 크레이트로 기능을 구성해 제공합니다.
- 패키지 내 Cargo.toml 파일은 패키지의 크레이트를 빌드하는 법을 나타냅니다.
패키지에 무엇을 포함할 수 있는가에 대해서는 규칙이 몇 가지 있습니다.
- 라이브러리 크레이트는 하나만 넣을 수 있습니다.
- 바이너리 크레이트는 원하는 만큼 포함할 수 있습니다.
- 단, 패키지에는 적어도 하나 이상의 크레이트(라이브러리이건, 바이너리이건)가 포함되어야 합니다.
패키지를 생성할 때 어떤 일이 일어나는지 살펴보죠. 먼저 cargo new
명령어를 입력합니다.
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
-
명령어를 입력하면 Cargo는 Cargo.toml 파일을 생성하여, 새로운 패키지를 만들어 줍니다.
- 패키지명과 같은 이름의 바이너리 크레이트는 크레이트 루트가 src/main.rs 라는 규칙이 있기 때문에, Cargo.toml 파일을 살펴보아도 src/main.rs 가 따로 언급되진 않습니다.
-
마찬가지로, 패키지 디렉토리에 src/lib.rs 파일이 존재할 경우, Cargo는 해당 패키지가 패키지명과 같은 이름의 라이브러리 크레이트를 포함하고 있다고 판단합니다.
- 물론 그 라이브러리 크레이트의 크레이트 루트는 src/lib.rs 이고요. Cargo는 크레이트를 빌드할 때(라이브러리이건, 바이너리이건) 크레이트 루트 파일을
rustc
로 전달합니다.
- 물론 그 라이브러리 크레이트의 크레이트 루트는 src/lib.rs 이고요. Cargo는 크레이트를 빌드할 때(라이브러리이건, 바이너리이건) 크레이트 루트 파일을
-
현재 패키지는 src/main.rs 만 포함하고 있으므로
my-project
바이너리 크레이트만 포함합니다. -
만약 어떤 패키지가 src/main.rs 와 src/lib.rs 를 포함한다면 해당 패키지는 패키지와 이름이 같은 바이너리, 라이브러리 크레이트를 포함하게 됩니다.
-
src/bin 디렉토리 내에 파일을 배치하면 각각의 파일이 바이너리 크레이트가 되어, 여러 바이너리 크레이트를 패키지에 포함할 수 있습니다.
-
크레이트는 관련된 기능을 그룹화함으로써 특정 기능을 쉽게 여러 프로젝트 사이에서 공유합니다.
-
예를 들어, 2장 에서 사용한
rand
크레이트는 랜덤한 숫자를 생성하는 기능을 제공합니다. -
우린 프로젝트 스코프에
rand
크레이트를 가져오기만 하면 우리가 만든 프로젝트에서 랜덤 숫자 생성 기능을 이용할 수 있죠. -
rand
크레이트가 제공하는 모든 기능은 크레이트의 이름인rand
를 통해 접근 가능합니다.
크레이트의 기능이 각각의 스코프를 갖도록 하면 특정 기능이 우리 크레이트에 있는지, rand
크레이트에 있는지를 명확하게 알 수 있으며, 잠재적인 충돌을 방지할 수도 있습니다. 예를 들어, 우리가 만든 크레이트에 Rng
라는 이름의 구조체를 정의한 상태로, Rng
트레잇을 제공하는 rand
크레이트를 의존성에 추가하더라도 컴파일러는 Rng
라는 이름이 무엇을 가리키는지 정확히 알 수 있습니다. Rng
는 우리가 만든 크레이트 내에서 정의한 struct Rng
를 가르키고, rand
크레이트의 Rng
트레잇은 rand::Rng
로 접근해야 하죠.
7.2 모듈을 정의하여 스코프 및 공개 여부 제어하기
이번에는 모듈, 항목의 이름을 지정하는 경로(path), 스코프에 경로를 가져오는 use
키워드, 항목을 공개하는 데 사용하는 pub
키워드를 알아보겠습니다. as
키워드, 외부 패키지, 글롭 연산자 등도 다룰 예정이지만, 일단은 모듈에 집중하죠!
- 모듈(module) 은 가독성과 재사용성을 위해서 크레이트 내 코드를 그룹화하는 데 사용됩니다.
- 외부에 공개(public) 할지, 내부의 세부 구현이니 외부에서 사용할 수 없도록 비공개(private) 할지 제어하는 역할도 있습니다.
예시로, 레스토랑 기능을 제공하는 라이브러리 크레이트를 작성한다고 가정해보죠. 코드 구조에 집중할 수 있도록 레스토랑을 실제 코드로 구현하지는 않고, 본문은 비워둔 함수 시그니처만 정의하겠습니다.
레스토랑 업계에서는 레스토랑을 크게 접객 부서(front of house) 와 지원 부서(back of house) 로 나눕니다. 접객 부서는 호스트가 고객을 안내하고, 웨이터가 주문 접수 및 결제를 담당하고, 바텐더가 음료를 만들어 주는 곳입니다. 지원 부서는 셰프, 요리사, 주방보조가 일하는 주방과 매니저가 행정 업무를 하는 곳입니다.
함수를 중첩 모듈로 구성하면 크레이트 구조를 실제 레스토랑이 일하는 방식과 동일하게 구성할 수 있습니다. cargo new --lib restaurant
명령어를 실행하여 restaurant
라는 새 라이브러리를 생성하고, Listing 7-1 코드를 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
키워드와 모듈 이름(이 경우 front_of_house
)을 명시하고, 본문을 중괄호로 감싸 모듈을 정의하였습니다. hosting
, serving
모듈처럼, 모듈 내에는 다른 모듈을 넣을 수 있습니다. 모듈은 구조체, 열거형, 상수, 트레잇, 함수(Listing 7-1처럼) 등의 항목 정의를 지닐 수 있습니다.
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
위 코드를 모듈 트리로 나타낸 모습
- 트리는 모듈이 서로 어떻게 중첩되어 있는지 보여줍니다
- (예시:
hosting
모듈은front_of_house
내에 위치함)
- (예시:
hosting
,serving
모듈이 둘 다 동일하게front_of_house
모듈 내에 위치한 것처럼, 어떤 모듈이 형제 관계에 있는지 나타내기도 합니다.- 가족 관계로 계속 비유하면, 모듈 A가 모듈 B 내에 있을 경우, 모듈 A는 모듈 B의 자식 이며, 모듈 B는 모듈 A의 부모 라고 표현할 수 있습니다.
- 전체 모듈 트리 최상위에
crate
라는 모듈이 암묵적으로 위치한다는 점을 기억해두세요.
모듈 트리에서 컴퓨터 파일 시스템의 디렉토리 트리를 연상하셨다면, 적절한 비유입니다! 파일 시스템의 디렉토리처럼, 여러분은 모듈로 코드를 조직화합니다.
7.3 경로를 사용해 모듈 트리에서 항목 가리키기
러스트 모듈 조직도에서 항목을 찾는 방법은, 파일 시스템에서 경로를 사용하는 방법과 동일합니다.
경로는 두 가지 형태가 존재합니다.
- 절대 경로 는 크레이트 이름이나
crate
리터럴을 사용하며, 크레이트 루트를 기준점으로 사용합니다. - 상대 경로 는
self
,super
를 사용하며, 현재 모듈을 기준점으로 사용합니다.
절대 경로, 상대 경로 뒤에는 ::
으로 구분된 식별자가 하나 이상 따라옵니다.
add_to_waitlist
함수를 호출하려면 어떻게 해야 할까요?- 다시 말해서,
add_to_waitlist
함수의 경로는 무엇일까요? eat_at_restaurant
라는 새로운 함수에서add_to_waitlist
함수를 두 가지 방법으로 호출하는 예시를 보여줍니다.eat_at_restaurant
함수는 우리가 만든 라이브러리 크레이트의 공개 API 중 하나입니다.- 따라서
pub
키워드로 지정되어 있습니다. pub
키워드는 "pub
키 워드로 경로 노출하기" 절에서 자세히 알아볼 예정입니다.
mod front_of_house {
mod hosting {
fn add_to_waitlist() {}
}
}
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
-
eat_at_restaurant
함수에서 처음add_to_waitlist
함수를 호출할 때는 절대 경로를 사용했습니다.add_to_waitlist
함수는eat_at_restaurant
함수와 동일한 크레이트에 정의되어 있으므로, 절대 경로의 시작점에crate
키워드를 사용할 수 있습니다.crate
뒤에는add_to_waitlist
함수에 도달할 때까지의 모듈을 연속해서 작성합니다.- 파일 시스템 구조로 생각해 보죠.
/front_of_house/hosting/add_to_waitlist
경로를 작성하여add_to_waitlist
프로그램을 실행했군요. crate
를 작성해 크레이트 루트를 기준으로 사용하는 것은 셸(shell)에서/
로 파일 시스템의 최상위 디렉토리를 기준으로 사용하는 것과 같습니다.
-
eat_at_restaurant
함수에서 두 번째로add_to_waitlist
함수를 호출할 때는 상대 경로를 사용했습니다.- 경로는 모듈 트리에서
eat_at_restaurant
함수와 동일한 위치에 정의되어 있는front_of_house
모듈로 시작합니다. - 파일 시스템으로 비유하자면
front_of_house/hosting/add_to_waitlist
가 되겠네요. - 파일 시스템 경로에서, 항목의 이름으로 시작하는 경로는 상대 경로입니다.
- 경로는 모듈 트리에서
-
상대 경로, 절대 경로 중 무엇을 사용할지는 프로젝트에 맞추어 여러분이 선택해야 합니다.
-
이는 여러분이 항목을 정의하는 코드와 항목을 사용하는 코드를 분리하고 싶은지, 혹은 같이 두고 싶은지에 따라 결정되어야 합니다.
-
예를 들어,
front_of_house
모듈과eat_at_restaurant
함수를customer_experience
라는 모듈 내부로 이동시켰다고 가정해보죠. -
add_to_waitlist
함수를 절대 경로로 작성했다면 코드를 수정해야 하지만, 상대 경로는 수정할 필요가 없습니다. 반면,eat_at_restaurant
함수를 분리하여dining
이라는 모듈 내부로 이동시켰다면,add_to_waitlist
함수를 가리키는 절대 경로는 수정할 필요가 없지만, 상대 경로는 수정해야 합니 다. -
우리가 선호하는 경로는 절대 경로입니다.
- 항목을 정의하는 코드와 호출하는 코드는 분리되어 있을 가능성이 높기 때문입니다.
우리는 hosting
모듈과 add_to_waitlist
함수의 경로를 정확히 명시했지만, 해당 영역은 비공개 영역이기 때문에 러스트가 접근을 허용하지 않습니다.
-
모듈은 코드를 조직화하는 용도로만 쓰이지 않습니다.
-
러스트의 비공개 경계(privacy boundary) 를 정의하는 역할도 있습니다.
-
캡슐화
된 세부 구현은 외부 코드에서 호출하거나 의존할 수 없고, 알 수도 없습니다. -
따라서 비공개로 만들고자 하는 함수나 구조체가 있다면, 모듈 내에 위치시키면 됩니다.
-
러스트에서, 모든 항목(함수, 메소드, 구조체, 열거형, 모듈, 상수)은 기본적으로 비공개입니다.
-
부모 모듈 내 항목은 자식 모듈 내 비공개 항목을 사용할 수 없지만, 자식 모듈 내 항목은 부모 모듈 내 항목을 사용할 수 있습니다.
-
이유는, 자식 모듈의 세부 구현은 감싸져서 숨겨져 있지만, 자식 모듈 내에서는 자신이 정의된 컨텍스트를 볼 수 있기 때문입니다.
-
러스트 모듈 시스템은 내부의 세부 구현을 기본적으로 숨기도록 되어 있습니다.
- 이로써, 여러분은 외부 코드의 동작을 망가뜨릴 걱정 없이 수정할 수 있는 코드가 어느 부분인지 알 수 있죠.
- 만약, 자식 모듈의 내부 요소를 공개(public)함으로써 외부 상위 모듈로 노출하고자 한다면,
pub
키워드를 사용합니다.