모듈 트리에서 항목을 가리키는 경로

모듈 트리에서 항목을 가리키는 경로 (Paths for Referring to an Item in the Module Tree)

파일시스템에서 파일을 찾을 때 경로(path)를 쓰듯이, 러스트도 모듈 트리에서 항목을 찾을 위치를 알려 주려면 경로를 써요. 함수를 호출하려면 그 함수의 경로를 알아야 하죠. 경로에는 절대 경로(absolute path)와 상대 경로(relative path) 두 가지 형태가 있어요. 이번 절에서는 두 경로의 차이와, 항목을 다른 코드에서 쓸 수 있게 하는 pub 키워드의 역할을 살펴볼게요.

출처: The Rust Book — 모듈 트리에서 항목을 가리키는 경로

경로의 두 가지 형태

경로는 두 가지 형태를 취할 수 있어요.

  • 절대 경로(absolute path) 는 크레이트 루트에서 시작하는 전체 경로예요. 외부 크레이트의 코드라면 크레이트 이름으로 시작하고, 현재 크레이트의 코드라면 crate라는 글자 그대로 시작해요.
  • 상대 경로(relative path) 는 현재 모듈에서 시작해서 self, super, 또는 현재 모듈 안의 식별자를 사용해요.

절대 경로와 상대 경로 모두 그 뒤에 이중 콜론(::)으로 구분된 식별자가 하나 이상 따라와요.

Listing 7-1로 돌아가서, add_to_waitlist 함수를 호출하고 싶다고 해 볼게요. 이건 "add_to_waitlist 함수의 경로가 뭐지?"라고 묻는 것과 같아요. Listing 7-3은 Listing 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();
}

eat_at_restaurant에서 add_to_waitlist 함수를 처음 호출할 때는 절대 경로를 사용했어요. add_to_waitlist 함수는 eat_at_restaurant와 같은 크레이트 안에 정의되어 있으므로 crate 키워드로 절대 경로를 시작할 수 있어요. 그리고 add_to_waitlist에 도달할 때까지 연속되는 모듈을 하나씩 넣어요. 같은 구조의 파일시스템을 상상해 볼게요. add_to_waitlist 프로그램을 실행하려면 /front_of_house/hosting/add_to_waitlist 경로를 지정하겠죠. 크레이트 이름으로 크레이트 루트에서 시작하는 것은 셸에서 /로 파일시스템 루트에서 시작하는 것과 같아요.

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 호출의 절대 경로는 그대로지만 상대 경로는 갱신해야 해요. 일반적으로는 절대 경로를 쓰는 걸 선호해요. 코드 정의와 항목 호출을 서로 독립적으로 옮기고 싶을 가능성이 더 높으니까요.

Listing 7-3을 컴파일해 보고 아직 왜 컴파일되지 않는지 확인해 볼게요. 우리가 받는 오류가 Listing 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 함수에 대한 경로는 올바르지만, 러스트가 비공개 섹션에 접근할 수 없기 때문에 그 경로를 사용하지 못하게 하는 거예요. 러스트에서 모든 항목(함수, 메서드, struct, enum, 모듈, 상수)은 기본적으로 부모 모듈에 대해 비공개예요. 함수나 struct 같은 항목을 비공개로 만들고 싶다면 그 항목을 모듈 안에 넣으면 돼요.

부모 모듈의 항목은 자식 모듈 안의 비공개 항목을 사용할 수 없지만, 자식 모듈의 항목은 조상 모듈의 항목을 사용할 수 있어요. 자식 모듈이 자신의 구현 세부 사항을 감싸고 숨기지만, 자식 모듈은 자신이 정의된 문맥을 볼 수 있기 때문이에요. 레스토랑 비유를 계속해 보면, 비공개 규칙은 레스토랑의 백 오피스와 같다고 생각하면 돼요. 그 안에서 일어나는 일은 레스토랑 손님에게는 비공개지만, 사무실 매니저는 자신이 운영하는 레스토랑의 모든 것을 보고 할 수 있죠.

러스트가 모듈 시스템을 이런 식으로 동작하게 만든 이유는 내부 구현 세부 사항을 숨기는 것을 기본값으로 하기 위해서예요. 그렇게 하면 어떤 내부 코드 부분을 바꿔도 바깥 코드를 망가뜨리지 않는지 알 수 있어요. 하지만 러스트는 pub 키워드로 항목을 공개로 만들어 자식 모듈 코드의 내부 부분을 바깥 조상 모듈에 노출하는 선택지도 줍니다.

