모듈 트리의 항목을 경로로 가리키기
모듈 트리 안에서 어떤 항목을 어디서 찾을지 러스트에게 알려 주려면, 파일시스템을 탐색할 때 경로를 쓰는 것과 같은 방식으로 경로(path)를 사용합니다. 함수를 호출하려면 그 함수의 경로를 알아야 합니다.
경로는 두 가지 형태를 가질 수 있습니다.
- 절대 경로(absolute path) 는 크레이트 루트에서 시작하는 전체 경로입니다. 외부
크레이트의 코드에 대해서는 절대 경로가 크레이트 이름으로 시작하고, 현재 크레이트의
코드에 대해서는 리터럴
crate로 시작합니다. - 상대 경로(relative path) 는 현재 모듈에서 시작하며,
self,super, 또는 현재 모듈 안의 식별자를 사용합니다.
절대 경로든 상대 경로든, 그 뒤에는 이중 콜론(::)으로 구분된 하나 이상의 식별자가
이어집니다.
목록 7-1로 돌아가서, add_to_waitlist 함수를 호출하고 싶다고 해 봅시다. 이것은
결국 “add_to_waitlist 함수의 경로가 무엇인가?”라고 묻는 것과 같습니다. 목록 7-3은
일부 모듈과 함수를 제거한 목록 7-1의 코드입니다.
여기서는 크레이트 루트에 정의된 새 함수 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();
}
add_to_waitlist 함수 호출하기eat_at_restaurant 에서 add_to_waitlist 를 처음 호출할 때는 절대 경로를 사용합니다.
add_to_waitlist 함수는 eat_at_restaurant 와 같은 크레이트 안에 정의되어 있으므로,
절대 경로를 시작할 때 crate 키워드를 사용할 수 있습니다. 그런 다음
add_to_waitlist 에 도달할 때까지 각 모듈 이름을 차례로 이어 붙입니다. 이를
파일시스템으로 비유하면, /front_of_house/hosting/add_to_waitlist 경로로
add_to_waitlist 프로그램을 실행하는 것과 비슷합니다. crate 로 시작하는 것은
셸에서 파일시스템 루트 / 로 시작하는 것과 같은 느낌입니다.
두 번째 호출에서는 상대 경로를 사용합니다. 경로는 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 모듈로 옮긴다면, 절대 경로는
그대로 유지되지만 상대 경로는 수정해야 합니다. 일반적으로는, 코드 정의와 항목 호출을
독립적으로 옮기고 싶어질 가능성이 더 크기 때문에 절대 경로를 선호합니다.
이제 목록 7-3을 실제로 컴파일해 보고 왜 아직 컴파일되지 않는지 확인해 봅시다. 얻게 되는 오류는 목록 7-4에 나와 있습니다.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: module `hosting` is private
--> src/lib.rs:9:28
|
9 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| 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:12:21
|
12 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^ --------------- function `add_to_waitlist` is not publicly re-exported
| |
| private module
|
note: the module `hosting` is defined here
--> src/lib.rs:2:5
|
2 | mod hosting {
| ^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
오류 메시지는 hosting 모듈이 private 하다고 말합니다. 다시 말해 hosting 모듈과
add_to_waitlist 함수로 가는 경로 자체는 맞지만, 러스트는 private 한 부분에 접근할
수 없기 때문에 그것을 사용하게 해 주지 않습니다. 러스트에서는 모든 항목(함수, 메서드,
구조체, enum, 모듈, 상수)이 기본적으로 부모 모듈 기준으로 private 입니다. 어떤 항목을
private 로 만들고 싶다면 그냥 모듈 안에 두면 됩니다.
부모 모듈 안의 코드는 자식 모듈 안의 private 항목을 사용할 수 없습니다. 하지만 자식 모듈 안의 코드는 조상 모듈 안의 항목을 사용할 수 있습니다. 자식 모듈은 구현 세부를 감싸고 숨기지만, 자식 모듈 자체는 자신이 정의된 바깥 맥락을 볼 수 있기 때문입니다. 이 비유를 계속 사용하자면, privacy 규칙은 레스토랑의 백오피스와 비슷합니다. 그 안에서 무슨 일이 벌어지는지는 손님에게는 비공개이지만, 매니저는 자신이 운영하는 레스토랑 전체를 보고 필요한 일을 할 수 있습니다.
러스트가 모듈 시스템을 이렇게 동작하게 만든 것은, 내부 구현 세부를 숨기는 것이 기본이
되게 하기 위해서입니다. 그래야 내부 코드 중 어떤 부분은 바꿔도 외부 코드를 깨뜨리지
않는지 쉽게 알 수 있습니다. 물론 러스트는 pub 키워드를 사용해 자식 모듈 안의
코드를 바깥 조상 모듈에 노출할 수 있는 선택지도 제공합니다.
pub 키워드로 경로 노출하기
hosting 모듈이 private 하다고 알려 준 목록 7-4의 오류로 다시 돌아갑시다.
우리는 부모 모듈 안의 eat_at_restaurant 함수가 자식 모듈 안의 add_to_waitlist
함수에 접근하길 원합니다. 그러므로 목록 7-5처럼 hosting 모듈 앞에 pub
키워드를 붙입니다.
mod front_of_house {
pub mod hosting {
fn add_to_waitlist() {}
}
}
// -- snip --
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 에서 사용할 수 있도록 hosting 모듈을 pub 로 선언하기안타깝게도 목록 7-5의 코드도 여전히 컴파일 오류를 일으킵니다. 목록 7-6이 그 오류를 보여 줍니다.
$ cargo build
Compiling restaurant v0.1.0 (file:///projects/restaurant)
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:10:37
|
10 | crate::front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
error[E0603]: function `add_to_waitlist` is private
--> src/lib.rs:13:30
|
13 | front_of_house::hosting::add_to_waitlist();
| ^^^^^^^^^^^^^^^ private function
|
note: the function `add_to_waitlist` is defined here
--> src/lib.rs:3:9
|
3 | fn add_to_waitlist() {}
| ^^^^^^^^^^^^^^^^^^^^
For more information about this error, try `rustc --explain E0603`.
error: could not compile `restaurant` (lib) due to 2 previous errors
무슨 일이 있었을까요? mod hosting 앞에 pub 키워드를 붙이면 그 모듈 자체는
공개가 됩니다. 이 변경으로, 우리가 front_of_house 에 접근할 수 있다면 이제
hosting 에도 접근할 수 있습니다. 하지만 hosting 안의 내용물 은 여전히
private 입니다. 모듈을 공개로 만든다고 해서 내용물까지 자동으로 공개되지는 않습니다.
모듈에 붙은 pub 키워드는 조상 모듈의 코드가 그 모듈을 참조 할 수 있게 할 뿐,
그 안의 코드를 바로 사용할 수 있게 하지는 않습니다. 모듈은 컨테이너이기 때문에,
모듈만 공개로 만들어서는 할 수 있는 일이 많지 않습니다. 그 안의 항목 중 하나 이상도
명시적으로 공개해야 합니다.
목록 7-6의 오류는 add_to_waitlist 함수가 private 하다고 말합니다. privacy 규칙은
모듈뿐 아니라 구조체, enum, 함수, 메서드에도 모두 적용됩니다.
그렇다면 목록 7-7처럼 add_to_waitlist 함수 정의 앞에도 pub 키워드를 붙여 이
함수도 공개로 만들어 봅시다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
// -- snip --
pub fn eat_at_restaurant() {
// Absolute path
crate::front_of_house::hosting::add_to_waitlist();
// Relative path
front_of_house::hosting::add_to_waitlist();
}
mod hosting 과 fn add_to_waitlist 에 pub 를 추가하면 eat_at_restaurant 에서 함수를 호출할 수 있다이제 코드는 컴파일됩니다! privacy 규칙과 관련해서 왜 pub 를 추가하면
eat_at_restaurant 안에서 이 경로들을 사용할 수 있게 되는지, 절대 경로와 상대
경로를 기준으로 다시 봅시다.
절대 경로에서는 먼저 크레이트 모듈 트리의 루트인 crate 에서 시작합니다.
front_of_house 모듈은 크레이트 루트에 정의되어 있습니다. front_of_house
자체는 public 이 아니지만, eat_at_restaurant 함수가 front_of_house 와 같은
모듈(즉 둘이 형제) 안에 정의되어 있으므로 eat_at_restaurant 에서
front_of_house 를 참조할 수 있습니다. 그 다음은 pub 로 표시된 hosting
모듈입니다. 우리는 hosting 의 부모 모듈에 접근할 수 있으므로 hosting 에도
접근할 수 있습니다. 마지막으로 add_to_waitlist 함수도 pub 로 표시되어 있고,
그 부모 모듈에도 접근할 수 있으므로, 이 함수 호출은 유효합니다.
상대 경로에서는 첫 단계만 다르고 논리는 같습니다. 크레이트 루트가 아니라
front_of_house 에서 시작합니다. front_of_house 모듈은 eat_at_restaurant 와
같은 모듈 안에 정의되어 있으므로, eat_at_restaurant 가 속한 모듈에서 시작하는
상대 경로도 작동합니다. 그 뒤로는 hosting 과 add_to_waitlist 가 둘 다 pub
이므로 나머지 경로도 유효합니다.
만약 여러분이 라이브러리 크레이트를 공유해서 다른 프로젝트에서도 그 코드를 사용하게 하려면, 공개 API는 사용자와의 계약이 됩니다. 사용자들이 여러분의 코드와 어떻게 상호작용할 수 있는지를 결정하기 때문입니다. 사람들이 여러분의 크레이트에 쉽게 의존할 수 있도록 공개 API 변경을 관리하는 데는 여러 고려사항이 있습니다. 이 책의 범위를 벗어나는 주제이므로, 관심이 있다면 Rust API Guidelines를 참고하세요.
바이너리와 라이브러리를 함께 가진 패키지의 모범 사례
앞에서 하나의 패키지는 src/main.rs 바이너리 크레이트 루트와 src/lib.rs 라이브러리 크레이트 루트를 모두 가질 수 있고, 이 둘은 기본적으로 패키지 이름을 공유한다고 말했습니다. 보통 이런 형태의 패키지는, 바이너리 크레이트 안에는 실행 파일을 시작할 만큼의 코드만 두고, 실제 핵심 로직은 라이브러리 크레이트에 두는 것이 일반적입니다. 이렇게 하면 패키지가 제공하는 기능 대부분을 다른 프로젝트도 재사용할 수 있기 때문입니다.
모듈 트리는 src/lib.rs 에 정의하는 것이 좋습니다. 그러면 공개된 항목은 패키지 이름으로 시작하는 경로를 통해 바이너리 크레이트 안에서 사용할 수 있습니다. 바이너리 크레이트는 완전히 외부의 다른 크레이트가 라이브러리 크레이트를 쓰는 것과 같은 방식으로 라이브러리 크레이트를 사용하는 사용자가 됩니다. 즉 공개 API만 쓸 수 있습니다. 이는 좋은 API 설계를 돕습니다. 여러분은 라이브러리의 작성자일 뿐 아니라, 동시에 그 라이브러리의 클라이언트이기도 하기 때문입니다.
12장에서는 바이너리 크레이트와 라이브러리 크레이트를 모두 포함하는 커맨드라인 프로그램을 만들면서, 이 조직 방식을 실제로 보여 줄 것입니다.
super 로 상대 경로 시작하기
현재 모듈이나 크레이트 루트가 아니라, 부모 모듈 에서 시작하는 상대 경로도 만들
수 있습니다. 경로 시작에 super 를 사용하면 됩니다. 이는 파일시스템 경로에서
부모 디렉터리로 가는 .. 와 비슷합니다. super 를 사용하면 부모 모듈 안에 있는
항목을 참조할 수 있으므로, 어떤 모듈이 부모와 밀접하게 관련되어 있지만 언젠가
모듈 트리 안의 다른 위치로 부모와 함께 이동할 가능성이 있을 때 특히 유용합니다.
목록 7-8의 코드는 셰프가 잘못된 주문을 바로잡은 뒤 직접 손님에게 전달하는 상황을
모델링합니다. back_of_house 모듈 안에 정의된 fix_incorrect_order 함수는
super 로 시작하는 경로를 사용해 부모 모듈 안에 정의된 deliver_order 함수를
호출합니다.
fn deliver_order() {}
mod back_of_house {
fn fix_incorrect_order() {
cook_order();
super::deliver_order();
}
fn cook_order() {}
}
super 로 시작하는 상대 경로로 함수 호출하기fix_incorrect_order 함수는 back_of_house 모듈 안에 있으므로, super 를 사용해
back_of_house 의 부모 모듈로 올라갈 수 있습니다. 이 경우 부모는 크레이트 루트인
crate 입니다. 거기서부터 deliver_order 를 찾으면 됩니다. 성공입니다!
우리는 back_of_house 모듈과 deliver_order 함수가 앞으로도 서로 같은 관계를
유지한 채 함께 이동할 가능성이 크다고 생각합니다. 따라서 이 코드가 나중에 다른
모듈로 옮겨지더라도 수정할 곳을 줄이기 위해 super 를 사용했습니다.
구조체와 enum을 공개로 만들기
구조체와 enum도 pub 를 사용해 공개로 지정할 수 있지만, 이 경우에는 몇 가지 추가
세부가 있습니다. 구조체 정의 앞에 pub 를 붙이면 구조체 자체는 공개되지만, 구조체의
필드들은 여전히 private 입니다. 각 필드는 공개할지 말지를 개별적으로 정할 수 있습니다.
목록 7-9에서는 public 한 back_of_house::Breakfast 구조체를 정의했는데, toast
필드는 public 이고 seasonal_fruit 필드는 private 입니다. 이는 손님은 식사에 어떤
빵이 나올지 선택할 수 있지만, 어떤 과일이 곁들여질지는 계절과 재고 상황에 따라
셰프가 결정하는 레스토랑 상황을 모델링한 것입니다. 과일 종류는 자주 바뀌므로,
손님은 과일을 고를 수도 없고 어떤 과일이 나올지도 볼 수 없습니다.
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 fn eat_at_restaurant() {
// Order a breakfast in the summer with Rye toast.
let mut meal = back_of_house::Breakfast::summer("Rye");
// Change our mind about what bread we'd like.
meal.toast = String::from("Wheat");
println!("I'd like {} toast please", meal.toast);
// The next line won't compile if we uncomment it; we're not allowed
// to see or modify the seasonal fruit that comes with the meal.
// meal.seasonal_fruit = String::from("blueberries");
}
back_of_house::Breakfast 구조체의 toast 필드는 public 이므로,
eat_at_restaurant 안에서 점 표기법으로 toast 필드를 읽고 쓸 수 있습니다.
하지만 seasonal_fruit 는 private 이기 때문에 eat_at_restaurant 에서 사용할 수
없습니다. 직접 seasonal_fruit 값을 바꾸려는 줄의 주석을 풀어 보면 어떤 오류가
나는지 확인할 수 있습니다!
또한 back_of_house::Breakfast 는 private 필드를 하나 가지고 있으므로,
Breakfast 인스턴스를 만들어 주는 public 연관 함수가 필요합니다(여기서는 summer
라는 이름을 붙였습니다). 만약 Breakfast 에 이런 함수가 없다면,
eat_at_restaurant 안에서 Breakfast 인스턴스를 만들 수 없습니다. private 필드
seasonal_fruit 값을 설정할 방법이 없기 때문입니다.
반면 enum을 public 으로 만들면, 그 variant 들은 자동으로 모두 public 이 됩니다.
즉 enum 키워드 앞에만 pub 를 붙이면 됩니다. 목록 7-10을 보세요.
mod back_of_house {
pub enum Appetizer {
Soup,
Salad,
}
}
pub fn eat_at_restaurant() {
let order1 = back_of_house::Appetizer::Soup;
let order2 = back_of_house::Appetizer::Salad;
}
Appetizer enum을 public 으로 만들었기 때문에, eat_at_restaurant 안에서 Soup
와 Salad variant를 사용할 수 있습니다.
Enum은 variant 가 public 이 아니면 사실상 별로 쓸모가 없습니다. 그래서 매번 variant
마다 전부 pub 를 달아야 한다면 꽤 성가실 것입니다. 이런 이유로 enum variant는
기본적으로 public 입니다. 반면 구조체는 필드가 공개되지 않아도 유용한 경우가 많기
때문에, 구조체 필드는 pub 로 표시하지 않는 한 기본적으로 private 라는 일반 규칙을
따릅니다.
이제 pub 과 관련해 아직 다루지 않은 마지막 모듈 시스템 기능이 하나 남아 있습니다.
바로 use 키워드입니다. 먼저 use 자체를 살펴보고, 그다음 pub 과 use 를 함께
쓰는 방법을 보겠습니다.