안녕하세요, 단테입니다.
어느덧 7강입니다.
지금까지 공부하시느라 수고 많으셨습니다.
우리 좀만 더 달려봅시다.
규모가 큰 프로그램일수록 코드를 구조화 하고 나누는 작업은 필수 불가결한 작업이 되기 때문에
모듈화는 언어 사용에 있어 하나의 주요 기능으로 소개될 만큼 잘 알고 있어야 합니다.
여러분은 코드를 작성할 때 가장 중요하게 여기는 부분이 무엇인가요?
에러가 작은 코드? 경량화된 코드?
여러 관점에서 대단히 다양한 답변이 나올 수 있겠습니다만, 내가 작성하는 코드의 구조가 과거 / 현재 / 미래에 걸쳐서 어느 시점에서 보아도 변경이 용이한지의 여부가 제가 생각하는 가장 중요한 부분입니다.
멀티 패러다임 언어이든, 그렇지 않든 언어로 작성된 프로그램의 사이즈가 커질수록 객체지향에서 비롯된 다양한 패턴들을 담습하거나 모방할 수밖에 없게 되기 때문에 엔터프라이즈 프로그램을 작성하는데 있어서 변경 용이성은 꼭 달성해야 할 과제입니다.
코드를 잘게 나누어 다른 파일에 배치하고 적절한 디렉토리에 저장하는 것만으로도 모듈화의 많은 부분을 달성할 수 있습니다.
우리가 작성하는 하나의 패키지는 많은 바이너리 crates와 또 다른 library crate를 가지고 있습니다.
이 패키지가 커질수록 더 작은 create로 잘게 나누는 작업이 필요할 것입니다.
앞서 등장했던 crate(크레이트)는 러스트 컴파일러가 한번에 해석하는 러스트 코드의 단일 집합체입니다.
다른 프로그래밍 언어에서의 모듈이나 패키지와 동일한 개념으로 하나의 크레이트는 여러 모듈을 가지고 있을 수 있습니다.
이런 크레이트의 버전은 Cargo.toml
파일에 명시하여 의존성을 관리할 수 있습니다. 마치 자바스크립트의 package.json
처럼요.
러스트 생태계에는 npm registry와 같은 crates.io
라는 라이브러리 저장소가 있습니다.
러스트에서 crate는 고유의 이름으로 식별되며 새로운 crate를 생성할 때는 cargo new
명령어를 사용합니다. my_crate
라는 이름의 라이브러리를 만들고 싶다면 다음처럼 해주면 되겠습니다.
cargo new my_crate --lib
하나의 crate는 여러 모듈의 집합소이고 이 crate를 사용해 모듈화된 코드를 재사용할 수 있습니다.
바이너리 크레이트는 CLI 나 서버에서 컴파일하고 실행할 수 있는 프로그램입니다. 각 크레이트에는 main
함수를 정의해야하며 여태까지 봤었던 코드들은 모두 binray crate입니다.
library crate는 이와 반대로 main 함수가 없습니다. 그리고 실행가능하게 컴파일되지 않습니다. 그저 다른 프로젝트에서 공유되어 사용될 수 있게 기능을 정의해둡니다.
crate root는 러스트 컴파일러가 crate의 root module을 만들때 가장 먼저 해석을 시작하는 파일입니다.
패키지는 하나 이상의 크레이트가 모여 기능을 제공하는 집합입니다. 이 패키지는 Cargo.toml 파일을 가지고 있으며 이 파일에서 어떻게 crate를 빌드해야 하는지 결정합니다. Cargo 또한 CLI 환경에서 구동가능하게 만들어진 패키지로 binary crate도 있고 라이브러리 crate도 있습니다.
패키지는 여러 바이너리 크레이트를 가질 수 있지만, 라이브러리 크레이트는 최대 한개만 가질 수 있습니다.
그리고 종류와 상관없이 최소 한개 이상의 crate를 가지고 있어야 합니다.
$ cargo new my-project
Created binary (application) `my-project` package
$ ls my-project
Cargo.toml
src
$ ls my-project/src
main.rs
위처럼 새로운 패키지를 만든 후에 파일 경로를 보면 Cargo.toml이 있습니다. 별도의 엔트리 포인트를 설정해주지 않는다면 src/main.rs
를 바이너리 crate의 crate root로 간주합니다. 또한 src/lib.rs
가 있다면 이 파일을 라이브러리 crate의 root로 간주합니다. Cargo는 crate root 파일을 rustc
를 이용해 라이브러리나 바이너리로 빌드합니다.
src/main.rs
, src/lib.rs
가 같이 패키지 내부에 있다면 두 개의 crate가 생성됩니다.
패키지의 src/bin 폴더에 여러 파일을 만듬으로 여러 binary crate를 가질 수 있습니다.
러스트에서는 use
keyword 사용해 특정 항목을 스코프로 가져올 수 있습니다.
crate 를 컴파일 할 때 컴파일러는 crate root
파일을 살펴봅니다. `
src/lib.rs
sr/main.rs
crate root파일에 모듈을 선언해봅시다.
mod
키워드를 사용하며 컴파일러는 다음의 경로를 같이 살펴봅니다.mod garden
으로 정의된 중괄호 내부러스트에서 crate root가 아니라면 어떤 곳에서도 submodule을 선언할 수 있습니다.
러스트 컴파일러는 parent 모듈의 이름이 있는 디렉토리 아래의 다음 이름에서 서브 모듈을 찾습니다.
parent 모듈은 parent_module, 서브 모듈은 submodule
이라는 이름을 가지고 있다고 합시다.
mode submodule {}
내부에 선언된 코드들// parent_module
mod submodule {
fn my_module() {
println!("this is my_module function");
}
}
// parent_module
mod submodule
// submodule.rs
fn my_module() {
println!("this is my_module function");
}
// parent_module
mod submodule
// submodule/mod.rs
fn my_module() {
println!("this is my_module function");
}
두번째 방법으로 작성해봅시다.
parent_module
에 대한 서브모듈 submodule
을 정의한다고 가정하면
submodule.rs
파일을 작성합니다.
// submodule.rs
fn submodule_function() {
// submodule function code goes here
}
그리고 parent module에서 mod
키워드를 사용해 서브모듈이 정의된 파일의 path를 명시합니다.
// parent_module.rs
mod submodule;
fn parent_function() {
// parent function code goes here
}
이를 메인 함수에서 사용하기 위해 다음 처럼 ::
double colon을 사용해 참조합니다.
use parent_module::submodule;
fn main() {
submodule::submodule_function();
}
submodule에 정의되어있는 모든 정의들을 사용하고 싶다면 다음 처럼 *
를 사용해 스코프로 가져옵니다.
use parent_module::submodule::*;
fn main() {
submodule_function();
}
아래와 같은 backyard binary crate를 만들어봅시다.
backyard
├── Cargo.lock
├── Cargo.toml
└── src
├── garden
│ └── vegetables.rs
├── garden.rs
└── main.rs
// src/main.rs
use crate::garden::vegetables::Asparagus;
pub mod garden;
fn main() {
let plant = Asparagus {};
println!("I'm growing {:?}!", plant);
}
binary crate
이기 때문에 main.rs에 메인 함수를 선언해주어야 합니다.
use crate::garden::vegetables::Asparagus;
를 선언해 vegetables 서브 모듈의 Asparagus struct를 메인 함수의 스코프 내부에서 사용할 수 있게 합니다.
pub mod garden
은 컴파일러에게 모듈이 src/garden.rs에 있는 코드를 포함하라고 알려줍니다.
pub mod vegetables
garden.rs 파일에는 pub mod vegetables
를 선언해 서브 모듈이 선언된 src/garden/vegetables.rs
또한 포함되어야 한다는 사실을 말해줍니다.
pub struct Asparagus {}
러스트 모듈은 공개 범위를 설정할 수 있고 이를 privacy
라고 합니다.
privacy가 private
으로 ㅅ너언된 아이템들은 그들이 정의된 모듈 내부에서만 사용할 수 있습니다.
모듈에 선언된 각 코드들은 item
이라고 하며 이 item들은 별도의 추가 키워드와 함께 선언되지 않으면 기본적으로 private
으로 설정됩니다.
private 정의는 모듈 내부에 구현된 아이템이 실수로 외부에서 수정되지 않도록 도와줍니다.
public 아이템/모듈로 만들고 싶다면 pub mod
키워드를 사용해야 합니다. 이를 통해 외부 코드가 여러분이 작성한 코드를 의존하게 만들 수 있습니다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
pub fn seat_at_table() {}
}
}
예제에서 hosting
과 내부 아이템인 함수들은 pub
키워드를 통해 정의되었고 front_of_house
외부에서 참조될 수 있습니다.
front_of_house::hosting::function_name`
pub mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
fn seat_at_table() {}
}
}
위의 예제에서 add_to_waitlist
함수는 public privacy를 가지고 있고 seat_at_table
은 private privacy를 가지고 있습니다. seat_at_table
은 hosting
모듈 외부에서 사용될 수 없습니다.
src/main.rs
, src/lib.rs
를 crate root라고 부르는 이유는 각 crate root 파일들이 crate
라고 하는 모듈 구조를 만듭니다. 이 모듈 구조를 우리는 module tree
라고 합니다.
crate
└── front_of_house
├── hosting
│ ├── add_to_waitlist
│ └── seat_at_table
└── serving
├── take_order
├── serve_order
└── take_payment
위의 트리에서는 모듈이 서로 다른 서브 모듈을 어떻게 포함하는지 보여줍니다.
hosting 모듈은 front_of_house
모듈에 있습니다. hosting
모듈은 serving
모듈이라는 형제를 가지고 있습니다. hosting
모듈은 front_of_house
모듈의 child
입니다.
front_of_house
모듈은 hosting
모듈의 parent
입니다.
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 kitchen {
fn prepare_food() {}
fn cook_food() {}
fn clean_up() {}
}
위 예제에서 front_of_house
와 kitchen
은 top-level
모듈입니다.
hosting, serving
은 front_of_house
의 서브 모듈이며 각 모듈은 내부에 함수를 item으로 가지고 있습니다.
러스트가 참조되는 아이템을 모듈 트리 어디에서 찾는지 알아보겠습니다.
절대 경로는 crate root
에서 부터 시작합니다. crate name
으로 시작하며 crate::
로 선언되는 path는 absolute path입니다.
상대 경로는 현재 모듈로부터의 경로는 self
, super
를 사용함으로 사용할 수 있습니다.
// src/lib.rs
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
함수는 library crate의 public api 이기 때문에 pub
키워드로 선언이 되었습니다. front_of_house의 모듈 내부의 아이템인 add_to_waitlist
함수를 사용하기 위해 절대경로를 사용했습니다.
crate::front_of_house::hosting::add_to_waitlist();
여기서 crate::front_of_house
는 파일 디렉토리에서 /front_of_house
와 동일합니다.
add_to_waitlist
의 두번째 참조는 상대경로를 사용하는 경우입니다. 동일 레벨에서 선언된 모듈을 사용했습니다. 이는 front_of_house/hosting/add_to_waitlist
와 같이 참조하는 것과 동일합니다.
하지만 앞서 선언한 예제는 컴파일 시 에러가 발생합니다.
hosting 모듈은 상위 모듈에 대해 기본적으로 private 하기 때문에 아래와 같이 사용해야 합니다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {
super::private_example::private_list();
}
}
mod private_example {
pub fn private_list() {
}
}
}
pub fn eat_at_restaurant() {
crate::front_of_house::hosting::add_to_waitlist();
front_of_house::hosting::add_to_waitlist();
}
위 코드는 아래와 같이 컴파일 에러가 발생하지 않습니다. private_example이 private일지라도 add_to_waitlist에서 부모 모듈의 아이템인 private_example::private_list를 사용할 수 있습니다.
정의한 모듈을 매번 double colon ::
를 사용하는 것 보다 더 간편한 방법은 use 키워드를 사용하는 것입니다.
// src/lib.rs
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();
}
use crate::front_of_house::hosting
을 crate root에 선언함으로써 hosting은 이제 스코프 내부에서 사용할 수 있게 되었습니다.
use는 오직 특정 스코프 내부에만 단축 path를 사용할 수 있게 도와줍니다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting;
mod customer {
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
}
위 코드를 빌드할 시 다음과 같은 에러가 발생합니다.
error[E0433]: failed to resolve: use of undeclared crate or module `hosting`
--> src/lib.rs:11:9
|
11 | hosting::add_to_waitlist();
| ^^^^^^^ use of undeclared crate or module `hosting`
warning: unused import: `crate::front_of_house::hosting`
--> src/lib.rs:7:5
|
7 | use crate::front_of_house::hosting;
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= note: `#[warn(unused_imports)]` on by default
For more information about this error, try `rustc --explain E0433`.
warning: `playground` (lib) generated 1 warning
error: could not compile `playground` due to previous error; 1 warning emitted
이를 해결하기 위해서는 customer 모듈 내부에서 use 키워드를 사용하거나 super::hosting과 같이 parent module 참조를 사용해야 합니다.
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
바디에서 hosting::add_to_waitlist
와 같이 함수를 참조하기 위해 hosting까지만 use 키워드를 사용했습니다.
mod front_of_house {
pub mod hosting {
pub fn add_to_waitlist() {}
}
}
use crate::front_of_house::hosting::add_to_waitlist;
pub fn eat_at_restaurant() {
add_to_waitlist();
}
위의 예제에서는 eat_at_restaurant
에서 add_to_waitlist
와 같이 hosting::
를 생략하기 위해서 use 키워드로 hosting::add_to_waitlist까지 작성했습니다.
A 모듈에서 B 모듈의 함수를 사용할 때 상위 모듈을 참조하는 것이 로컬 함수를 사용하는 것과 명확히 구분되기 때문에 첫번째 예제 가
와 같이 사용하는 것이 좋습니다.
다른 상황을 보겠습니다.
다음처럼 structs, enums와 같은 item을 use 키워드를 사용해 가져올 때는 full path를 가져오는 것이 좋습니다.
use std::collections::HashMap;
fn main() {
let mut map = HashMap::new();
map.insert(1,2);
}
다른 모듈에서 선언된 동일한 이름의 아이템 Result를 사용하려면 상위 모듈의 path를 선언한 후 참조해서 사용해야 합니다.
use std::fmt;
use std::io;
fn function1() -> fmt::Result {
// --snip--
}
fn function2() -> io::Result<()> {
// --snip--
}
또 다른 해결방안은 as
키워드와 함께 사용하는 것입니다. 자바스크립트의 as가 떠오르죠?
use std::fmt::Result;
use std::io::Result as IoResult;
fn function1() -> Result {
// --snip--
}
fn function2() -> IoResult<()> {
// --snip--
}
// src/lib.rs
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();
}
외부 스코프에서 hosting을 사용하기 위해서는 restaurant::front_of_hoㅕse::hosting::add_to_waitlist()
로 호출해야 합니다.
이를 간편하게 하기 위해 pub use
키워드를 사용해 re-exporting 기법을 적용한다면 restaurant::hosting::add_to_waitlist()와 같이 줄여서 호출할 수 있습니다.
rand 의존성을 내부 패키지에서 사용하고 싶다면
Cargo.toml에 다음과 같이 버전과 함께 명시해야 합니다.
Cargo.toml
rand = "0.8.5"
use rand::Rng;
fn main() {
let secret_number = rand::thread_rng().gen_range(1..=100);
}
std
라이브러리도 위의 main 함수가 선언된 패키지에서는 외부 패키지입니다.
use std::collections::HashMap;
// --snip--
use std::cmp::Ordering;
use std::io;
// --snip--
use std::io;
use std::io::Write;
위 코드는 아래와 같이 한줄로 표현할 수 있습니다.
use std::{cmp::Ordering, io};
use std::io::{self, Write};
한 파일에 모두 작성되어 있는 모듈을 여러 파일에 나누어 분리해보겠습니다.
아래와 같이 crate root 내부에서 다른 파일에 선언된 front_of_house 모듈을 사용하기 위해서는 해당 모듈에 다른 파일에 작성해야 합니다.
// src/lib.rs
mod front_of_house;
pub use crate::front_of_house::hosting;
pub fn eat_at_restaurant() {
hosting::add_to_waitlist();
}
src/front_of_house.rs
에 작성합니다.
pub mod hosting {
pub fn add_to_waitlist() {}
}
mod는 다른 언어의 include, import와 같은 명령어가 아닙니다.
모듈 트리 내부에서 한번만 mod 명령어를 사용하면 컴파일러는 파일이 프로젝트 내부에 있는 것을 알게 됩니다.
hosting
모듈은 src/front_of_house/housing.rs
에 선언하겠습니다.
hosting 모듈을 옮기기에 앞서서 src/front_of_house.rs에 hosting 모듈을 선언합니다.
// src/front_of_house.rs
pub mod hosting;
// src/front_of_house/hosting.rs
pub fn add_to_waitlist() {}
hosting.rs를 src 디렉토리에 위치 시키면 컴파일러는 hosting.rs 코드가 hosting 모듈이 crate root에 선언된 모듈이라고 생각하지 front_of_house 모듈의 하위 모듈이라고 생각하지 않습니다. 특정 모듈의 하위 모듈은 해당 모듈의 이름으로 만들어진 디렉토리 하위 패스에 놓여야 합니다.
오늘 내용은 좀 길었죠?
러스트 언어로 프로그램을 작성하면서 꼭 알고 있어야 할 내용이므로 어쩔 수 없이 내용이 길어졌습니다.
여기까지 함께 공부하시느라 너무 수고하셨습니다.
다음 포스팅에서 뵙겠습니다.
메리크리스마스!