Rust 공식 문서를 참고한 글입니다.
우리는 지금까지 하나의 파일에서 하나의 모듈 안에 프로그램을 작성했다. 만약에 프로그램이 더 커지면 여러 개의 모듈, 여러 개의 파일들로 분리해야 할 것이다. 하나의 패키지는 여러 개의 바이너리 크레이트를 포함할 수 있고 필요에 따라서 하나의 라이브러리 크레이트를 포함할 수 있다. 약간 어려운 감이 있지만 패키지 관리에 필요한 개념들을 알아보도록 하자. 우리가 앞으로 배울 모듈 시스템에는 아래와 같은 개념이 포함된다.
- Packages : 크레이트를 빌드, 테스트, 공유할 수 있는 기능
- Crates : 라이브러리나 실행 파일을 생성하는 모듈 트리
- Modules과 use : 구조, 범위, 경로의 접근성 제어
- Paths : 구조체, 함수, 모듈 등 이름을 결정하는 방식
1. 패키지와 크레이트
패키지는 하나 이상의 크레이트를 구성하는 번들이고 Cargo.toml 파일으 포함한다. 크레이트는 러스트 컴파일러가 한번에 인지하는 가장 작은 단위의 코드를 말한다. cargo 대신에 rustc를 사용해서 싱글 코드는 컴파일한다고 하면, 컴파일러는 파일을 크레이트로 분리해 인지한다. 이런 크레이트는 바이너리 크레이트나 라이브러리 크레이트로 모듈로 이루어져 있다. 컴파일러가 컴파일을 시작하는 소스 파일을 크레이트 루트라고 한다.
새로운 프로젝트를 생성해서 직접 확인해보자.
src/main.rs은 바이너리 크레이트의 크레이트 루트라는 것이 약속되어 있다. 그래서 따로 명시하지 않아도 main.rs가 루트로 사용된다. 마찬가지로 라이브러리 크레이트인 경우, src/lib.rs가 존재하면 라이브러리 크레이트의 크레이트 루트가 된다. 여러 개의 바이너리 크레이트를 만들고 싶다면 src/bin에 작성하면 별개의 바이너리 크레이트로 사용할 수 있다.
2. 모듈과 범위
모듈과 경로에 대한 세부 내용으로 들어가기 전에, 컴파일러에서 모듈과 경로, use 키워드, pub 키워드가 어떻게 작동하는지 알아보자. 이 내용이 앞으로의 내용의 전체 과정이다.
크레이트를 컴파일할 때 컴파일러는 크레이트 루트 파일에서 컴파일할 코드를 찾는다. 예를 들어서 mod garden으로 garden 모듈을 선언한다고 할 때, 컴파일러는 모듈의 코드를 찾기 시작한다. 선언 이후의 코드, src/garden.rs, src/garden/mod.rs에서 모듈을 찾는다. 이때 크레이트 루트 이외의 파일에서 서브 모듈을 생성하면 그것 또한 같은 방식으로 찾는다. 크레이트의 일부가 변경되면 경로를 통해 같은 크레이트 내에서 모듈의 코드를 참조할 수 있다. 예를 들어서, garden 모듈의 서브 모듈인 vegetables 안에 Asparagus 타입은 crate::garden::vegetables::Asparagus로 찾을 수 있다.
보통 모듈 안 코드는 기본적으로 부모 모듈로부터 비공개다. 모듈을 공개로 바꾸려면 mod가 아닌 pub mod로 선언하면 된다. 특정 범위 안에서 use 키워드는 긴 경로의 반복을 줄이기 위해서 경로를 단축한다.
main에서 use 키워드를 사용해 축약 주소를 작성하고, graden을 public으로 가져온다.
// main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {plant:?}!");
}
garden에서도 vegetables를 public으로 가져온다.
// garden.rs
pub mod vegetables;
fn ...
// vegetables.rs
#[derive(Debug)]
pub struct Asparagus {}
이런 방식으로 하면 crate::garden::vegetables::Asparagus를 참조할 때, use crate::garden::vegetables::Asparagus;를 사용하면 해당 범위 내에서는 Asparagus만 작성해도 타입을 사용할 수 있다.
모듈 내 코드 그룹화
모듈은 크레이트 내부에서 코드를 그룹화한다. 또 앞서 언급했듯, 모듈 내부의 코드는 기본적으로 비공개이다. 모듈을 사용하면 공개 여부를 제어할 수 있다. 레스토랑의 기능이 있는 라이브러리 크레이트를 생성해서 예제를 작성해보자.
cargo new 를 통해 프로젝트를 생성하면 하나의 바이너리 크레이트를 가진 패키지를 생성한다. 라이브러리 크레이트를 하나 가진 패키지를 생성하려면 --lib 옵션을 추가하면 된다.
// 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, 크레이트의 루트는 main.rs 혹은 lib.rs가 해당된다.
3. 경로
이러한 모듈 트리에서 코드를 찾는 방법은 파일 시스템에서 경로를 사용하는 것과 같다.
- 절대 경로 : 크레이트 루트로 시작되는 전체 경로, crate로 시작
- 상대 경로 : 현 모듈을 시작점으로 self, super 혹은 현재 모듈 내 식별자를 이용
절대 경로, 상대 경로 뒤에는 ::로 구분된 식별자가 하나 이상 따라온다.
위의 레스토랑 예시를 이어서 설명해보겠다. eat_at_restaurant에서 처음에는 절대경로로 add_to_waitinglist()에 접근하고 두번째는 상대 경로로 접근했다. 이를 실행시키면 오류가 발생한다.
// lib.rs
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로 설정되어 있는데 우리가 외부에서 접근하려 했던 것이다. 모듈 자체도 pub로 정의하고 모듈 내 함수도 pub로 정의해야 접근할 수 있다.
pub 키워드
// lib.rs
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();
}
상대 경로 중에는 부모 모듈을 나타내는 super로 시작하는 경우도 있다. 만약에 잘못된 주문을 받은 경우, 요리 후 직접 가져다준다고 가정해보자. 이때 음식을 가져다주는 행위는 front_of_house 모듈에 정의되어 있고, 요리사의 작업은 back_of_house 모듈에 정의되어 있다고 하자.
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();
}
fix_incorrect_order() 함수는 back_of_house 모듈 밖에 있는 함수를 접근하기 때문에 super를 사용한다. 이후에 모듈 트리를 재구성하게 된다해도 이 모듈들끼리의 상대적 경로가 달라지지 않으면 코드 수정을 최소화할 수 있다.
함수와 모듈 뿐만 아니라, 구조체나 열거형도 pub을 사용해 외부에서 접근하도록 만들수 있다. 이때 구조체 정의에 pub을 사용하면 구조체 자체는 공개되지만 구조체 내부 필드들은 비공개로 유지된다. 공개 여부는 각 필드마다 설정할 수 있다.
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("Wheat");
println!("I'd like {} toast please", meal.toast);
}
이렇게 사용하면 toast 필드는 공개 필드라서 읽고 쓸 수 있지만 seasonal_fruit 필드는 비공개 필드다. Breakfast 비공개 필드를 가지기 때문에 Breakfast 인스턴스를 생성할 공개 연관 함수를 제공해야 한다. 연관함수가 존재하지 않으면 eat_at_restaurant 함수에서 Breakfast 인스턴스를 생성할 수 없다. 반면, 열거형의 경우 열거형만 공개하면 안에 있는 값들은 자동으로 공개되기 때문에 열거형 앞에 pub만 작성하면 된다.
use 키워드
우리가 작성한 코드를 보면 모듈이 생길수록 반복적이고 불편하다. 절대 경로를 사용하든 상대 경로를 사용하든, add_to_waitlist 호출할 때마다 front_of_house, hosting 모듈을 매번 지정해줘야 한다. 그러나 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();
}
이때 add_to_waitlist 함수까지 경로를 전체 작성하지 않고 호출하는 데, 이는 보편적인 방식이다. 반면에 구조체나 열거형 등을 가져올 때는 전체 경로를 작성하는게 보편적이다. 만약 서로 다른 부모 모듈에서 같은 이름을 가진 타입을 가져온다면, 부모 모듈을 명시해 구분해줘야 한다. 아니면 경로 뒤에 as 라는 키워드를 사용해서 별칭을 지정해서 구분해도 된다.
pub와 use를 결합하면 해당 스코프로 가져온 것을 다시 내보낼 수 있다. 즉, 현재 스코프 밖에서도 pub use가 사용된 위치에서 정의된 것처럼 사용할 수 있다. 이 기법은 크레이트 구조는 관리하기 좋은 상태로 두고 외부에서 접근하기 좋은 형태로 만든다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
만약에 동일한 모듈이나 크레이트에서 정의된 것을 여러개 사용하면 생략해서 사용할 수 있다.
// 기존
use std::cmp::Ordering;
use std::io;
// 변경
use std::{cmp::Ordering, io};
모듈 분리
지금까지의 모든 예제들은 하나의 파일에서 여러 개의 모듈을 정의했다. 이제 각각을 다른 파일로 분리해보자.
// 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);
}
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
fn seat_at_table() {}
// src/front_of_house/serving.rs
fn take_order() {}
pub fn serve_order() {}
fn take_payment() {}
// src/front_of_house/mod.rs
pub mod hosting;
pub mod serving;
// 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() {}
'언어 > Rust' 카테고리의 다른 글
[Rust] 8. 컬렉션 (0) | 2024.10.09 |
---|---|
[Rust] 6. 열거형과 패턴 매칭 (8) | 2024.10.09 |
[Rust] 5-2. 메서드 (0) | 2024.10.09 |
[Rust] 5-1. 구조체 사용 (0) | 2024.10.09 |
[Rust] 4-3. Slice Type (0) | 2024.09.28 |