Rust 모듈화 환경 탐색 및 효율적인 프로젝트 관리
Wenhao Wang
Dev Intern · Leapcell

소개
현대 소프트웨어 개발의 활기찬 생태계에서 강력하고 확장 가능한 애플리케이션은 거의 단일 코드 블록으로 만들어지지 않습니다. 대신, 작고 상호 연결된 구성 요소들로 세심하게 제작됩니다. 프로젝트가 복잡해짐에 따라 이러한 구성 요소를 구성, 재사용 및 관리하는 능력은 매우 중요해집니다. 성능, 안전성 및 동시성으로 유명한 Rust는 이를 위한 정교하면서도 직관적인 시스템을 제공합니다. 바로 모듈 시스템과 패키지 관리자입니다. mod
, use
, super
가 Cargo Workspaces와 함께 작동하는 방식을 이해하는 것은 단순한 학문적인 연습이 아니라, 유지보수 가능하고 협업적이며 성공적인 Rust 애플리케이션을 작성하는 데 필수적입니다. 이 글은 이러한 필수 도구들을 깊이 파고들어 원칙과 실제 적용 사례를 조명하며, 기본적인 코드 구성부터 대규모 멀티 패키지 프로젝트 관리까지 안내할 것입니다.
핵심 개념 및 원칙
세부 사항에 들어가기 전에 Rust의 모듈성과 프로젝트 구조를 뒷받침하는 핵심 개념에 대한 공통된 이해를 확립해 봅시다.
- Crate: Rust에서 가장 작은 컴파일 가능한 단위입니다. Crate는 실행 가능한 프로그램인 바이너리 또는 재사용을 위한 코드 모음인 라이브러리일 수 있습니다. 각 Rust 프로젝트는 일반적으로 단일 Crate로 컴파일됩니다.
- Module (
mod
): 모듈은 Rust가 Crate 내에서 코드를 구성하는 방법입니다. 이름 충돌을 방지하고 항목(함수, struct, enum 등)의 가시성을 제어하기 위해 네임스페이스를 만듭니다. 모듈은 중첩될 수 있습니다. - Path: 경로는 모듈 트리 내에서 항목을 참조하는 방법입니다. 경로는 절대 경로(Crate 루트부터 시작,
crate::
) 또는 상대 경로(현재 모듈부터 시작,self::
또는 상위 모듈부터 시작,super::
)일 수 있습니다. use
키워드:use
키워드는 경로를 범위 내로 가져와 전체 경로 대신 짧은 이름으로 항목을 참조할 수 있도록 합니다.- Public/Private 가시성 (
pub
): 기본적으로 Rust의 모든 항목은 포함하는 모듈에 대해 비공개입니다.pub
키워드는 항목을 공개하여 부모 또는 형제 모듈에서 액세스할 수 있도록 합니다(배치 및pub
키워드 사용에 따라 다름). - Package: 패키지는 기능 집합을 제공하는 하나 이상의 Crate입니다. 패키지에는 해당 Crate를 빌드하는 방법을 설명하는
Cargo.toml
파일이 포함됩니다. - Cargo Workspaces: 워크스페이스는 루트의 단일
Cargo.toml
로 관리되는 모든 패키지 집합입니다. 워크스페이스는 서로 의존할 수 있는 관련 패키지에 대한 개발을 촉진하도록 설계되었습니다.
모듈 시스템: mod
, use
, super
Rust의 모듈 시스템은 트리 구조입니다. 이 트리의 루트는 우리 Crate입니다. Crate 내에서 mod
키워드를 사용하여 모듈을 정의합니다.
모듈 및 가시성 정의
간단한 예제로 시작해 봅시다:
// src/main.rs (Crate 루트) mod utilities { // 'utilities'라는 모듈을 정의합니다. pub fn greet() { // 이 함수는 'utilities' 모듈 내에서 공개됩니다. println!("Hello from utilities!"); helper(); // 같은 모듈 내의 비공개 항목을 호출할 수 있습니다. } fn helper() { // 이 함수는 기본적으로 비공개입니다. println!("This is a private helper."); } pub mod math { // 중첩된 공개 모듈 'math' pub fn add(a: i32, b: i32) -> i32 { // 'math' 내의 공개 함수 a + b } } } fn main() { // 전체 경로를 통해 항목에 액세스 utilities::greet(); // utilities::helper(); // 오류: `helper`는 비공개입니다. let sum = utilities::math::add(5, 3); println!("Sum: {}", sum); }
이 예제에서:
mod utilities
는 모듈을 만듭니다.pub fn greet()
는greet
를utilities
외부 모듈에서 액세스할 수 있도록 합니다.fn helper()
는utilities
에 대해 비공개이며utilities
(또는 해당 하위 모듈) 내에서만 호출될 수 있습니다.pub mod math
는math
모듈을 공개하여utilities
외부에서 해당 내용을 액세스할 수 있도록 합니다.pub fn add
는add
를math
내에서 공개합니다.
math
가 pub
이 아니면 main
에서 utilities::math::add
에 액세스할 수 없습니다.
use
를 사용한 경로 범위 내로 가져오기
긴 절대 경로를 입력하는 것은 지루할 수 있습니다. use
키워드는 항목을 현재 범위로 가져와 이를 돕습니다.
// src/main.rs mod utilities { pub fn greet() { println!("Hello from utilities!"); } pub mod math { pub fn add(a: i32, b: i32) -> i32 { a + b } pub fn subtract(a: i32, b: i32) -> i32 { a - b } } } use utilities::greet; // `greet`를 범위 내로 가져옵니다. use utilities::math::add; // `add`를 범위 내로 가져옵니다. use utilities::math::{self, subtract}; // `math`와 `subtract`를 범위 내로 가져옵니다. // 위는 다음과 동등합니다: // use utilities::math; // use utilities::math::subtract; fn main() { greet(); // 이제 `greet`를 직접 호출할 수 있습니다. let sum = add(10, 7); // 그리고 `add`를 직접 호출할 수 있습니다. println!("Sum: {}", sum); let diff = math::subtract(10, 7); // `math`도 범위 내에 있으므로 `math::subtract`를 사용할 수 있습니다. println!("Difference: {}", diff); }
use
키워드는 코드를 더 간결하고 읽기 쉽게 만듭니다. 또한 crate
루트에서 절대 경로를 참조하려면 use crate::path::to::item;
을 사용하거나 현재 모듈에서 상대 경로를 참조하려면 use self::path::to::item;
을 사용할 수 있습니다.
super
를 사용하여 상위 모듈 참조
super
키워드는 상위 모듈의 항목에 액세스하는 데 사용됩니다. 이는 깊게 중첩된 모듈이 있을 때 특히 유용합니다.
// src/main.rs mod client { pub mod network { pub fn connect() { println!("Connecting to network..."); } pub mod messages { use super::connect; // `super`는 `network` 모듈을 참조합니다. // 이는 `use crate::client::network::connect;`와 동등합니다. pub fn send_message() { connect(); // 상위 모듈에서 connect를 호출합니다. println!("Sending message!"); } } } pub fn start_client() { // `network`가 공개되어 있다면 `messages`에 직접 액세스할 수 있습니다. network::messages::send_message(); } } fn main() { client::start_client(); }
여기서 use super::connect;
는 messages
모듈의 send_message
함수가 전체 경로 없이 상위 모듈인 network
에 정의된 connect
함수를 직접 호출할 수 있도록 합니다.
파일 및 모듈
모듈이 커지면 Rust는 이를 별도의 파일로 분할할 수 있습니다.
src/main.rs
에 mod utilities;
가 있다면 Cargo는 src/utilities.rs
또는 src/utilities/mod.rs
라는 파일을 예상합니다.
예시:
// src/main.rs mod geometry; // 'geometry' 모듈을 선언하고, 내용은 src/geometry.rs 또는 src/geometry/mod.rs에 있습니다. fn main() { geometry::shapes::circle_area(5.0); }
// src/geometry.rs (또는 src/geometry/mod.rs) pub mod shapes { pub fn circle_area(radius: f64) -> f64 { std::f64::consts::PI * radius * radius } pub fn square_area(side: f64) -> f64 { side * side } }
이 구조는 개별 파일을 관리하기 쉽고 집중적으로 유지하는 데 도움이 됩니다.
Cargo Workspaces: 여러 패키지 관리
프로젝트가 성장함에 따라 서로 의존하는 여러 관련 Crate가 필요할 수 있습니다. 예를 들어, 웹 애플리케이션에는 server
Crate, cli
Crate 및 shared_types
라이브러리 Crate가 있을 수 있습니다. Cargo Workspaces는 이러한 상호 의존적인 패키지를 관리하는 우아한 솔루션을 제공합니다.
워크스페이스 설정
워크스페이스는 루트의 Cargo.toml
파일로 정의되며, 이 파일에는 [workspace]
섹션이 포함됩니다.
워크스페이스를 만들어 봅시다:
-
워크스페이스 디렉토리 및 해당
Cargo.toml
생성:mkdir my_project_workspace cd my_project_workspace touch Cargo.toml # 워크스페이스 Cargo.toml 생성
-
my_project_workspace/Cargo.toml
편집:# my_project_workspace/Cargo.toml [workspace] members = [ "libs/utils", "apps/server", "apps/cli", ]
members
배열은 워크스페이스 내의 멤버 패키지(Crate) 경로를 나열합니다. -
멤버 패키지 생성:
mkdir libs cd libs cargo new utils --lib cd .. mkdir apps cd apps cargo new server cargo new cli cd ..
이제 프로젝트 구조는 다음과 같아야 합니다:
my_project_workspace/ ├── Cargo.toml # 워크스페이스 루트 Cargo.toml ├── apps/ │ ├── cli/ │ │ ├── Cargo.toml │ │ └── src/main.rs │ └── server/ │ ├── Cargo.toml │ └── src/main.rs └── libs/ └── utils/ ├── Cargo.toml └── src/lib.rs
워크스페이스 내 패키지 간 종속성
워크스페이스의 주요 이점 중 하나는 멤버 간 종속성을 쉽게 관리하는 것입니다. 멤버 Crate는 path
종속성을 지정하여 다른 멤버 멤버 Crate에 의존할 수 있습니다.
server
와 cli
가 utils
라이브러리에 의존하게 해 봅시다.
# my_project_workspace/apps/server/Cargo.toml [package] name = "server" version = "0.1.0" edition = "2021" [dependencies] utils = { path = "../../libs/utils" } # server의 Cargo.toml에서 utils 패키지까지의 상대 경로
# my_project_workspace/apps/cli/Cargo.toml [package] name = "cli" version = "0.1.0" edition = "2021" [dependencies] utils = { path = "../../libs/utils" }
이제 두 server
와 cli
에서 utils
라이브러리를 사용해 봅시다.
// my_project_workspace/libs/utils/src/lib.rs pub fn greet_user(name: &str) -> String { format!("Hello, {}! Welcome to our application.", name) }
// my_project_workspace/apps/server/src/main.rs use utils::greet_user; // utils Crate 사용 fn main() { println!("Server starting up..."); let message = greet_user("Server User"); println!("{}", message); println!("Server gracefully shutting down."); }
// my_project_workspace/apps/cli/src/main.rs use utils::greet_user; fn main() { println!("CLI application running..."); let message = greet_user("CLI User"); println!("{}", message); }
워크스페이스에서의 Cargo 명령
워크스페이스 루트(예: my_project_workspace/
)에서 Cargo 명령을 실행하면 전체 워크스페이스에 대해 작동합니다.
cargo build
: 모든 멤버 Crate를 빌드합니다.cargo test
: 모든 멤버 Crate의 테스트를 실행합니다.cargo fmt
: 모든 멤버 Crate를 포맷합니다.cargo run -p server
:server
바이너리 Crate를 실행합니다.-p
플래그는 실행할 패키지를 지정합니다.cargo run -p cli
:cli
바이너리 Crate를 실행합니다.
워크스페이스는 다음과 같이 개발을 크게 간소화합니다:
- 중앙 집중식 종속성 관리: 일반적인 종속성은 워크스페이스 수준에서 관리될 수 있으며, 중복 다운로드 및 빌드를 피할 수 있습니다.
- 일관된 빌드: 모든 Crate가 함께 빌드 및 테스트되어 호환성을 보장합니다.
- 간소화된 코드 탐색: IDE는 전체 프로젝트 구조를 더 잘 이해할 수 있습니다.
- 용이한 리팩토링: 공유 라이브러리의 변경 사항은 워크스페이스 내의 종속된 Crate에 즉시 반영됩니다.
결론
mod
, use
, super
키워드를 포함하는 Rust의 모듈 시스템은 단일 Crate 내에서 코드를 구성할 수 있는 강력하고 유연한 방법을 제공하며, 명확성을 보장하고 이름 충돌을 방지하며 가시성을 제어합니다. 이를 보완하여 Cargo Workspaces는 여러 상호 의존적인 패키지를 관리하고, 모듈성을 촉진하며, 빌드를 단순화하고, 대규모 Rust 프로젝트의 유지보수성과 확장성을 크게 향상시키는 필수적인 메커니즘을 제공합니다. 이러한 도구를 마스터하는 것은 잘 구조화되고, 협업적이며, 효율적인 Rust 애플리케이션을 작성하는 데 핵심입니다.