Rust - 크레이트(Crate), 패키지(Package), 모듈(Module)

Chan Heo·2024년 11월 10일

Rust 공부

목록 보기
5/7
post-thumbnail

프로젝트가 커지면 커질수록 방대해지는 코드를 효율적으로 관리하는 것은 아주 중요합니다. Rust에서 이러한 코드 조직화를 다루기 위해 제공하는 크레이트 / 패키지 / 모듈 개념에 대한 내용을 정리해 보겠습니다.

크레이트 (Crate)

  • Rust가 컴파일 한 차례에 고려하는 가장 작은 코드 단위
  • 바이너리 크레이트와 라이브러리 크레이트 2종류가 있다.
  • 크레이트는 여러 모듈을 담을 수 있다. (모듈은 이 크레이트와 함께 컴파일되는 다른 파일들에 정의되어 있을 수도 있습니다.)
  • 바이너리 크레이트(Binary crate):
    • 실행 가능한 실행파일로 컴파일할 수 있는 프로그램
    • main 함수를 포함하고 있어야 한다. → 여태껏 만들어 본 모든 크레이트는 바이너리 크레이트였음.
  • 라이브러리 크레이트(Library crate):
    • main 함수를 가지고 있지 않고, 실행파일의 형태로 컴파일되지 않는다.
    • 대신, 여러 프로젝트에서 공용될 의도로 만들어진 기능들이 정의되어 있다.
    • 러스타시안들이 ‘크레이트’라 말하는 것은 대부분 라이브러리 크레이트를 의미하는 것이고, 이는 일반적인 프로그래밍 개념에서의 ‘라이브러리’와 혼용된다.
  • 크레이트 루트(crate root):
    • 러스트 컴파일러가 컴파일을 시작하는 소스 파일이다.
    • 크레이트의 루트 모듈을 구성한다.
    • Library crate → src/lib.rs 가 크레이트 루트 & Binary crate → src/main.rs 가 크레이트 루트.

패키지 (Package)

  • 일련의 기능을 제공하는 하나 이상의 crate들의 번들이다.
  • Cargo.toml 파일 포함
  • 적어도 하나의 crate를 포함해야 한다. (Binary든 Library든)
  • Binary 크레이트는 원하는 만큼 포함할 수 있지만, Library 크레이트는 단 하나만 넣을 수 있다.
  • cargo new 명령어 → 패키지를 만든다.
    $ cargo new my-project
         Created binary (application) `my-project` package
         
    $ ls my-project
    Cargo.toml
    src
    
    $ ls my-project/src
    main.rs
  • 현재 패키지는 src/main.rs 만 포함하고 있으므로 이 패키지는 my-project라는 이름의 바이너리 크레이트만으로 구성되어 있습니다. 만약 어떤 패키지가 src/main.rs와 src/lib.rs를 가지고 있다면 해당 패키지는 패키지와 같은 이름의 바이너리, 라이브러리 크레이트를 포함하게 됩니다. (즉, 한 패키지에 크레이트 2개!) src/bin 디렉터리 내에 파일을 배치하면 각각의 파일이 바이너리 크레이트가 되어, 여러 바이너리 크레이트를 패키지에 포함할 수 있습니다.

모듈 (Module)

모듈 치트 시트

https://doc.rust-kr.org/ch07-02-defining-modules-to-control-scope-and-privacy.html#모듈-치트-시트

  • 크레이트 루트부터 시작: 크레이트를 컴파일할 때 컴파일러는 먼저 크레이트 루트 파일을 본다.
  • 모듈 선언: 크레이트 루트 파일에는 새로운 모듈을 선언할 수 있다; mod garden;이라는 코드로 ‘garden’ 모듈을 선언할 수 있습니다. 컴파일러는 아래의 장소에서 이 모듈의 코드가 있는지 찾는다.
    • mod garden 뒤에 세미콜론 대신 중괄호를 써서 안쪽에 코드를 적은 인라인
    • src/garden.rs 파일 안
    • src/garden/mod.rs 파일 안 (예전 스타일)
  • 서브모듈 선언: crate root가 아닌 다른 파일에서는 서브모듈을 선언할 수 있다. (ex. src/garden.rs 안에 mod vegetables; 를 선언할 수 있다.) → 아래의 장소에서 찾는다.
    • mod vegetables 뒤에 세미콜론 대신 중괄호를 써서 안쪽에 코드를 적은 인라인
    • src/garden/vegetables.rs 파일 안
    • src/garden/vegetables/mod.rs 파일 안 (예전 스타일)
  • 모듈 내 코드로의 경로: 일단 모듈이 크레이트의 일부로서 구성되면, 공개 규칙이 허용하는 한도 내에서라면 해당 코드의 경로를 사용하여 동일한 크레이트의 어디에서든 이 모듈의 코드를 참조할 수 있게 됩니다. 예를 들면, garden vegetables 모듈 안에 있는 Asparagus 타입은 crate::garden::vegetables::Asparagus로 찾아 쓸 수 있습니다.
  • 비공개 vs 공개: 모듈 내의 코드는 기본적으로 부모 모듈에게 비공개 (private) 입니다. 모듈을 공개 (public) 로 만들려면, mod 대신 pub mod를 써서 선언하세요. 공개 모듈의 아이템들을 공개하려면 마찬가지로 그 선언 앞에 pub을 붙이세요.
  • use 키워드: 어떤 스코프 내에서 use 키워드는 긴 경로의 반복을 줄이기 위한 어떤 아이템으로의 단축경로를 만들어 줍니다. crate::garden::vegetables::Asparagus를 참조할 수 있는 모든 스코프에서 use crate::garden::vegetables::Asparagus;로 단축경로를 만들 수 있으며, 그 이후부터는 스코프에서 이 타입을 사용하려면 Asparagus만 작성해주면 됩니다.

