본문 바로가기

패키지와 크레이트

Cargo 패키지의 target이 크레이트 루트를 선택하고, 각 크레이트가 모듈 트리와 공개 경로를 만드는 구조를 구분합니다.

프로그램이 커지면 코드를 나누어 정리해야 합니다. 관련된 기능을 묶고 서로 다른 기능을 나누면, 특정 기능을 구현한 코드를 어디서 찾고 어디를 고쳐야 하는지 분명해집니다. 또 구현의 세부를 감추고 다른 코드가 쓸 수 있는 부분만 공개하면, 그 코드를 쓰는 쪽은 안의 구현을 몰라도 됩니다. 러스트에는 이런 일을 돕는 기능이 여럿 있으며, 이를 모아 모듈 시스템(module system)이라고 부릅니다. 6장에서는 패키지, 크레이트, 모듈, 경로를 차례로 다루며, 이 글에서는 먼저 패키지와 크레이트를 살펴봅니다.

아래 그림처럼 네 개념은 폴더의 단계가 아니라 서로 다른 단위입니다. Cargo의 설정 파일이 패키지와 그 안의 빌드 대상(target)을 정하고, 각 빌드 대상의 시작 파일에서 컴파일러가 모듈 트리를 만들며, 경로와 공개 범위가 그 트리 안의 아이템을 어디서 쓸 수 있는지 정합니다.

Cargo manifest가 package와 target을 설명하고, 각 target이 별도 crate root와 module tree를 만들며, path와 visibility가 아이템의 사용 경계를 정하는 네 단계 계약

Rust · PACKAGE TO PATH

manifest에서 공개 경로까지 경계를 한 단계씩 읽는다

Package, crate, module, path는 같은 이름의 폴더 단계가 아닙니다. Cargo가 target을 고르고, 각 target의 crate root에서 컴파일러의 module tree가 시작됩니다.

한 패키지의 라이브러리와 바이너리는 별도 크레이트다lesson-kit manifest가 두 target을 정한다. 5아이템노드5간선: package→library root, package→binary root는target구성, 각root→함수는정의, main→pubgreet는공개API호출이다. library/binary영역은별도crate경계이며하나의crate루트를공유하지않는다. src/lib.rs의greet를binary의crate::greet로찾는간선은없다.library targetbinary targetlesson-kitCargo.tomlpackage 하나src/lib.rslibrary crate root · lesson_kitpub fn greetname → String공개 함수src/main.rsbinary crate root · lesson-kitfn mainlesson_kit::greet("지수")정의정의API 호출
한 패키지의 라이브러리와 바이너리는 별도 크레이트다lesson-kit manifest가 두 target을 정한다. 5아이템노드5간선: package→library root, package→binary root는target구성, 각root→함수는정의, main→pubgreet는공개API호출이다. library/binary영역은별도crate경계이며하나의crate루트를공유하지않는다. 모바일의main호출은왼쪽외부레일을따라greet아래포트로들어간다. src/lib.rs의greet를binary의crate::greet로찾는간선은없다.library targetbinary targetlesson-kitCargo.tomlpackage 하나src/lib.rslibrary crate rootlesson_kitpub fn greetname → String공개 함수src/main.rsbinary crate rootlesson-kitfn mainlesson_kit::greet("지수")정의정의공개 API 호출
  1. Package · Cargo가 관리하는 기능 묶음

    Cargo.toml이 package metadata, edition, dependency와 target 설정을 설명합니다.

  2. Target → crate · 별도 컴파일 경계

    Library target은 최대 하나, binary target은 여러 개 둘 수 있으며 각 target은 하나의 crate root에서 별도 crate를 만듭니다.

  3. Crate root → module tree · 한 크레이트의 구조

    src/lib.rs나 src/main.rs의 루트 모듈에서 mod 선언을 따라 아이템과 하위 모듈을 구성합니다.

  4. Path + visibility · 이름과 공개 표면

    crate::garden 같은 경로가 아이템을 가리키고, pub이 접근 범위를 열며, use는 스코프에 단축 이름을 만듭니다.

