Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cargo 워크스페이스

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

워크스페이스 만들기

워크스페이스(workspace) 는 같은 Cargo.lock 과 출력 디렉터리를 공유하는 패키지 집합입니다. 워크스페이스를 사용하는 프로젝트를 하나 만들어 봅시다. 코드 자체는 아주 단순하게 유지해서, 워크스페이스 구조에만 집중하겠습니다. 워크스페이스를 구성하는 방법은 여러 가지가 있지만, 여기서는 흔한 방식 하나만 보여 줍니다. 바이너리 하나와 라이브러리 둘을 포함한 워크스페이스를 만들 것입니다. 메인 기능을 제공하는 바이너리는 두 라이브러리에 의존합니다. 한 라이브러리는 add_one 함수를, 다른 라이브러리는 add_two 함수를 제공합니다. 이 세 크레이트는 같은 워크스페이스 안에 속하게 됩니다. 먼저 워크스페이스용 새 디렉터리를 만듭니다.

$ mkdir add
$ cd add

다음으로 add 디렉터리 안에 워크스페이스 전체를 설정할 Cargo.toml 파일을 만듭니다. 이 파일에는 [package] 섹션이 없습니다. 대신 [workspace] 섹션으로 시작하고, 여기에 워크스페이스 구성원을 추가합니다. 또한 Cargo의 resolver 알고리즘은 최신 버전을 쓰도록 resolver 값을 "3" 으로 지정합니다.

파일명: 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는 새로 만든 패키지를 자동으로 워크스페이스 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 디렉터리를 공유하면 그런 불필요한 재빌드를 피할 수 있습니다.

워크스페이스에 두 번째 패키지 만들기

이제 워크스페이스에 또 다른 멤버 패키지를 추가해 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.tomlmembers 목록에는 add_one 경로도 들어갑니다.

파일명: 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 함수를 넣어 봅시다.

파일명: add_one/src/lib.rs

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

이제 바이너리를 가진 adder 패키지가 라이브러리를 가진 add_one 패키지에 의존하도록 만들 수 있습니다. 먼저 adder/Cargo.tomladd_one 에 대한 path 의존성을 추가해야 합니다.

파일명: adder/Cargo.toml

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

Cargo는 워크스페이스 안의 크레이트들이 자동으로 서로 의존할 것이라고 가정하지 않기 때문에, 이런 의존성 관계는 직접 명시해야 합니다.

그 다음 adder/src/main.rs 파일을 열고, 목록 14-7처럼 add_one 크레이트의 add_one 함수를 호출하도록 main 을 바꿉니다.

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!

이 명령은 adder/src/main.rs 안의 코드를 실행하는데, 이 코드는 add_one 크레이트에 의존합니다.

워크스페이스에서 외부 패키지에 의존하기

워크스페이스에는 각 크레이트 디렉터리에 Cargo.lock 이 따로 있는 대신, 최상위에 오직 하나의 Cargo.lock 만 있다는 점에 주목하세요. 이렇게 하면 모든 크레이트가 같은 버전의 의존성을 사용하게 됩니다. 만약 adder/Cargo.tomladd_one/Cargo.toml 모두에 rand 패키지를 추가하면, Cargo는 둘 모두를 하나의 rand 버전으로 해결하고, 그 버전을 단 하나의 Cargo.lock 에 기록합니다. 워크스페이스 안의 모든 크레이트가 같은 의존성 버전을 사용하면, 서로 항상 호환 가능하다는 장점도 있습니다. 이제 add_one/Cargo.toml[dependencies] 섹션에 rand 를 추가해, add_one 크레이트 안에서 rand 를 사용할 수 있게 해 봅시다.

파일명: add_one/Cargo.toml

[dependencies]
rand = "0.8.5"

이제 add_one/src/lib.rsuse 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 가 워크스페이스 어딘가에서 쓰이고 있다고 해서, 다른 크레이트에서 자동으로 사용할 수 있는 것은 아닙니다. 예를 들어 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는 각각을 따로 해결합니다. 그래도 가능한 한 버전 수를 적게 유지하려 노력합니다.

워크스페이스에 테스트 추가하기

한 단계 더 나아가, add_one 크레이트 안 add_one::add_one 함수에 대한 테스트를 추가해 봅시다.

파일명: 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 크레이트 안에서 테스트를 하나도 찾지 못했다는 내용이고, 마지막 섹션은 add_one 크레이트에 문서 테스트가 없었다는 뜻입니다.

워크스페이스 최상위 디렉터리에서, 특정 크레이트에 대한 테스트만 실행할 수도 있습니다. -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 크레이트도 추가해 보세요!

프로젝트가 커질수록 워크스페이스 사용을 고려해 보세요. 하나의 거대한 코드 덩어리보다 더 작고 이해하기 쉬운 컴포넌트들로 작업할 수 있게 되기 때문입니다. 또한 워크스페이스 안에 크레이트들을 함께 두면, 이들이 자주 동시에 바뀌는 경우 서로 간 조율도 훨씬 쉬워집니다.