위의 규칙들을 보여주는 backyard라는 이름의 바이너리 크레이트를 만들어 보았습니다. 디렉터리명 또한 backyard로서, 아래의 파일들과 디렉터리들로 구성되어 있습니다.

backyard
├── Cargo.lock
├── Cargo.toml
└── src
    ├── garden
    │   └── vegetables.rs
    ├── garden.rs
    └── main.rs

지금의 경우 크레이트 루트 파일은 src/main.rs이고, 내용은 아래와 같습니다:

파일명: src/main.rs

use crate::garden::vegetables::Asparagus;

pub mod garden;

fn main() {
    let plant = Asparagus {};
    println!("I'm growing {:?}!", plant);
}

pub mod garden; 라인이 컴파일러에게 src/garden.rs에 있는 코드를 포함할 것을 알려주고, src/garden.rs는 아래와 같습니다:

파일명: src/garden.rs

pub mod vegetables;

여기 pub mod vegetables;은 src/garden/vegetables.rs의 코드 또한 포함되어야 함을 의미합니다. 해당 파일의 코드는 아래와 같습니다:

#[derive(Debug)]
pub struct Asparagus {}

이제 위 규칙들의 세부 사항으로 넘어가서 실제로 해보면서 확인합시다!

모듈 트리 예시

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)을 지정하여 모듈을 정의합니다.
  • 모듈의 본문은 중괄호로 감싸져 있습니다.
  • hostingserving 모듈처럼, 모듈 내에는 다른 모듈을 넣을 수 있습니다.
  • 모듈에는 구조체, 열거형, 상수, 트레이트, 함수(예제 7-1처럼) 등의 아이템 정의 또한 가질 수 있습니다.

위 예시를 트리 구조로 표현하면,

crate
 └── front_of_house
     ├── hosting
     │   ├── add_to_waitlist
     │   └── seat_at_table
     └── serving
         ├── take_order
         ├── serve_order
         └── take_payment
  • 마치, 파일 디렉터리 구조가 연상된다!
  • 전체 모듈 트리 최상위에 crate라는 모듈이 암묵적으로 위치한다는 점을 기억해 두세요.
  • 앞서 src/main.rs와 src/lib.rs는 크레이트 루트라고 부른다고 언급했습니다. 이 두 파일이 그런 이름을 갖게 된 이유는 모듈 트리 (module tree) 라고 불리는 크레이트 모듈 구조에서 최상위에 crate라는 이름을 갖는 일종의 모듈로 형성되기 때문입니다.

경로를 사용하여 모듈 트리의 아이템 참조하기

경로는 두 가지 형태가 존재합니다.

  • 절대 경로 (absolute path) 는 크레이트 루트로부터 시작되는 전체 경로이다.
    외부 크레이트로부터의 코드에 대해서는 해당 크레이트 이름으로 절대 경로가 시작되고 현재의 크레이트로부터의 코드에 대해서는 crate 리터럴로부터 시작됩니다.
  • 상대 경로 (relative path) 는 현재의 모듈을 시작점으로 하여 selfsuper 혹은 현재 모듈 내의 식별자를 사용합니다.

절대 경로, 상대 경로 뒤에는 ::으로 구분된 식별자가 하나 이상 따라옵니다.

mod front_of_house {
    mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 절대 경로
    crate::front_of_house::hosting::add_to_waitlist();

    // 상대 경로
    front_of_house::hosting::add_to_waitlist();
}