Dependency boundary

[dependencies]와 use는 역할이 다르다

Manifest 선언이 Cargo의 dependency graph를 바꿉니다. use는 이미 접근 가능한 경로의 이름만 현재 스코프에 가져옵니다.

Workspace boundary

Package 하나와 여러 package의 workspace를 구분한다

Package는 자체 target을 제공하고, workspace는 함께 개발하는 여러 package의 lockfile과 출력 디렉터리를 조정합니다.

소스 파일이 늘었다고 crate가 자동으로 늘지는 않습니다. 먼저 manifest의 target을 찾고, 각 crate root 안에서는 mod 선언과 공개 경로를 따라갑니다.

패키지, 크레이트, 모듈, 경로

러스트의 모듈 시스템은 다음 네 개념으로 이루어집니다.

개념뜻
패키지(package)크레이트를 빌드하고 테스트하고 공유하는 Cargo의 단위. Cargo.toml에 이름, 버전, 에디션, 의존성, 빌드 대상이 적힘
크레이트(crate)컴파일러가 한 번에 컴파일하는 코드의 단위. 라이브러리 하나나 실행 파일 하나가 됨
모듈(module)크레이트 안에서 아이템을 트리로 정리하고, 스코프와 공개 범위를 나누는 단위
경로(path)crate::garden::vegetables::Asparagus처럼 모듈 트리 안의 아이템을 가리키는 이름

모듈은 다른 파일에 둘 수 있지만, 파일 하나가 크레이트 하나가 되는 것은 아닙니다. 또 Cargo.toml의 [dependencies]는 패키지가 쓸 다른 패키지를 더하는 것이고, 다음 글들에서 볼 use는 이미 쓸 수 있는 경로의 이름을 지금의 스코프로 가져올 뿐 의존성을 설치하거나 선언하지 않습니다. 함께 개발하는 여러 패키지를 묶는 작업 공간(workspace)은 Cargo 작업공간에서 다룹니다.

크레이트와 크레이트 루트

크레이트는 컴파일러가 한 번에 다루는 가장 작은 코드 단위입니다. rustc에 파일 하나를 넘겨도, Hello, World!에서처럼 Cargo 없이 rustc main.rs로 컴파일해도, 컴파일러는 그 파일을 크레이트 하나로 봅니다. 크레이트에는 모듈이 들어 있을 수 있고, 그 모듈은 크레이트와 함께 컴파일되는 다른 파일에 정의될 수도 있습니다.

크레이트에는 두 종류가 있습니다. 바이너리 크레이트(binary crate)는 명령줄 프로그램이나 서버처럼 실행할 수 있는 파일로 컴파일되는 프로그램이며, 실행할 때 처음 실행되는 main 함수가 있어야 합니다. 지금까지 만든 크레이트는 모두 바이너리 크레이트였습니다. 라이브러리 크레이트(library crate)는 main이 없고 실행 파일로 컴파일되지 않으며, 여러 프로젝트가 함께 쓸 기능을 정의합니다. 추리 게임에서 쓴 rand 크레이트가 무작위 수를 만드는 기능을 제공하는 라이브러리 크레이트입니다. 러스트 사용자들이 그냥 크레이트라고 말하면 대개 라이브러리 크레이트를 뜻하며, 다른 언어의 라이브러리와 같은 뜻으로 씁니다.

크레이트 루트(crate root)는 컴파일러가 컴파일을 시작하는 소스 파일로, 그 크레이트의 루트 모듈이 됩니다. 모듈은 다음 글에서 자세히 다룹니다.

패키지는 빌드 대상을 설명하는 설정 파일을 가진다