pub 키워드로 경로 노출하기 (Exposing Paths with the pub Keyword)

Listing 7-4의 오류로 돌아가서, hosting 모듈이 비공개라고 했던 걸 기억할게요. 부모 모듈의 eat_at_restaurant 함수가 자식 모듈의 add_to_waitlist 함수에 접근할 수 있게 하고 싶으니, Listing 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();
}

안타깝게도 Listing 7-5의 코드는 Listing 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의 내용물은 여전히 비공개예요. 모듈을 공개로 만든다고 그 내용물까지 공개되는 건 아니에요. 모듈의 pub 키워드는 그 모듈의 조상 모듈에 있는 코드가 그 모듈을 참조할 수 있게만 해 주지, 내부 코드에 접근하게 해 주지는 않아요.

모듈은 컨테이너이므로 모듈만 공개한다고 해서 할 수 있는 일이 많지 않아요. 더 나아가서 그 모듈 안의 항목 중 하나 이상을 공개로 선택해야 해요. Listing 7-6의 오류는 add_to_waitlist 함수가 비공개라고 말해요. 비공개 규칙은 모듈뿐 아니라 struct, enum, 함수, 메서드에도 적용돼요.

Listing 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();
}

이제 코드가 컴파일돼요! pub 키워드를 붙이면 비공개 규칙 측면에서 왜 eat_at_restaurant에서 이 경로들을 사용할 수 있는지, 절대 경로와 상대 경로를 각각 살펴볼게요.

절대 경로에서는 우리 크레이트 모듈 트리의 루트인 crate로 시작해요. front_of_house 모듈은 크레이트 루트에 정의되어 있어요. front_of_house는 공개가 아니지만, eat_at_restaurant 함수가 front_of_house와 같은 모듈에 정의되어 있으므로(eat_at_restaurantfront_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가 정의된 모듈에서 시작하는 상대 경로가 동작해요. 그 다음 hostingadd_to_waitlistpub로 표시되어 있으니 나머지 경로도 동작해서 이 함수 호출이 유효해요!

라이브러리 크레이트를 공유해서 다른 프로젝트가 여러분의 코드를 쓰게 하려면, 공개 API(public API)는 크레이트 사용자와의 계약(contract)이에요. 사용자들이 여러분의 코드와 어떻게 상호작용할 수 있는지를 결정하죠. 사람들이 크레이트에 의존하기 쉽게 만들기 위해 공개 API의 변경을 관리하는 방법에는 고려할 것도 많아요. 그 고려 사항들은 이 책의 범위를 벗어나므로, 관심이 있다면 Rust API Guidelines를 참고하세요.

바이너리와 라이브러리를 모두 가진 패키지의 모범 사례

패키지는 src/main.rs 바이너리 크레이트 루트와 src/lib.rs 라이브러리 크레이트 루트를 모두 담을 수 있고, 두 크레이트 모두 기본적으로 패키지 이름을 가진다고 언급했어요. 보통 이렇게 라이브러리와 바이너리 크레이트를 모두 가진 패키지는, 라이브러리 크레이트에 정의된 코드를 호출하는 실행 파일을 시작하기에 충분한 코드만 바이너리 크레이트에 담아 두는 편이에요. 이렇게 하면 라이브러리 크레이트의 코드를 공유할 수 있으니 다른 프로젝트도 패키지가 제공하는 기능 대부분을 활용할 수 있어요.

모듈 트리는 src/lib.rs에 정의해야 해요. 그러면 공개 항목은 패키지의 이름으로 시작하는 경로로 바이너리 크레이트에서 사용할 수 있어요. 바이너리 크레이트는 완전히 외부의 크레이트가 라이브러리 크레이트를 쓰는 것처럼, 그 라이브러리 크레이트의 사용자가 돼요. 즉 공개 API만 사용할 수 있죠. 이렇게 하면 좋은 API를 설계하는 데도 도움이 돼요. 여러분은 작성자이면서 동시에 클라이언트(client)이기도 하니까요.

12장에서 바이너리 크레이트와 라이브러리 크레이트를 모두 가진 명령줄 프로그램으로 이 조직 방식의 실제 예시를 보여 줄게요.

super로 상대 경로 시작하기 (Starting Relative Paths with super)

경로의 시작에 super를 쓰면 현재 모듈이나 크레이트 루트가 아니라 부모 모듈에서 시작하는 상대 경로를 만들 수 있어요. 이는 파일시스템 경로의 .. 구문이 상위 디렉터리로 가는 것을 의미하는 것과 같아요. super를 쓰면 부모 모듈에 있다는 걸 아는 항목을 참조할 수 있어요. 이 항목이 부모와 밀접하게 관련되어 있고, 언젠가 부모가 모듈 트리의 다른 곳으로 옮겨질 수도 있을 때 모듈 트리를 재배치하기 쉬워져요.

Listing 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() {}
}