→ 이 코드는 에러가 발생합니다! 에러 메시지는 hosting 모듈이 비공개 (private) 라는 내용입니다. hosting 모듈과 add_to_waitlist 함수의 경로를 정확히 명시했지만, 해당 영역은 비공개 영역이기 때문에 러스트가 접근을 허용하지 않습니다.

  • 러스트에서는 (함수, 메서드, 구조체, 열거형, 모듈, 그리고 상수 등) 모든 아이템이 기본적으로 부모 모듈에 대해 비공개입니다.
    → 함수나 구조체 같은 아이템을 비공개로 하고 싶다면 모듈에 넣으면 됩니다.
  • 부모 모듈 내 아이템은 자식 모듈 내 비공개 아이템을 사용할 수 없지만, 자식 모듈 내 아이템은 부모 모듈 내 아이템을 사용할 수 있습니다.
    이유는, 자식 모듈의 세부 구현은 감싸져서 숨겨져 있지만, 자식 모듈 내에서는 자신이 정의된 컨텍스트를 볼 수 있기 때문입니다.
  • 러스트 모듈 시스템은 내부의 세부 구현을 기본적으로 숨기도록 되어 있습니다. 이로써, 여러분은 외부 코드의 동작을 망가뜨릴 걱정 없이 수정할 수 있는 코드가 어느 부분인지 알 수 있죠. (당연히 공개되어 있지 않은 부분이겠지?) 그렇지만 러스트에서는 pub 키워드를 사용하여 자식 모듈의 내부 구성 요소를 공개 (public) 함으로써 외부의 상위 모듈로 노출할 방법을 제공합니다.

pub 키워드로 경로 노출하기 (공개하기?)

mod front_of_house {
    pub mod hosting {
        fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 절대 경로
    crate::front_of_house::hosting::add_to_waitlist();

    // 상대 경로
    front_of_house::hosting::add_to_waitlist();
}

→ 앞의 코드와 비교했을 때, hosting 모듈에 pub 키워드를 추가했습니다. 하지만 이 코드 또한 에러가 발생합니다. 왜 그런 걸까요?

hosting 모듈에 pub을 붙여서 모듈이 공개되었으므로, 이제 front_of_house 에 접근할 수 있다면 hosting 에도 접근할 수 있게 되었지만, hosting 모듈의 내용은 여전히 비공개입니다. 모듈을 공개했다고 해서 모듈의 내용까지 공개되지는 않습니다.

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

pub fn eat_at_restaurant() {
    // 절대 경로
    crate::front_of_house::hosting::add_to_waitlist();

    // 상대 경로
    front_of_house::hosting::add_to_waitlist();
}

→ 이렇게 add_to_waitlist 함수도 정의에 pub 키워드를 추가해서 공개해야 합니다. 이제서야 우리는 코드를 컴파일할 수 있게 되었습니다!

추가적으로,

  • 서로 형제 관계에 있을 때는(즉, 같은 모듈 상에 있을 때는) pub 키워드로 공개해주지 않아도 접근할 수 있습니다.
  • pub 키워드로 외부에 공개되었다면, “해당 대상의 부모에게 접근할 수 있을 때, 해당 대상에게도 접근할 수 있다” 라고 핵심을 정리할 수 있겠습니다.

super로 시작하는 상대 경로

  • 상대 경로에서, super 로 시작하면 현재 모듈 혹은 크레이트 루트 대신 자기 부모 모듈부터 시작되는 경로를 만들 수 있습니다.

구조체, 열거형을 공개하기

  • 구조체 정의에 pub를 쓰면 구조체는 공개되지만, 구조체의 필드는 비공개로 유지됩니다.
  • 반대로, 열거형은 공개로 지정할 경우 모든 배리언트가 공개됩니다. (열거형은 그 배리언트가 공개되지 않는다면 큰 쓸모가 없습니다.)

use 키워드로 경로를 스코프 안으로 가져오기

  • use 키워드를 한번 사용하여 어떤 경로의 단축경로 (shortcut) 를 만들 수 있고, 그러면 스코프 안쪽 어디서라도 짧은 이름을 사용할 수 있습니다.
  • 스코프에 use 키워드와 경로를 작성하는 건 파일 시스템에서 심볼릭 링크 (symbolic link) 를 생성하는 것과 유사합니다.
  • use 키워드로 가져온 경우도 다른 경로와 마찬가지로 비공개 규칙이 적용됩니다.
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {}
    }
}

use crate::front_of_house::hosting;

pub fn eat_at_restaurant() {
    hosting::add_to_waitlist();
}

eat_at_restaurant 함수 내에서 add_to_waitlist 함수를 hosting::add_to_waitlist 경로만으로 호출하는 예제입니다.

  • use가 사용된 특정한 스코프에서만 단축경로가 만들어진다는 점을 주의하세요. 
    → 다른 스코프에서는 use 구문으로 만들어진 단축 경로가 적용되지 않습니다.

읽어주셔서 감사합니다 🙇‍♂️

References

https://doc.rust-kr.org/ch07-00-managing-growing-projects-with-packages-crates-and-modules.html

profile
안녕하세요:)

0개의 댓글