Cargo 워크스페이스

Cargo 워크스페이스 (Cargo Workspaces)

12장에서 바이너리 크레이트와 라이브러리 크레이트를 포함하는 패키지를 만들었어요. 프로젝트가 발전하면서 라이브러리 크레이트가 계속 커지고, 패키지를 여러 라이브러리 크레이트로 더 나누고 싶어질 수 있어요. Cargo는 함께 개발되는 여러 관련 패키지를 관리하는 데 도움을 주는 **워크스페이스(workspaces)**라는 기능을 제공해요.

출처: The Rust Book

워크스페이스 만들기 (Creating a Workspace)

워크스페이스는 같은 Cargo.lock과 출력 디렉터리를 공유하는 패키지들의 집합이에요. 워크스페이스를 사용하는 프로젝트를 하나 만들어 볼게요. 워크스페이스의 구조에 집중할 수 있도록 사소한 코드를 사용할 거예요. 워크스페이스를 구성하는 방법은 여러 가지가 있는데, 그중 한 가지 흔한 방식을 보여드릴게요. 바이너리 하나와 라이브러리 두 개를 포함하는 워크스페이스를 만들 거예요. 주 기능을 제공할 바이너리가 두 라이브러리에 의존해요. 한 라이브러리는 add_one 함수를 제공하고, 다른 라이브러리는 add_two 함수를 제공할 거예요. 이 세 크레이트는 같은 워크스페이스의 일부가 돼요. 먼저 워크스페이스용 새 디렉터리를 만들게요:

$ mkdir add
$ cd add

다음으로 add 디렉터리 안에 전체 워크스페이스를 구성할 Cargo.toml 파일을 만들어요. 이 파일에는 [package] 섹션이 없어요. 대신 워크스페이스에 멤버를 추가할 수 있게 해 주는 [workspace] 섹션으로 시작해요. 그리고 워크스페이스에서 Cargo 리졸버 알고리즘의 최신 버전을 사용하도록 resolver 값을 "3"으로 설정했어요:

Filename: Cargo.toml

[workspace]
resolver = "3"

다음으로 add 디렉터리 안에서 cargo new를 실행해 adder 바이너리 크레이트를 만들어요:

$ cargo new adder
     Created binary (application) `adder` package
      Adding `adder` as member of workspace at `file:///projects/add`

워크스페이스 안에서 cargo new를 실행하면 새로 만든 패키지가 워크스페이스 Cargo.toml[workspace] 정의에 있는 members 키에 자동으로 추가돼요:

[workspace]
resolver = "3"
members = ["adder"]

이 시점에서 cargo build를 실행해 워크스페이스를 빌드할 수 있어요. add 디렉터리의 파일들은 이렇게 생겼을 거예요:

├── Cargo.lock
├── Cargo.toml
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

워크스페이스에는 컴파일된 산출물이 들어갈 최상위 target 디렉터리가 하나 있고, adder 패키지는 자기만의 target 디렉터리를 가지지 않아요. adder 디렉터리 안에서 cargo build를 실행해도 컴파일된 산출물은 add/adder/target이 아니라 add/target에 들어가요. Cargo가 워크스페이스에서 target 디렉터리를 이렇게 구성하는 이유는 워크스페이스의 크레이트들이 서로 의존하도록 설계되었기 때문이에요. 각 크레이트가 자기만의 target 디렉터리를 가진다면, 각 크레이트가 워크스페이스의 다른 크레이트들을 모두 다시 컴파일해서 자기 target 디렉터리에 산출물을 넣어야 해요. 하나의 target 디렉터리를 공유함으로써 크레이트들은 불필요한 재빌드를 피할 수 있어요.

워크스페이스에서 두 번째 패키지 만들기 (Creating the Second Package in the Workspace)

이제 워크스페이스에 또 다른 멤버 패키지를 만들고 add_one이라고 부를게요. add_one이라는 새 라이브러리 크레이트를 생성해요:

$ cargo new add_one --lib
     Created library `add_one` package
      Adding `add_one` as member of workspace at `file:///projects/add`

