프로그램이 커지면 코드를 나누고, 외부 라이브러리를 가져다 쓰고, 여러 프로젝트를 함께 관리해야 합니다. 이 장에서는 Rust의 모듈 시스템과 Cargo의 패키지 관리, 워크스페이스, 기능 플래그(features)를 다룹니다.
용어 정리
실행 크레이트의 뿌리는 src/main.rs, 라이브러리 크레이트의 뿌리는 src/lib.rs입니다. crates.io에 올라가 공유되는 라이브러리도 "크레이트"라고 부릅니다.
모듈과 공개 범위
mod로 모듈을 만듭니다. 모듈 안의 항목은 기본적으로 비공개이며, 밖에서 쓰려면 pub을 붙여야 합니다. 구조체는 pub struct로 공개해도 필드는 따로 pub을 붙여야 공개됩니다. use는 긴 경로를 짧은 이름으로 가져옵니다.
mod geometry {
pub mod shapes {
pub struct Circle {
pub radius: f64,
}
impl Circle {
pub fn area(&self) -> f64 {
std::f64::consts::PI * self.radius * self.radius
}
}
}
pub fn double(x: f64) -> f64 {
helper(x) * 2.0
}
// pub 이 없으므로 geometry 모듈 안에서만 보입니다
fn helper(x: f64) -> f64 {
x
}
}
use geometry::shapes::Circle;
fn main() {
let c = Circle { radius: 2.0 };
println!("{:.2}", c.area());
println!("{}", geometry::double(1.5));
}
경로는 크레이트 뿌리에서 시작하는 crate::, 현재 모듈 기준 self::, 부모 모듈 기준 super::로 씁니다. 공개 범위를 좁히고 싶으면 pub(crate)(이 크레이트 안에서만)나 pub(super)(부모 모듈까지만)를 씁니다.
모듈을 파일로 나누기
mod config;처럼 본문 없이 선언하면 Cargo는 src/config.rs(또는 src/config/mod.rs)에서 내용을 찾습니다. 하위 모듈은 src/config/ 폴더에 둡니다.
// src/main.rs
mod config;
use config::Config;
fn main() {
let cfg = Config::new();
println!("port = {}", cfg.port);
}
// src/config.rs
pub struct Config {
pub port: u16,
}
impl Config {
pub fn new() -> Self {
Config { port: 8080 }
}
}
use 문은 여러 항목을 한 번에 가져오거나(use std::collections::{HashMap, HashSet};), 이름을 바꾸거나(use std::io::Result as IoResult;), 다시 내보낼 수 있습니다. 라이브러리에서 pub use로 내부 경로를 다시 내보내면 사용자는 깊은 경로를 몰라도 됩니다.
crates.io 의존성
공개 라이브러리는 crates.io에 모여 있고, 문서는 docs.rs에서 볼 수 있습니다. 의존성은 cargo add로 추가하는 것이 가장 쉽습니다.
cargo add serde --features derive
cargo add serde_json
cargo add --dev pretty_assertions
cargo update # Cargo.lock 의 버전을 호환 범위 안에서 올립니다
명령을 실행하면 Cargo.toml에 다음과 같이 기록됩니다. 버전 "1"은 SemVer 호환 범위(1.x)를 뜻하며, 실제로 고른 정확한 버전은 Cargo.lock에 고정됩니다.
[dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[dev-dependencies]
pretty_assertions = "1"
이제 코드에서 크레이트 이름으로 바로 쓸 수 있습니다. 이름에 하이픈이 있으면 코드에서는 밑줄로 바꿔 씁니다.
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug)]
struct User {
name: String,
age: u32,
}
fn main() -> Result<(), serde_json::Error> {
let user = User { name: "kim".to_string(), age: 30 };
let json = serde_json::to_string(&user)?;
println!("{json}"); // {"name":"kim","age":30}
let back: User = serde_json::from_str(&json)?;
println!("{back:?}");
Ok(())
}
워크스페이스
여러 패키지를 한 저장소에서 함께 개발할 때는 워크스페이스를 씁니다. 멤버들은 Cargo.lock과 target/ 폴더를 공유하므로 빌드가 빠르고 버전이 어긋나지 않습니다.
# 루트 Cargo.toml
[workspace]
members = ["app", "shared"]
resolver = "2"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
# app/Cargo.toml
[dependencies]
shared = { path = "../shared" }
serde = { workspace = true }
[workspace.dependencies]에 버전을 한 번만 적고 멤버에서는 workspace = true로 물려받습니다. 특정 멤버만 빌드하거나 실행하려면 cargo build -p app, cargo run -p app처럼 -p를 붙입니다. 루트에 패키지가 없는 워크스페이스는 resolver를 직접 적는 것이 좋습니다(2021 에디션은 "2", 2024 에디션은 "3").
기능 플래그(features)
features는 선택적으로 켜고 끌 수 있는 컴파일 옵션입니다. 무거운 의존성을 필요할 때만 포함하게 할 때 주로 씁니다.
[features]
default = ["json"]
json = ["dep:serde_json"]
[dependencies]
serde_json = { version = "1", optional = true }
코드에서는 #[cfg(feature = "json")]로 해당 기능이 켜졌을 때만 컴파일할 부분을 표시합니다. 빌드할 때 cargo build --no-default-features로 기본 기능을 끄거나 cargo build --features json으로 켭니다.
정리
- 패키지는 크레이트를 담고, 크레이트는 모듈로 나뉩니다. 모듈 항목은 기본이 비공개입니다.
mod 이름;은 같은 이름의 파일을 불러오며, use로 경로를 짧게 씁니다.- 의존성은
cargo add로 추가하고, 정확한 버전은 Cargo.lock에 고정됩니다. - 여러 패키지는 워크스페이스로, 선택 기능은 features로 관리합니다.
댓글 0개
로그인 · 로그인하면 댓글을 남길 수 있습니다.