fix_incorrect_order 함수는 back_of_house 모듈 안에 있으므로, super를 써서 back_of_house의 부모 모듈로 갈 수 있어요. 여기서는 그게 루트인 crate예요. 거기서 deliver_order를 찾으면 찾을 수 있어요. 성공! back_of_house 모듈과 deliver_order 함수는 서로 같은 관계를 유지할 가능성이 높고, 크레이트의 모듈 트리를 재조직하기로 결정하면 함께 옮겨질 가능성이 높아요. 그래서 super를 써서, 이 코드가 나중에 다른 모듈로 옮겨질 때 갱신해야 할 곳을 줄였어요.

Struct와 Enum을 공개로 만들기 (Making Structs and Enums Public)

pub를 써서 struct와 enum을 공개로 지정할 수도 있는데, struct나 enum에 pub를 쓰는 방법에는 몇 가지 추가 세부 사항이 있어요. struct 정의 앞에 pub를 붙이면 struct는 공개가 되지만, struct의 필드는 여전히 비공개예요. 각 필드를 공개할지 말지는 경우에 따라 정할 수 있어요. Listing 7-9에서 공개된 back_of_house::Breakfast struct를 정의했는데, toast 필드는 공개이고 seasonal_fruit 필드는 비공개예요. 이건 레스토랑에서 손님은 식사에 딸린 빵 종류를 고를 수 있지만, 어떤 과일이 곁들여질지는 셰프가 계절과 재고에 따라 결정하는 경우를 모델링해요. 제공 가능한 과일은 빨리 바뀌므로 손님은 과일을 고를 수 없고, 어떤 과일을 받을지조차 볼 수 없어요.

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 struct의 toast 필드는 공개이므로 eat_at_restaurant에서 점 표기법(dot notation)으로 toast 필드에 쓰고 읽을 수 있어요. seasonal_fruit는 비공개이므로 eat_at_restaurant에서는 이 필드를 쓸 수 없다는 점도 확인해 보세요. seasonal_fruit 필드 값을 수정하는 그 줄의 주석을 해제해서 어떤 오류가 나는지 확인해 봐요!

back_of_house::Breakfast에 비공개 필드가 있으므로, struct가 Breakfast 인스턴스를 만드는 공개 연관 함수(associated function)를 제공해야 해요. 여기서는 그 함수 이름을 summer로 지었어요. Breakfast에 이런 함수가 없다면 eat_at_restaurant에서 Breakfast 인스턴스를 만들 수 없어요. 비공개 seasonal_fruit 필드의 값을 eat_at_restaurant에서 설정할 수 없으니까요.

반대로 enum을 공개로 만들면 그 변형(variant)은 전부 공개가 돼요. Listing 7-10처럼 enum 키워드 앞에 pub만 붙이면 돼요.

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을 공개로 만들었으니 eat_at_restaurant에서 SoupSalad 변형을 사용할 수 있어요.

enum은 변형이 공개되지 않으면 별로 쓸모가 없어요. 모든 경우에 모든 enum 변형에 pub를 붙여야 한다면 정말 짜증날 테니, enum 변형은 기본값으로 공개가 되도록 정했어요. 반면 struct는 필드가 공개되지 않아도 자주 유용하므로, struct 필드는 "특별히 pub로 표시하지 않으면 모두 비공개"라는 일반 규칙을 따라요.

pub와 관련해 아직 다루지 않은 상황이 하나 더 있어요. 바로 마지막 모듈 시스템 기능인 use 키워드예요. use를 먼저 단독으로 다루고, 그 다음 pubuse를 결합하는 방법을 보여 드릴게요.

더 알아보기 (Learn more)