최상위 Cargo.toml에는 이제 members 목록에 add_one 경로가 포함될 거예요:

Filename: Cargo.toml

[workspace]
resolver = "3"
members = ["adder", "add_one"]

이제 add 디렉터리에는 이 디렉터리들과 파일들이 있어야 해요:

├── Cargo.lock
├── Cargo.toml
├── add_one
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
├── adder
│   ├── Cargo.toml
│   └── src
│       └── main.rs
└── target

add_one/src/lib.rs 파일에 add_one 함수를 추가해 볼게요:

Filename: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

이제 바이너리가 있는 adder 패키지가 라이브러리가 있는 add_one 패키지에 의존하게 만들 수 있어요. 먼저 adder/Cargo.tomladd_one에 대한 **경로 의존성(path dependency)**을 추가해야 해요.

Filename: adder/Cargo.toml

[dependencies]
add_one = { path = "../add_one" }

Cargo는 워크스페이스의 크레이트들이 서로 의존할 거라고 가정하지 않으므로, 의존 관계를 명시적으로 표현해야 해요.

이제 adder 크레이트에서 add_one 크레이트의 add_one 함수를 사용해 볼게요. adder/src/main.rs 파일을 열고 Listing 14-7처럼 main 함수를 바꿔 add_one 함수를 호출해요.

Filename: adder/src/main.rs

fn main() {
    let num = 10;
    println!("Hello, world! {num} plus one is {}!", add_one::add_one(num));
}

Listing 14-7: adder 크레이트에서 add_one 라이브러리 크레이트 사용하기

최상위 add 디렉터리에서 cargo build를 실행해 워크스페이스를 빌드해 볼게요!

$ cargo build
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.22s

add 디렉터리에서 바이너리 크레이트를 실행하려면 cargo run-p 인자와 패키지 이름을 지정해 워크스페이스의 어떤 패키지를 실행할지 정할 수 있어요:

$ cargo run -p adder
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/adder`
Hello, world! 10 plus one is 11!

이렇게 하면 add_one 크레이트에 의존하는 adder/src/main.rs의 코드가 실행돼요.

외부 패키지에 의존하기 (Depending on an External Package)

워크스페이스에는 각 크레이트 디렉터리에 Cargo.lock이 아니라 최상위에 Cargo.lock 파일이 하나만 있다는 점을 주목하세요. 이렇게 하면 모든 크레이트가 모든 의존성의 같은 버전을 사용하게 보장돼요. rand 패키지를 adder/Cargo.tomladd_one/Cargo.toml 파일에 추가하면, Cargo는 둘 다를 rand의 한 버전으로 해석하고 그걸 하나의 Cargo.lock에 기록해요. 워크스페이스의 모든 크레이트가 같은 의존성을 사용하게 하는 것은 크레이트들이 항상 서로 호환되게 한다는 뜻이에요. add_one 크레이트에서 rand 크레이트를 사용할 수 있도록 add_one/Cargo.toml 파일의 [dependencies] 섹션에 rand 크레이트를 추가해 볼게요:

Filename: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

이제 add_one/src/lib.rs 파일에 use rand;를 추가할 수 있고, add 디렉터리에서 cargo build를 실행해 전체 워크스페이스를 빌드하면 rand 크레이트가 끌려 들어와 컴파일돼요. 스코프로 가져온 rand를 사용하지 않기 때문에 경고가 하나 나와요:

$ cargo build
    Updating crates.io index
  Downloaded rand v0.8.5
   --snip--
   Compiling rand v0.8.5
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
warning: unused import: `rand`
 --> add_one/src/lib.rs:1:5
  |
1 | use rand;
  |     ^^^^
  |
  = note: `#[warn(unused_imports)]` on by default

warning: `add_one` (lib) generated 1 warning (run `cargo fix --lib -p add_one` to apply 1 suggestion)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.95s