패키지는 기능을 제공하는 하나 이상의 크레이트를 묶은 것입니다. 패키지에는 그 크레이트들을 어떻게 빌드할지 설명하는 Cargo.toml 파일이 있습니다. Cargo 자체도 패키지로, 터미널에서 쓰는 명령줄 도구를 만드는 바이너리 크레이트와, 그 도구가 쓰는 라이브러리 크레이트를 함께 담고 있습니다. 다른 프로젝트도 Cargo의 라이브러리 크레이트에 의존해 Cargo 명령줄 도구와 같은 기능을 쓸 수 있습니다.

패키지에는 바이너리 크레이트를 원하는 만큼 둘 수 있지만, 라이브러리 크레이트는 많아야 하나입니다. 그리고 라이브러리든 바이너리든 크레이트가 적어도 하나는 있어야 합니다. Cargo.toml은 패키지와 빌드 대상에 대한 뜻을 기록하고, Cargo.lock은 의존성을 고른 뒤 선택한 버전을 기록할 뿐 새 크레이트나 모듈 트리를 만들지 않습니다.

새 패키지 만들기

cargo new로 패키지를 만들면 어떤 일이 일어나는지 봅니다. 지금 Cargo가 새 패키지에 넣는 기본 에디션은 2024이며, 이전 에디션인 2021이 필요한 프로젝트라면 --edition 2021처럼 에디션을 지정합니다.

cargo new --edition 2021 my-project
ls my-project
ls my-project/src
Cargo.toml
src
main.rs

cargo new가 만든 폴더에는 Cargo.toml과 src 폴더가 있고, src 안에 main.rs가 있습니다. Cargo.toml에는 지정한 에디션이 기록됩니다.

my-project/Cargo.toml
[package]
name = "my-project"
version = "0.1.0"
edition = "2021"

[dependencies]

Cargo.toml에는 src/main.rs에 대한 말이 없습니다. Cargo는 src/main.rs가 패키지와 같은 이름의 바이너리 크레이트의 크레이트 루트라는 관례를 따르기 때문입니다. 아래 표는 Cargo가 이런 파일 관례에서 빌드 대상과 크레이트 루트를 찾는 규칙입니다.

Cargo.toml과 src/lib.rs, src/main.rs, src/bin 파일 및 명시적 target 설정이 package의 library와 binary crate root로 해석되는 규칙표

Cargo · TARGET DISCOVERY

파일 경로가 target이 되는 관례와 예외를 함께 본다

Cargo는 source layout에서 target을 자동 탐색합니다. 각 발견된 library·binary target의 시작 파일은 별도 crate root이며, 일반 module 파일과 구분됩니다.