이제 최상위 Cargo.lock에는 add_onerand에 의존한다는 정보가 들어 있어요. 하지만 rand가 워크스페이스 어딘가에서 사용되더라도, 다른 크레이트의 Cargo.toml 파일에도 rand를 추가하지 않는 한 워크스페이스의 다른 크레이트에서는 rand를 사용할 수 없어요. 예를 들어 adder 패키지의 adder/src/main.rs 파일에 use rand;를 추가하면 오류가 나요:

$ cargo build
  --snip--
   Compiling adder v0.1.0 (file:///projects/add/adder)
error[E0432]: unresolved import `rand`
 --> adder/src/main.rs:2:5
  |
2 | use rand;
  |     ^^^^ no external crate `rand`

이걸 고치려면 adder 패키지의 Cargo.toml 파일을 수정해 rand도 그 패키지의 의존성이라고 표시해요. adder 패키지를 빌드하면 Cargo.lockadder 의존성 목록에 rand를 추가하지만, rand의 추가 복사본은 다운로드되지 않아요. Cargo는 워크스페이스의 모든 패키지에서 rand 패키지를 사용하는 모든 크레이트가 호환되는 버전을 지정하는 한 같은 버전을 사용하도록 보장해서, 공간을 절약하고 워크스페이스의 크레이트들이 서로 호환되게 해줘요.

워크스페이스의 크레이트들이 같은 의존성의 호환되지 않는 버전을 지정하면 Cargo는 각각을 해석하되, 가능한 한 적은 수의 버전으로 해석하려고 노력해요.

워크스페이스에 테스트 추가하기 (Adding a Test to a Workspace)

또 다른 개선으로 add_one 크레이트 안에 add_one::add_one 함수의 테스트를 추가해 볼게요:

Filename: add_one/src/lib.rs

pub fn add_one(x: i32) -> i32 {
    x + 1
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        assert_eq!(3, add_one(2));
    }
}

이제 최상위 add 디렉터리에서 cargo test를 실행해요. 이렇게 구성된 워크스페이스에서 cargo test를 실행하면 워크스페이스의 모든 크레이트의 테스트가 실행돼요:

$ cargo test
   Compiling add_one v0.1.0 (file:///projects/add/add_one)
   Compiling adder v0.1.0 (file:///projects/add/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/adder-3a47283c568d2b6a)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

출력의 첫 섹션은 add_one 크레이트의 it_works 테스트가 통과했음을 보여줘요. 다음 섹션은 adder 크레이트에서 테스트가 0개 발견됐음을, 마지막 섹션은 add_one 크레이트에서 문서 테스트가 0개 발견됐음을 보여줘요.

최상위 디렉터리에서 -p 플래그와 테스트할 크레이트의 이름을 지정하면 워크스페이스의 특정 크레이트 하나의 테스트만 실행할 수도 있어요:

$ cargo test -p add_one
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running unittests src/lib.rs (target/debug/deps/add_one-93c49ee75dc46543)

running 1 test
test tests::it_works ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests add_one

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

이 출력은 cargo testadd_one 크레이트의 테스트만 실행했을 뿐 adder 크레이트의 테스트는 실행하지 않았음을 보여줘요.

워크스페이스의 크레이트들을 crates.io에 공개한다면, 워크스페이스의 각 크레이트를 개별적으로 공개해야 해요. cargo test와 마찬가지로 -p 플래그와 공개할 크레이트의 이름을 지정하면 워크스페이스의 특정 크레이트 하나를 공개할 수 있어요.

연습을 더 하고 싶다면 add_one 크레이트처럼 비슷한 방식으로 add_two 크레이트를 이 워크스페이스에 추가해 보세요!

프로젝트가 커지면 워크스페이스 사용을 고려해 보세요. 워크스페이스는 하나의 큰 코드 덩어리보다 더 작고 이해하기 쉬운 컴포넌트로 작업할 수 있게 해줘요. 게다가 크레이트들을 워크스페이스에 두면, 같은 시기에 자주 변경되는 크레이트들 사이의 조정이 더 쉬워져요.

더 알아보기 (Learn more)