Package 파일과 Cargo target 해석
역할 Cargo가 만드는 경계 선택·실행 계약
Manifest Cargo.toml: package metadata, edition, dependency와 target override를 설명하는 manifest 그 자체는 crate root가 아님
Library src/lib.rs: package당 최대 하나의 library target과 library crate root cargo build --lib
Default binary src/main.rs: package 이름의 기본 binary target과 binary crate root cargo run --bin <package>
Extra binary src/bin/*.rs: admin.rs처럼 파일 stem을 이름으로 쓰는 추가 binary target과 별도 crate root cargo run --bin admin
Explicit target [lib] · [[bin]]: name, path 등으로 관례 밖 target을 명시 자동 탐색과 병행; 끄려면 autolib·autobins 설정
Cargo.toml · manifest
Package metadata, edition, dependency와 target override를 설명합니다.
Manifest 자체는 crate root가 아닙니다.
src/lib.rs · library
Package당 최대 하나의 library target과 library crate root입니다.
cargo build --lib
src/main.rs · default binary
Package 이름의 기본 binary target과 binary crate root입니다.
cargo run --bin <package-name>
src/bin/admin.rs · extra binary
admin target을 만들고 별도 crate root에서 컴파일합니다.
cargo run --bin admin
[lib] · [[bin]]
name, path 등으로 관례 밖 target을 명시합니다.
자동 탐색과 병행됩니다. 끄려면 autolib·autobins를 설정합니다.
Target roots

Library와 binary는 같은 package에서도 별도 crate다

src/lib.rs와 src/main.rs가 함께 있으면 각각 컴파일됩니다. Binary는 library의 공개 API를 사용할 수 있습니다.

Module files

mod가 읽는 파일은 새 target이 아니다

src/garden.rs 같은 파일은 선언한 crate의 module tree에 들어가며 독립 실행 파일이나 library가 되지 않습니다.

현재 cargo new의 기본 edition은 2024입니다. Rust 2021 package는 --edition 2021로 생성하고 manifest의 edition 값을 확인합니다.

파일 관례가 빌드 대상으로 해석되는 방식

Cargo는 다음 관례로 빌드 대상을 찾습니다. src/lib.rs가 있으면 패키지와 같은 이름의 라이브러리 크레이트가 있다고 보고 그 파일을 크레이트 루트로 삼으며, 이때 패키지 이름의 하이픈은 크레이트 이름에서 밑줄로 바뀝니다. src/main.rs가 있으면 패키지와 같은 이름의 바이너리 크레이트의 크레이트 루트로 삼습니다. src/bin/admin.rs처럼 src/bin 폴더에 파일을 두면, 파일마다 그 이름의 바이너리 크레이트가 하나씩 더 생깁니다.

Cargo는 크레이트 루트 파일을 rustc에 넘겨 라이브러리나 실행 파일을 빌드합니다. 그래서 src/main.rs와 src/lib.rs가 모두 있는 패키지에는 패키지와 이름이 같은 바이너리 크레이트와 라이브러리 크레이트가 하나씩 있습니다. 반면 src/garden.rs 같은 파일은 크레이트 루트에서 모듈로 선언해야 쓰이는 모듈 파일일 뿐, 새 크레이트가 되지는 않습니다. 관례와 다른 이름이나 경로가 필요하면 Cargo.toml의 [lib]나 [[bin]]에 name과 path를 적으며, 2018 에디션부터는 이렇게 적어도 파일 관례에 따른 자동 탐색은 계속 켜져 있습니다.

바이너리 크레이트가 하나뿐이면 cargo run으로 실행하고, 여럿이면 cargo run --bin 이름으로 고르거나 Cargo.toml의 [package]에 default-run을 적습니다. 여기서 빌드 대상(target)은 Cargo가 빌드할 라이브러리나 바이너리를 가리키는 말로, 빌드 결과가 놓이는 target/ 폴더와는 다른 개념입니다. 같은 패키지의 바이너리 크레이트는 그 패키지의 라이브러리 크레이트가 공개한 API를 쓸 수 있습니다.

한 패키지의 두 크레이트

한 패키지에 라이브러리 크레이트와 바이너리 크레이트를 함께 두고, 바이너리에서 라이브러리의 함수를 불러 봅니다. cargo new는 src/main.rs만 만들므로 src/lib.rs는 직접 추가합니다. Cargo는 두 파일이 모두 있으면 따로 설정하지 않아도 두 크레이트를 함께 빌드합니다.

cargo new --edition 2024 lesson-kit
cd lesson-kit
src/lib.rs
pub fn greet(name: &str) -> String {
    format!("Hello, {name}!")
}
src/main.rs
fn main() {
    println!("{}", lesson_kit::greet("Jisu"));
}
Hello, Jisu!

src/lib.rs가 크레이트 루트인 라이브러리 크레이트의 이름은 패키지 이름의 하이픈을 밑줄로 바꾼 lesson_kit이고, src/main.rs의 바이너리 크레이트는 그 이름으로 공개 함수 greet를 부릅니다. 그림에는 이름이 한글로 적혀 있지만, 본문에서는 영어 출력으로 맞추려고 로마자 Jisu를 썼습니다. 두 크레이트는 같은 패키지에 있어도 별개이므로, src/main.rs에서 crate::greet라고 쓰면 바이너리 크레이트 안에서 찾으므로 찾지 못합니다. 라이브러리의 함수는 라이브러리 크레이트의 이름으로 부르며, greet 앞의 pub은 다음 글들에서 볼 공개 표시입니다. 자동 탐색과 같은 패키지의 라이브러리를 쓰는 규칙은 Cargo의 빌드 대상 설명에 있습니다.

Cargo 프로젝트를 읽는 순서

처음 보는 러스트 프로젝트는 다음 순서로 읽으면 구조를 빨리 파악할 수 있습니다.

순서볼 것
1가장 가까운 Cargo.toml이 [package]인지 [workspace]인지
2패키지 이름, 에디션, 의존성, 직접 적은 [lib]와 [[bin]]
3적지 않은 빌드 대상을 src/lib.rs, src/main.rs, src/bin 관례로 찾기
4각 크레이트 루트에서 mod 선언을 따라 모듈 트리 그리기
5pub과 crate:: 경로를 확인하고, use가 어떤 이름을 가져오는지 읽기

폴더 이름을 그대로 크레이트의 단위로 보지 않는 것이 핵심입니다. 설정 파일이 패키지와 빌드 대상을 정하고, 각 빌드 대상의 크레이트 루트가 따로 모듈 트리를 시작하며, 경로와 공개 범위가 그 트리에서 쓸 수 있는 부분을 정합니다.

정리

  • 패키지는 Cargo의 단위로 Cargo.toml을 가지며, 바이너리 크레이트는 여럿, 라이브러리 크레이트는 많아야 하나를 담습니다.
  • 크레이트는 컴파일러가 한 번에 컴파일하는 단위이며, 실행 파일이 되는 바이너리 크레이트와 기능을 제공하는 라이브러리 크레이트가 있습니다.
  • 크레이트 루트는 컴파일을 시작하는 파일로, 그 크레이트의 루트 모듈이 됩니다.
  • src/main.rs는 바이너리, src/lib.rs는 라이브러리의 크레이트 루트이며, src/bin의 파일은 바이너리 크레이트를 더 만듭니다.
  • 라이브러리 크레이트의 이름은 패키지 이름의 하이픈을 밑줄로 바꾼 것이며, 같은 패키지의 바이너리는 그 이름으로 공개 API를 씁니다.

연습 문제

1. 크레이트는 몇 개일까

lesson-kit 패키지에 자동 탐색을 끄지 않고 src/bin/admin.rs를 더해, 그 main이 lesson_kit::greet("Admin")을 출력하게 했습니다. 라이브러리 크레이트와 바이너리 크레이트는 각각 몇 개인가요? cargo run만 입력하면 어떻게 되나요?

해설 보기

패키지는 하나이고, 라이브러리 크레이트 하나(src/lib.rs)와 바이너리 크레이트 두 개(src/main.rs의 lesson-kit, src/bin/admin.rs의 admin)가 있어 크레이트는 모두 세 개입니다. 바이너리가 둘이므로 cargo run만 입력하면 Cargo는 어느 것을 실행할지 몰라 실행하지 않고, --bin으로 고르라고 알려 줍니다. cargo run --bin admin으로 실행하면 Hello, Admin!이 출력되며, Cargo.toml에 default-run = "lesson-kit"을 적으면 cargo run이 그 바이너리를 실행합니다.

2. 새 크레이트가 되는 파일

다음 가운데 새 크레이트의 크레이트 루트가 되는 파일은 무엇인가요? (가) src/lib.rs (나) src/garden.rs (다) src/bin/report.rs (라) Cargo.lock

해설 보기

(가)와 (다)입니다. src/lib.rs는 라이브러리 크레이트의, src/bin/report.rs는 report라는 바이너리 크레이트의 크레이트 루트가 됩니다. (나)는 크레이트 루트에서 mod garden;으로 선언해야 쓰이는 모듈 파일이며, (라)는 의존성의 선택 결과를 기록한 파일일 뿐 크레이트가 아닙니다.

다음 글 모듈 스코프와 공개 범위에서는 크레이트 안의 코드를 모듈로 묶고, 무엇을 공개하고 무엇을 감출지 정하는 방법을 살펴봅니다.