Rust 완벽 마스터

Rust의 핵심 개념과 실전 활용법

Rust중급
12시간
7개 항목
학습 진행률0 / 7 (0%)

학습 항목

1. Rust
Cargo|워크스페이스|모노레포|구성|가이드
퀴즈튜토리얼
2. Rust
Rust|Serde|직렬화|역직렬화|완벽|가이드
퀴즈튜토리얼
3. Rust
Rust|Serde|직렬화|역직렬화|완벽가이드
퀴즈튜토리얼
4. Rust
Rust|소유권|시스템|완벽|이해
퀴즈튜토리얼
5. Rust
Tokio|비동기|프로그래밍|Rust|고급
퀴즈튜토리얼
6. TypeScript
Actix-web|REST|API|서버|개발
퀴즈튜토리얼
7. TypeScript
초급
Rust|실무|활용|팁|가이드
퀴즈튜토리얼
1 / 7

이미지 로딩 중...

Cargo 워크스페이스 모노레포 구성 가이드 - 슬라이드 1/11

Rust Cargo 워크스페이스와 모노레포 완벽 가이드

Rust 프로젝트가 커질수록 코드 관리가 복잡해집니다. Cargo 워크스페이스를 활용한 모노레포 구성 방법을 실무 관점에서 단계별로 알아봅니다. 의존성 관리, 코드 공유, 빌드 최적화까지 실전 노하우를 담았습니다.


목차

  1. Cargo 워크스페이스 기초
  2. 워크스페이스 초기 설정
  3. 멤버 크레이트 추가
  4. 공유 의존성 관리
  5. 워크스페이스 빌드 전략
  6. 크레이트 간 코드 공유
  7. 워크스페이스 테스트
  8. 릴리스 관리

1. Cargo 워크스페이스 기초

시작하며

여러분이 Rust 프로젝트를 진행하다 보면 이런 상황을 겪어본 적 있나요? API 서버, CLI 도구, 공통 라이브러리를 각각 별도의 프로젝트로 관리하다가 공통 코드를 수정할 때마다 세 곳을 다 고쳐야 하는 상황 말이죠.

이런 문제는 실제 개발 현장에서 자주 발생합니다. 각 프로젝트가 독립적으로 관리되면 의존성 버전이 달라지고, 공통 로직을 복사-붙여넣기 하게 되며, 결국 유지보수가 악몽이 됩니다.

특히 팀 프로젝트에서는 코드 동기화 문제로 버그가 발생하기 쉽습니다. 바로 이럴 때 필요한 것이 Cargo 워크스페이스입니다.

여러 관련 프로젝트를 하나의 루트 아래에서 통합 관리하면서도 각각의 독립성은 유지할 수 있습니다.

개요

간단히 말해서, Cargo 워크스페이스는 여러 개의 Rust 패키지(크레이트)를 하나의 프로젝트 구조 안에서 관리하는 메커니즘입니다. 왜 이 개념이 필요한지 실무 관점에서 설명하자면, 대규모 프로젝트에서는 코드베이스를 모듈화하여 관리해야 합니다.

예를 들어, 웹 서버, 데이터베이스 레이어, 비즈니스 로직, CLI 도구를 모두 별도의 크레이트로 분리하면 각 부분을 독립적으로 개발하고 테스트할 수 있습니다. 기존에는 각 크레이트를 별도의 git 저장소나 디렉토리에서 관리했다면, 이제는 워크스페이스를 통해 하나의 저장소에서 통합 관리하면서도 논리적 분리를 유지할 수 있습니다.

워크스페이스의 핵심 특징은 첫째, 모든 멤버 크레이트가 하나의 Cargo.lock을 공유하여 의존성 버전을 일관되게 관리하고, 둘째, 공통 target 디렉토리를 사용하여 빌드 산출물을 공유하며, 셋째, 한 번의 명령으로 모든 크레이트를 빌드하거나 테스트할 수 있다는 점입니다. 이러한 특징들이 팀 협업과 CI/CD 파이프라인 구성을 훨씬 간편하게 만들어줍니다.

코드 예제

// 프로젝트 루트의 Cargo.toml - 워크스페이스 정의
[workspace]
// 워크스페이스에 포함될 모든 멤버 크레이트 지정
members = [
    "server",      // API 서버 크레이트
    "cli",         // CLI 도구 크레이트
    "common",      // 공통 라이브러리 크레이트
]

// 워크스페이스 전체에 적용될 공통 설정
[workspace.package]
edition = "2021"
version = "0.1.0"
authors = ["Your Team <team@example.com>"]

설명

이것이 하는 일: Cargo 워크스페이스는 관련된 여러 Rust 패키지를 하나의 통합된 환경에서 관리할 수 있게 해줍니다. 마치 여러 개의 작은 프로젝트를 하나의 큰 우산 아래 두는 것과 같습니다.

첫 번째로, [workspace] 섹션이 이 Cargo.toml 파일을 워크스페이스의 루트로 지정합니다. 이 파일이 있는 디렉토리가 전체 워크스페이스의 중심이 되며, 모든 멤버 크레이트는 이 루트를 기준으로 관리됩니다.

members 배열에 나열된 각 경로는 하위 크레이트의 위치를 나타냅니다. 그 다음으로, members 배열에 지정된 각 디렉토리는 자체적인 Cargo.toml 파일을 가진 독립적인 크레이트입니다.

"server"는 웹 서버를, "cli"는 커맨드라인 도구를, "common"은 공통 라이브러리를 담당합니다. 이들은 서로를 의존성으로 참조할 수 있으며, 외부 크레이트 의존성도 각자 선언할 수 있습니다.

마지막으로, [workspace.package] 섹션은 모든 멤버 크레이트가 상속받을 수 있는 공통 메타데이터를 정의합니다. edition, version, authors 같은 필드를 여기서 한 번만 정의하면 각 멤버 크레이트에서 반복해서 작성할 필요가 없습니다.

이렇게 하면 프로젝트 전체의 일관성을 유지하기 쉽습니다. 여러분이 이 구조를 사용하면 프로젝트 루트에서 cargo build 명령 하나로 모든 크레이트를 한 번에 빌드할 수 있고, cargo test로 전체 테스트를 실행할 수 있습니다.

또한 모든 크레이트가 같은 버전의 의존성을 사용하게 되어 "내 컴퓨터에서는 되는데" 같은 문제를 방지할 수 있습니다. CI/CD 파이프라인도 훨씬 단순해지며, 코드 리뷰 시 관련 변경사항을 한눈에 파악할 수 있습니다.

실전 팁

💡 워크스페이스 루트 디렉토리에는 src 폴더를 만들지 마세요. 루트는 순수하게 워크스페이스 관리 용도로만 사용하고, 실제 코드는 모두 멤버 크레이트 안에 작성하는 것이 베스트 프랙티스입니다.

💡 .gitignore 파일은 루트에 하나만 두고 /target, /Cargo.lock(라이브러리인 경우)을 관리하세요. 각 멤버 크레이트마다 따로 만들 필요가 없습니다.

💡 처음부터 완벽한 구조를 만들려 하지 마세요. 2-3개의 크레이트로 시작해서 필요에 따라 점진적으로 분리하는 것이 실패 위험을 줄입니다.

💡 워크스페이스 이름은 프로젝트의 도메인을 반영하되, 너무 구체적이지 않게 지으세요. 나중에 범위가 확장될 수 있습니다.

💡 VS Code를 사용한다면 rust-analyzer가 워크스페이스 전체를 자동으로 인식하므로, 각 크레이트 간 이동과 코드 완성이 매끄럽게 작동합니다.


2. 워크스페이스 초기 설정

시작하며

여러분이 새로운 Rust 프로젝트를 시작할 때 이런 고민을 해본 적 있나요? "지금은 작은 프로젝트지만 나중에 커질 것 같은데, 처음부터 워크스페이스로 만들어야 할까?" 많은 개발자들이 이 지점에서 망설입니다.

이런 고민은 매우 현실적입니다. 나중에 단일 크레이트를 워크스페이스로 전환하는 것은 가능하지만, 의존성 경로를 모두 수정하고 CI 스크립트를 고쳐야 하는 등 번거로운 작업이 많습니다.

초기에 잘못된 구조를 선택하면 기술 부채가 쌓입니다. 바로 이럴 때 필요한 것이 체계적인 워크스페이스 초기 설정 전략입니다.

프로젝트의 규모와 성격에 맞는 올바른 시작점을 선택하면 나중에 확장이 훨씬 쉬워집니다.

개요

간단히 말해서, 워크스페이스 초기 설정은 프로젝트의 기반을 다지는 과정으로, 디렉토리 구조와 설정 파일을 체계적으로 구성하는 것입니다. 왜 이 단계가 중요한지 실무 관점에서 설명하자면, 초기 구조가 향후 수년간의 개발 생산성을 좌우하기 때문입니다.

예를 들어, 마이크로서비스 아키텍처를 고려 중이라면 각 서비스를 별도 크레이트로 분리하는 구조가 필요하고, 공유 라이브러리를 npm 패키지처럼 배포할 계획이라면 별도의 libs 디렉토리 구조가 유용합니다. 기존에는 cargo new로 단일 프로젝트를 만들고 나중에 리팩토링했다면, 이제는 처음부터 확장 가능한 워크스페이스 구조로 시작할 수 있습니다.

워크스페이스 초기 설정의 핵심 요소는 첫째, 명확한 디렉토리 네이밍 컨벤션 (예: apps/, libs/, tools/), 둘째, 공통 설정의 중앙 집중화 (workspace.dependencies, workspace.package), 셋째, 빌드 스크립트와 CI 파이프라인의 통합입니다. 이러한 요소들이 제대로 갖춰지면 새로운 팀원이 합류했을 때도 프로젝트 구조를 빠르게 이해할 수 있습니다.

코드 예제

# 워크스페이스 초기 구조 생성 스크립트
# 프로젝트 루트 생성
mkdir my-workspace && cd my-workspace

# 워크스페이스 루트 Cargo.toml 생성
cat > Cargo.toml << 'EOF'
[workspace]
members = ["apps/*", "libs/*"]
resolver = "2"  // Rust 2021 에디션의 새로운 의존성 해석 알고리즘

[workspace.dependencies]
// 공통으로 사용할 의존성 버전을 여기서 중앙 관리
tokio = { version = "1.35", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
EOF

# 멤버 크레이트 디렉토리 생성
mkdir -p apps libs

설명

이것이 하는 일: 이 설정 과정은 미래의 확장을 고려한 워크스페이스 골격을 만들어줍니다. 단순히 디렉토리를 만드는 것이 아니라, 프로젝트의 성장 방향을 정의하는 것입니다.

첫 번째로, 디렉토리 구조를 apps/와 libs/로 명확히 분리합니다. apps/에는 실행 가능한 바이너리 크레이트들이, libs/에는 재사용 가능한 라이브러리 크레이트들이 위치합니다.

이 분리는 코드의 역할을 직관적으로 보여주며, "이 코드는 어디에 두어야 할까?"라는 질문에 명확한 답을 제공합니다. members = ["apps/", "libs/"] 구문은 와일드카드를 사용하여 해당 디렉토리 아래의 모든 크레이트를 자동으로 워크스페이스에 포함시킵니다.

그 다음으로, resolver = "2" 설정이 중요합니다. 이것은 Rust 2021 에디션의 개선된 의존성 해석 알고리즘을 활성화합니다.

이 새로운 해석기는 feature 통합을 더 정확하게 처리하여, 한 크레이트에서 활성화한 feature가 다른 크레이트에 의도치 않게 영향을 주는 문제를 방지합니다. 특히 큰 워크스페이스에서 빌드 시간을 단축하고 예측 가능성을 높여줍니다.

마지막으로, [workspace.dependencies] 섹션은 게임 체인저입니다. 여기서 tokio = "1.35"처럼 버전을 정의하면, 각 멤버 크레이트의 Cargo.toml에서는 tokio = { workspace = true }라고만 작성하면 됩니다.

이렇게 하면 의존성 버전을 업데이트할 때 한 곳만 수정하면 되고, 모든 크레이트가 자동으로 동일한 버전을 사용하게 됩니다. 특히 serde, tokio, anyhow 같은 기본 의존성들을 이렇게 관리하면 유지보수가 엄청나게 쉬워집니다.

여러분이 이 구조를 사용하면 새로운 크레이트를 추가할 때 단순히 apps/ 또는 libs/ 아래에 cargo new를 실행하기만 하면 됩니다. 워크스페이스가 자동으로 감지하며, 공통 의존성도 즉시 사용할 수 있습니다.

팀원들도 "이 기능은 어디에 추가해야 하지?"라는 고민 없이 명확한 위치를 찾을 수 있습니다. 코드 리뷰 시에도 파일 경로만 봐도 그것이 애플리케이션 코드인지 공유 라이브러리인지 바로 알 수 있습니다.

실전 팁

💡 resolver = "2"는 필수입니다. 구버전 resolver는 feature 충돌 문제가 많으므로, 새 프로젝트에서는 반드시 버전 2를 사용하세요.

💡 디렉토리 이름은 팀의 컨벤션에 맞추세요. apps/libs/ 대신 services/packages/나 bins/crates/를 사용해도 됩니다. 중요한 건 일관성입니다.

💡 .cargo/config.toml 파일을 루트에 추가하여 빌드 옵션, 타겟 설정, alias 명령어 등을 프로젝트 전체에 적용할 수 있습니다.

💡 문서화를 위해 루트에 README.md를 만들고 디렉토리 구조와 각 크레이트의 역할을 설명하세요. 신규 팀원의 온보딩 시간이 절반으로 줄어듭니다.

💡 justfile이나 Makefile을 추가하여 자주 사용하는 명령어(전체 빌드, 테스트, 린트 등)를 간편한 단축 명령으로 만들면 생산성이 크게 향상됩니다.


3. 멤버 크레이트 추가

시작하며

여러분이 워크스페이스를 만들고 나서 이런 상황을 겪어본 적 있나요? "새로운 API 엔드포인트를 추가해야 하는데, 이걸 기존 크레이트에 넣어야 할까, 아니면 새 크레이트를 만들어야 할까?" 이 판단은 생각보다 어렵습니다.

이런 딜레마는 실제 개발 현장에서 매일 발생합니다. 너무 많은 크레이트로 쪼개면 관리 오버헤드가 커지고, 너무 적게 나누면 단일 크레이트가 비대해져서 빌드 시간이 길어집니다.

잘못된 결정은 리팩토링 비용으로 돌아옵니다. 바로 이럴 때 필요한 것이 체계적인 멤버 크레이트 추가 전략입니다.

언제 새 크레이트를 만들고, 어떻게 기존 구조와 통합하는지 알면 코드베이스가 깔끔하게 유지됩니다.

개요

간단히 말해서, 멤버 크레이트 추가는 워크스페이스 내에 새로운 독립적인 Rust 패키지를 생성하고 통합하는 과정입니다. 왜 이 과정을 제대로 이해해야 하는지 실무 관점에서 설명하자면, 크레이트 분리는 컴파일 병렬화와 직결되기 때문입니다.

예를 들어, 데이터베이스 모델 크레이트와 API 핸들러 크레이트를 분리하면, 모델 코드를 수정하지 않았을 때 API만 재컴파일할 수 있습니다. 대규모 프로젝트에서는 이것이 빌드 시간을 수 분에서 수 초로 단축시킵니다.

기존에는 모든 코드를 하나의 src/ 디렉토리에 모듈로만 분리했다면, 이제는 논리적으로 독립적인 기능을 별도 크레이트로 분리하여 더 명확한 경계와 재사용성을 확보할 수 있습니다. 멤버 크레이트 추가의 핵심 원칙은 첫째, 단일 책임 원칙 - 각 크레이트는 하나의 명확한 목적을 가져야 하고, 둘째, 최소 의존성 - 크레이트 간 의존성은 필요한 최소한으로 유지하며, 셋째, 적절한 가시성 - public API는 신중하게 설계해야 합니다.

이러한 원칙들이 장기적인 유지보수성을 보장합니다.

코드 예제

# apps 디렉토리에 새로운 바이너리 크레이트 생성
cd apps
cargo new api-server --bin
cd api-server

# api-server/Cargo.toml - 워크스페이스 설정 상속
[package]
name = "api-server"
version.workspace = true  // 워크스페이스의 버전 상속
edition.workspace = true  // 워크스페이스의 에디션 상속

[dependencies]
// 워크스페이스 공통 의존성 사용
tokio = { workspace = true }
// 같은 워크스페이스의 다른 크레이트 참조
common = { path = "../../libs/common" }

설명

이것이 하는 일: 멤버 크레이트 추가 과정은 워크스페이스의 모듈화 전략을 실행에 옮기는 핵심 작업입니다. 새로운 기능이나 서비스를 독립적인 단위로 캡슐화합니다.

첫 번째로, cargo new api-server --bin 명령이 apps/ 디렉토리 안에 새로운 바이너리 크레이트를 생성합니다. --bin 플래그는 실행 가능한 애플리케이션을 만든다는 의미입니다.

만약 라이브러리를 만들고 싶다면 --lib 플래그를 사용하면 됩니다. 이 명령은 기본적인 Cargo.toml과 src/main.rs(또는 lib.rs) 파일을 자동으로 생성해줍니다.

와일드카드 패턴("apps/*")을 사용했기 때문에 워크스페이스가 자동으로 이 새 크레이트를 인식합니다. 그 다음으로, version.workspace = true와 edition.workspace = true 설정이 매우 중요합니다.

이것은 루트 Cargo.toml의 [workspace.package]에 정의된 값을 상속받는다는 의미입니다. 이렇게 하면 워크스페이스의 모든 크레이트가 동일한 에디션과 버전 번호를 사용하게 되어 일관성이 유지됩니다.

나중에 버전을 올릴 때도 한 곳만 수정하면 모든 크레이트에 반영됩니다. 마지막으로, dependencies 섹션에서 두 가지 타입의 의존성을 볼 수 있습니다.

tokio = { workspace = true }는 워크스페이스에서 중앙 관리하는 외부 크레이트를 사용하는 것이고, common = { path = "../../libs/common" }은 같은 워크스페이스 내의 다른 멤버 크레이트를 참조하는 것입니다. path 의존성을 사용하면 common 크레이트의 코드를 수정했을 때 api-server가 자동으로 최신 버전을 사용합니다.

별도의 버전 관리나 배포 과정이 필요 없습니다. 여러분이 이 방식을 사용하면 기능별로 코드를 명확히 분리할 수 있습니다.

api-server는 HTTP 요청 처리에만 집중하고, common은 비즈니스 로직과 데이터 모델을 담당하는 식으로 책임을 나눌 수 있습니다. 이렇게 하면 테스트도 쉬워집니다 - common 크레이트는 HTTP 없이 순수 유닛 테스트를 작성할 수 있고, api-server는 통합 테스트에 집중할 수 있습니다.

또한 나중에 CLI 도구를 추가하고 싶을 때 common 크레이트를 재사용할 수 있어 코드 중복을 방지할 수 있습니다.

실전 팁

💡 크레이트 이름은 kebab-case를 사용하세요(my-crate). Rust 컨벤션이며, 코드에서는 자동으로 snake_case(my_crate)로 변환됩니다.

💡 바이너리 크레이트의 main.rs는 최대한 얇게 유지하세요. 실제 로직은 lib.rs로 분리하면 테스트와 재사용이 쉬워집니다.

💡 새 크레이트를 추가한 후 cargo build를 루트에서 실행하여 워크스페이스가 제대로 인식했는지 확인하세요. 에러가 있다면 즉시 발견됩니다.

💡 path 의존성의 상대 경로는 신중하게 작성하세요. ../../로 시작하는 경로가 많다면 디렉토리 구조를 다시 고민해볼 필요가 있습니다.

💡 각 크레이트의 Cargo.toml에 description과 license 필드를 추가하면 나중에 일부 크레이트를 오픈소스로 공개할 때 준비가 되어 있습니다.


4. 공유 의존성 관리

시작하며

여러분이 워크스페이스에서 개발하다가 이런 상황을 겪어본 적 있나요? api-server는 tokio 1.35를 쓰는데, cli-tool은 tokio 1.28을 쓰고 있어서 컴파일이 두 번 되고 바이너리 크기도 커지는 상황 말이죠.

이런 문제는 실제 프로젝트에서 매우 흔합니다. 각 크레이트의 개발자가 독립적으로 의존성을 추가하다 보면, 같은 라이브러리의 여러 버전이 프로젝트에 공존하게 됩니다.

이것은 빌드 시간을 늘리고, 바이너리 크기를 키우며, 미묘한 버그의 원인이 되기도 합니다. 바로 이럴 때 필요한 것이 공유 의존성 관리 전략입니다.

워크스페이스 레벨에서 의존성 버전을 중앙 집중화하면 이런 문제를 원천적으로 방지할 수 있습니다.

개요

간단히 말해서, 공유 의존성 관리는 워크스페이스의 모든 멤버 크레이트가 동일한 버전의 외부 라이브러리를 사용하도록 강제하는 메커니즘입니다. 왜 이것이 중요한지 실무 관점에서 설명하자면, 의존성 통일은 단순히 깔끔함의 문제가 아닙니다.

예를 들어, serde를 사용하는 5개의 크레이트가 모두 다른 버전을 쓴다면, Cargo는 5개 버전을 모두 컴파일해야 합니다. 이것은 증분 컴파일의 이점을 무력화시키고, 특히 CI 환경에서 캐시 효율성을 크게 떨어뜨립니다.

기존에는 각 크레이트의 Cargo.toml에 tokio = "1.35"를 일일이 작성하고, 업데이트할 때마다 모든 파일을 찾아 수정했다면, 이제는 워크스페이스 루트에서 한 번만 정의하고 각 크레이트에서는 { workspace = true }로 참조하기만 하면 됩니다. 공유 의존성 관리의 핵심 장점은 첫째, 버전 일관성 - 모든 크레이트가 동일한 API를 사용하고, 둘째, 쉬운 업데이트 - 한 곳만 수정하면 전체 반영되며, 셋째, 빌드 최적화 - 중복 컴파일을 방지하여 시간과 디스크 공간을 절약합니다.

이러한 장점들이 특히 대규모 팀 프로젝트에서 빛을 발합니다.

코드 예제

// 워크스페이스 루트의 Cargo.toml
[workspace]
members = ["apps/*", "libs/*"]

[workspace.dependencies]
// 공통 의존성을 버전과 feature까지 상세히 정의
tokio = { version = "1.35", features = ["full", "tracing"] }
serde = { version = "1.0", features = ["derive"] }
anyhow = "1.0"
// 조건부 의존성도 공유 가능
log = { version = "0.4", optional = true }

// 멤버 크레이트의 Cargo.toml
[dependencies]
// workspace = true로 공유 의존성 사용
tokio = { workspace = true }
serde = { workspace = true }
// 필요시 추가 feature 활성화 가능
anyhow = { workspace = true, features = ["backtrace"] }

설명

이것이 하는 일: 공유 의존성 관리 시스템은 워크스페이스 전체에 걸쳐 라이브러리 버전의 단일 진실 공급원(Single Source of Truth)을 만듭니다. 마치 팀의 스타일 가이드처럼, 모든 크레이트가 따라야 하는 의존성 표준을 제공합니다.

첫 번째로, 워크스페이스 루트의 [workspace.dependencies] 섹션이 의존성 "카탈로그" 역할을 합니다. 여기서 tokio = { version = "1.35", features = ["full", "tracing"] }처럼 상세하게 정의하면, 이것이 프로젝트의 공식 tokio 설정이 됩니다.

features 배열도 함께 정의할 수 있어서, 모든 크레이트가 동일한 tokio 기능 세트를 사용하게 됩니다. 이것은 "tokio의 런타임 feature는 항상 활성화한다"같은 팀 정책을 코드로 강제하는 효과가 있습니다.

그 다음으로, 멤버 크레이트에서 tokio = { workspace = true }라고 작성하면, 루트에 정의된 전체 설정(버전과 features 모두)이 그대로 적용됩니다. 이것은 단순한 버전 참조가 아니라 완전한 의존성 설정의 상속입니다.

Cargo가 의존성 그래프를 해석할 때, 워크스페이스 내의 모든 tokio 참조를 동일한 버전으로 통합하여 단 한 번만 컴파일합니다. 세 번째로, anyhow = { workspace = true, features = ["backtrace"] }처럼 개별 크레이트에서 추가 feature를 활성화할 수도 있습니다.

이 경우 워크스페이스 공통 설정을 기반으로 하되, 이 크레이트만 backtrace feature를 추가로 사용합니다. 이것은 "기본은 공유하되, 필요한 부분만 확장한다"는 유연성을 제공합니다.

중요한 점은 이렇게 해도 같은 버전이므로 중복 컴파일은 발생하지 않는다는 것입니다. 마지막으로, optional = true 의존성도 공유할 수 있습니다.

log = { version = "0.4", optional = true }로 정의하면, 각 크레이트에서 log = { workspace = true }를 쓸 때 자동으로 optional이 됩니다. 이것은 feature flag와 결합하여 조건부 기능을 구현할 때 유용합니다.

여러분이 이 시스템을 사용하면 의존성 업데이트가 놀라울 정도로 간단해집니다. 보안 패치가 나왔을 때 루트 Cargo.toml의 한 줄만 수정하고 cargo update를 실행하면, 모든 멤버 크레이트가 자동으로 새 버전을 사용합니다.

코드 리뷰에서도 "왜 이 크레이트만 구버전을 쓰나요?"같은 질문이 사라집니다. CI 빌드 시간도 크게 단축되는데, 특히 캐시가 있는 환경에서는 같은 의존성을 재사용하므로 효과가 극대화됩니다.

실전 팁

💡 자주 사용하는 의존성(tokio, serde, anyhow 등)은 무조건 workspace.dependencies에 등록하세요. 3개 이상의 크레이트에서 쓴다면 공유 의존성으로 만드는 것이 이득입니다.

💡 의존성 업데이트는 cargo update 대신 cargo upgrade(cargo-edit 플러그인 필요)를 사용하면 Cargo.toml의 버전 번호도 자동으로 수정됩니다.

💡 버전 지정 시 "1.0" 대신 "1.0.35"처럼 구체적으로 명시하는 것을 고려하세요. 재현 가능한 빌드에 도움이 되며, Cargo.lock이 있더라도 명시성이 좋습니다.

💡 cargo tree 명령어로 의존성 트리를 확인하여 중복 버전이 없는지 주기적으로 점검하세요. 중복이 발견되면 workspace.dependencies로 통합할 기회입니다.

💡 dev-dependencies도 [workspace.dev-dependencies]로 공유할 수 있습니다. 테스트 라이브러리(criterion, proptest 등)를 통일하면 벤치마크와 테스트 코드를 크레이트 간 재사용하기 쉽습니다.


5. 워크스페이스 빌드 전략

시작하며

여러분이 큰 워크스페이스에서 작업하다가 이런 좌절을 느껴본 적 있나요? 한 줄의 코드를 고쳤는데 cargo build가 5분이나 걸려서, 작은 수정을 확인하는 것조차 고통스러운 상황 말이죠.

이런 문제는 워크스페이스가 커질수록 심각해집니다. 모든 크레이트가 매번 재컴파일되고, 의존성 체인이 복잡해지면서 증분 빌드의 이점이 사라집니다.

특히 CI 환경에서는 캐시 전략이 없으면 매번 처음부터 빌드하게 되어 파이프라인이 느려집니다. 바로 이럴 때 필요한 것이 체계적인 워크스페이스 빌드 전략입니다.

증분 빌드, 병렬 컴파일, 캐싱을 최적화하면 개발 속도가 극적으로 향상됩니다.

개요

간단히 말해서, 워크스페이스 빌드 전략은 컴파일 시간을 최소화하고 빌드 결과물을 효율적으로 재사용하기 위한 설정과 워크플로우의 조합입니다. 왜 이것이 중요한지 실무 관점에서 설명하자면, 빌드 시간은 개발자 경험의 핵심입니다.

예를 들어, 빌드가 30초 이내에 완료되면 개발자는 집중력을 유지하면서 빠르게 반복 작업을 할 수 있지만, 5분이 걸리면 다른 일을 하다가 컨텍스트 스위칭 비용이 발생합니다. 이것은 생산성에 직접적인 영향을 미칩니다.

기존에는 cargo build만 반복적으로 실행하고 느린 빌드를 당연하게 받아들였다면, 이제는 cargo build --package, cargo check, sccache 같은 도구를 전략적으로 조합하여 빌드 시간을 10분의 1로 줄일 수 있습니다. 워크스페이스 빌드 최적화의 핵심 요소는 첫째, 선택적 빌드 - 변경된 크레이트만 재컴파일, 둘째, 병렬화 - 독립적인 크레이트를 동시에 빌드, 셋째, 캐싱 - 이전 빌드 결과 재사용, 넷째, 프로파일 설정 - 개발과 릴리스에 맞는 최적화 레벨 적용입니다.

이러한 요소들이 조화를 이루면 대규모 워크스페이스도 빠르게 개발할 수 있습니다.

코드 예제

# .cargo/config.toml - 워크스페이스 빌드 설정
[build]
# 병렬 빌드 작업 수 (CPU 코어 수에 맞춰 조정)
jobs = 8

# 증분 컴파일 활성화 (기본값이지만 명시적으로 표시)
incremental = true

# 루트 Cargo.toml - 빌드 프로파일 최적화
[profile.dev]
# 개발 중에는 최소한의 최적화로 빌드 속도 우선
opt-level = 0
# 디버그 정보는 line-tables-only로 충분 (빌드 시간 단축)
debug = "line-tables-only"

[profile.dev.package."*"]
# 의존성은 최적화하여 실행 속도 개선 (한 번만 컴파일됨)
opt-level = 3

설명

이것이 하는 일: 워크스페이스 빌드 전략은 Rust 컴파일러의 강력한 기능들을 프로젝트에 맞게 튜닝하여, 개발 경험과 성능 사이의 최적 균형점을 찾습니다. 첫 번째로, .cargo/config.toml 파일은 프로젝트별 Cargo 설정을 담는 핵심 파일입니다.

jobs = 8 설정은 최대 8개의 크레이트를 동시에 컴파일한다는 의미입니다. 워크스페이스에서 의존성이 없는 크레이트들(예: 여러 독립적인 마이크로서비스들)은 병렬로 빌드되므로, 멀티코어 CPU를 최대한 활용합니다.

여러분의 CPU 코어 수에 맞춰 이 값을 조정하면 됩니다. incremental = true는 변경되지 않은 함수나 모듈을 재컴파일하지 않는 증분 컴파일을 활성화합니다.

그 다음으로, [profile.dev] 섹션이 개발 중 빌드 전략의 핵심입니다. opt-level = 0은 최적화를 하지 않는다는 의미인데, 이것이 컴파일 시간을 극적으로 단축시킵니다.

최적화는 코드 변환 과정이 복잡하고 시간이 오래 걸리므로, 개발 중에는 생략하고 빠르게 빌드하는 것이 낫습니다. debug = "line-tables-only"는 전체 디버그 정보 대신 스택 트레이스에 필요한 최소한의 정보만 포함하여, 디버그 기호 크기를 줄이고 링킹 시간을 단축합니다.

세 번째로, [profile.dev.package."*"] 섹션은 매우 영리한 최적화입니다. 이것은 "내 코드는 opt-level 0으로 빠르게 빌드하되, 외부 의존성(tokio, serde 등)은 opt-level 3으로 최적화해서 컴파일하라"는 의미입니다.

외부 의존성은 여러분이 수정하지 않으므로 한 번만 컴파일되고 캐시됩니다. 하지만 실행 시에는 최적화된 코드가 돌아가므로, 애플리케이션 성능은 좋습니다.

이것은 "빠른 빌드"와 "빠른 실행" 사이의 완벽한 타협점입니다. 네 번째로, 실제 빌드 명령어도 전략적으로 선택할 수 있습니다.

cargo check는 컴파일 체크만 하고 실행 파일을 생성하지 않아서 cargo build보다 훨씬 빠릅니다. 타입 에러를 확인할 때는 cargo check를, 실제 실행이 필요할 때만 cargo build를 사용하는 습관이 시간을 절약합니다.

cargo build --package api-server처럼 특정 크레이트만 빌드할 수도 있습니다. 여러분이 이 전략들을 사용하면 일반적인 "코드 수정 → 빌드 → 테스트" 사이클이 몇 초 이내로 완료됩니다.

특히 증분 컴파일이 제대로 작동하면, 한 파일만 수정했을 때는 그 파일과 의존하는 크레이트만 재컴파일되므로 거의 즉각적입니다. CI 환경에서는 sccache나 GitHub Actions의 cache 액션을 추가로 활용하여 빌드 결과물을 캐싱하면, 풀 리퀘스트마다 처음부터 빌드할 필요가 없어집니다.

실전 팁

💡 cargo clean은 최후의 수단입니다. 증분 빌드가 이상하게 작동한다고 느껴질 때만 사용하세요. 대부분의 경우 불필요합니다.

💡 개발 중에는 cargo run 대신 cargo watch를 사용하세요(cargo-watch 설치 필요). 파일이 변경되면 자동으로 재빌드하여 생산성이 크게 향상됩니다.

💡 링크 시간이 오래 걸린다면 lld 또는 mold 같은 빠른 링커로 교체하세요. .cargo/config.toml에 linker 설정을 추가하면 됩니다.

💡 릴리스 빌드([profile.release])에서는 lto = "thin"과 codegen-units = 16 설정으로 빌드 시간과 최적화를 균형있게 조절할 수 있습니다.

💡 cargo build --timings 옵션으로 어떤 크레이트가 빌드 시간을 많이 차지하는지 시각화된 리포트를 볼 수 있습니다. 병목 지점을 찾는 데 매우 유용합니다.


6. 크레이트 간 코드 공유

시작하며

여러분이 워크스페이스에서 개발하다가 이런 상황을 겪어본 적 있나요? API 서버와 CLI 도구에서 똑같은 데이터베이스 모델 코드를 복사-붙여넣기 하고 있고, 나중에 스키마가 바뀌면 두 곳을 다 고쳐야 하는 상황 말이죠.

이런 문제는 모노레포의 장점을 전혀 살리지 못하는 전형적인 안티패턴입니다. 코드 중복은 버그의 원인이 되고, 변경할 때마다 여러 곳을 수정해야 하므로 실수할 가능성이 높습니다.

"DRY(Don't Repeat Yourself)" 원칙을 위반하는 것이죠. 바로 이럴 때 필요한 것이 체계적인 크레이트 간 코드 공유 전략입니다.

공통 로직을 별도 크레이트로 분리하고 내부 의존성으로 연결하면 코드 재사용성이 극대화됩니다.

개요

간단히 말해서, 크레이트 간 코드 공유는 공통 기능을 라이브러리 크레이트로 추출하고, 이것을 여러 애플리케이션 크레이트에서 참조하는 패턴입니다. 왜 이 패턴이 중요한지 실무 관점에서 설명하자면, 이것이 모노레포의 핵심 가치입니다.

예를 들어, 사용자 인증 로직을 공유 라이브러리로 만들면, 웹 API, 관리자 대시보드, 백그라운드 워커가 모두 동일한 인증 메커니즘을 사용하게 됩니다. 보안 패치가 필요할 때 한 곳만 고치면 모든 서비스에 반영됩니다.

기존에는 공통 코드를 각 프로젝트에 복사하거나 별도의 git 저장소로 분리해서 버전 관리를 해야 했다면, 이제는 같은 워크스페이스 안에서 path 의존성으로 즉시 공유할 수 있습니다. 크레이트 간 코드 공유의 핵심 패턴은 첫째, 공유 라이브러리 크레이트(models, utils, core 등), 둘째, 레이어드 아키텍처(하위 레이어를 상위 레이어가 참조), 셋째, feature flag를 통한 선택적 기능 활성화입니다.

이러한 패턴들이 코드베이스의 구조를 명확하고 유지보수 가능하게 만듭니다.

코드 예제

// libs/common/src/lib.rs - 공유 라이브러리
// 모든 애플리케이션에서 사용할 공통 타입과 함수
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
    pub id: i64,
    pub email: String,
    pub name: String,
}

// 공통 유틸리티 함수
pub fn validate_email(email: &str) -> bool {
    email.contains('@') && email.len() > 5
}

// apps/api-server/Cargo.toml - 공유 라이브러리 사용
[dependencies]
common = { path = "../../libs/common" }

// apps/api-server/src/main.rs - 공유 코드 활용
use common::{User, validate_email};

설명

이것이 하는 일: 크레이트 간 코드 공유 시스템은 워크스페이스를 진정한 모노레포로 만들어주는 핵심 메커니즘입니다. 코드의 재사용성과 일관성을 동시에 확보합니다.

첫 번째로, libs/common 크레이트는 프로젝트의 "공통 어휘"를 정의합니다. User 구조체는 API 서버, CLI 도구, 데이터베이스 마이그레이션 스크립트 등 모든 곳에서 동일한 정의를 사용합니다.

#[derive(Serialize, Deserialize)]를 통해 이 타입은 JSON, 데이터베이스, 네트워크 전송 등 다양한 컨텍스트에서 사용할 수 있습니다. 이것은 "User"라는 개념이 프로젝트 전체에서 일관되게 표현되도록 보장합니다.

그 다음으로, validate_email 같은 유틸리티 함수도 공유됩니다. 이메일 검증 로직이 API 서버에서도 같고, CLI 도구에서도 같고, 관리자 대시보드에서도 같습니다.

나중에 검증 규칙을 강화하고 싶을 때(예: 정규표현식 추가) common/src/lib.rs 한 곳만 수정하면 모든 애플리케이션이 자동으로 새 로직을 사용합니다. 이것이 버그 수정과 개선을 얼마나 쉽게 만드는지 상상해보세요.

세 번째로, path = "../../libs/common" 의존성이 워크스페이스 내부 참조를 만듭니다. 이것은 외부 crates.io 크레이트가 아니라 같은 저장소의 다른 디렉토리를 가리킵니다.

Cargo는 이것을 자동으로 인식하여, common 크레이트를 먼저 빌드한 다음 api-server를 빌드합니다. 의존성 그래프가 자동으로 해석되므로, 여러분은 빌드 순서를 신경 쓸 필요가 없습니다.

네 번째로, use common::{User, validate_email}; 구문으로 공유 코드를 마치 외부 라이브러리처럼 사용합니다. IDE의 자동완성과 go-to-definition도 완벽하게 작동합니다.

api-server에서 User 타입을 Ctrl+클릭하면 libs/common/src/lib.rs의 정의로 바로 이동합니다. 이것은 개발 경험을 크게 향상시킵니다.

여러분이 이 패턴을 사용하면 리팩토링이 안전해집니다. User 구조체에 새 필드를 추가하면, 컴파일러가 해당 필드를 사용하지 않는 모든 코드에 경고를 표시합니다.

타입 시스템이 변경 사항을 추적해주므로, "어디에서 이 타입을 쓰고 있었지?"라고 고민할 필요가 없습니다. 테스트도 쉬워집니다 - common 크레이트의 유닛 테스트로 핵심 로직을 검증하고, 애플리케이션 크레이트는 통합 테스트에 집중할 수 있습니다.

실전 팁

💡 공유 라이브러리는 의존성을 최소화하세요. common이 tokio나 axum 같은 무거운 의존성을 가지면, CLI 도구까지 불필요하게 포함하게 됩니다. 필요시 여러 개의 작은 공유 라이브러리로 분리하세요.

💡 순환 의존성을 피하세요. A가 B를 참조하고 B가 A를 참조하면 컴파일 에러가 발생합니다. 의존성 그래프는 항상 방향성 비순환 그래프(DAG)여야 합니다.

💡 공유 라이브러리의 pub API는 신중하게 설계하세요. 한번 공개한 API를 변경하면 모든 의존하는 크레이트를 수정해야 합니다. 처음부터 명확한 인터페이스를 만드는 것이 좋습니다.

💡 feature flag를 활용하여 선택적 기능을 제공하세요. 예를 들어 common = { path = "...", features = ["database"] }처럼 데이터베이스 관련 타입은 feature로 감싸면, 필요 없는 크레이트는 포함하지 않을 수 있습니다.

💡 공유 라이브러리에 예제를 추가하세요(examples/ 디렉토리). 다른 팀원이 어떻게 사용하는지 빠르게 이해할 수 있습니다.


7. 워크스페이스 테스트

시작하며

여러분이 워크스페이스에 새로운 크레이트를 추가한 후 이런 불안감을 느껴본 적 있나요? "내가 방금 고친 코드가 다른 크레이트를 망가뜨리진 않았을까?" 크레이트가 많아질수록 이 두려움은 커집니다.

이런 문제는 통합된 테스트 전략이 없을 때 발생합니다. 각 크레이트를 개별적으로 테스트하면 놓치는 부분이 생기고, 크레이트 간 상호작용에서 발생하는 버그를 잡기 어렵습니다.

특히 공유 라이브러리를 수정했을 때 영향 범위를 파악하기 힘듭니다. 바로 이럴 때 필요한 것이 체계적인 워크스페이스 테스트 전략입니다.

한 번의 명령으로 전체 워크스페이스를 테스트하고, 각 크레이트의 역할에 맞는 테스트 스타일을 적용하면 안정성이 크게 향상됩니다.

개요

간단히 말해서, 워크스페이스 테스트는 모든 멤버 크레이트의 유닛 테스트, 통합 테스트, 문서 테스트를 통합적으로 실행하고 관리하는 시스템입니다. 왜 이것이 중요한지 실무 관점에서 설명하자면, 테스트는 리팩토링의 안전망입니다.

예를 들어, 공통 라이브러리의 함수 시그니처를 변경했을 때, 전체 워크스페이스 테스트를 실행하면 어떤 크레이트가 영향받는지 즉시 알 수 있습니다. CI 파이프라인에서도 전체 테스트를 자동화하여 배포 전에 문제를 잡아냅니다.

기존에는 각 크레이트 디렉토리에 들어가서 cargo test를 일일이 실행했다면, 이제는 워크스페이스 루트에서 한 번의 명령으로 모든 테스트를 실행할 수 있습니다. 워크스페이스 테스트 전략의 핵심 요소는 첫째, 계층별 테스트 - 유닛/통합/E2E 테스트의 적절한 분배, 둘째, 테스트 격리 - 각 크레이트의 테스트가 서로 간섭하지 않도록, 셋째, 공유 테스트 유틸리티 - 테스트 헬퍼와 픽스처의 재사용입니다.

이러한 요소들이 버그를 조기에 발견하고 회귀를 방지합니다.

코드 예제

// libs/common/src/lib.rs - 유닛 테스트 포함
pub fn validate_email(email: &str) -> bool {
    email.contains('@') && email.len() > 5
}

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

    #[test]
    fn test_valid_email() {
        assert!(validate_email("test@example.com"));
    }

    #[test]
    fn test_invalid_email() {
        assert!(!validate_email("invalid"));
        assert!(!validate_email("a@b"));  // 너무 짧음
    }
}

// apps/api-server/tests/integration_test.rs - 통합 테스트
use common::validate_email;

#[test]
fn test_email_validation_integration() {
    // 실제 사용 시나리오 테스트
    let user_input = "user@domain.com";
    assert!(validate_email(user_input));
}

설명

이것이 하는 일: 워크스페이스 테스트 시스템은 다층 방어선을 구축하여 버그가 프로덕션에 도달하기 전에 여러 단계에서 잡아냅니다. 첫 번째로, 유닛 테스트는 개별 함수와 모듈의 정확성을 검증합니다.

#[cfg(test)] mod tests { ... } 블록은 테스트 빌드에만 포함되고 릴리스 빌드에서는 제외됩니다.

validate_email 함수 바로 아래에 테스트를 작성하면, 코드와 테스트를 함께 볼 수 있어 유지보수가 쉽습니다. use super::*;로 부모 모듈의 모든 항목을 가져오므로, private 함수도 테스트할 수 있습니다.

이것은 구현 세부사항을 검증하는 화이트박스 테스트에 적합합니다. 그 다음으로, 통합 테스트는 크레이트를 외부 사용자 관점에서 테스트합니다.

tests/ 디렉토리의 각 파일은 별도의 바이너리로 컴파일되어, 마치 외부 크레이트에서 사용하듯이 테스트합니다. use common::validate_email;처럼 public API만 사용할 수 있으므로, 크레이트의 인터페이스가 제대로 설계되었는지 검증합니다.

이것은 블랙박스 테스트로, 실제 사용 시나리오를 시뮬레이션합니다. 세 번째로, 워크스페이스 루트에서 cargo test를 실행하면 Cargo가 의존성 순서대로 모든 크레이트의 테스트를 실행합니다.

먼저 common 크레이트의 테스트가 통과한 후, 그것을 의존하는 api-server의 테스트가 실행됩니다. 만약 common의 테스트가 실패하면, 의존하는 크레이트들의 테스트는 건너뜁니다.

이것은 실패의 근본 원인을 빠르게 파악하게 해줍니다. 네 번째로, cargo test --workspace 옵션을 명시적으로 사용하면 모든 멤버를 강제로 테스트합니다.

cargo test --package common처럼 특정 크레이트만 테스트할 수도 있습니다. 빠른 피드백이 필요할 때는 변경한 크레이트만 테스트하고, 커밋 전이나 CI에서는 전체를 테스트하는 전략이 효율적입니다.

여러분이 이 테스트 전략을 사용하면 리팩토링이 자신감 있게 진행됩니다. 공통 라이브러리의 함수 시그니처를 변경하면, 컴파일 에러와 테스트 실패가 정확히 어떤 부분을 수정해야 하는지 알려줍니다.

코드 리뷰에서도 "이 변경이 다른 부분에 영향을 주나요?"라는 질문에 "모든 테스트가 통과했습니다"라고 자신있게 답할 수 있습니다. CI 파이프라인에서 테스트가 자동화되면, 풀 리퀘스트마다 전체 워크스페이스의 건강 상태를 확인하여 회귀를 방지합니다.

실전 팁

💡 테스트 실행 시 cargo test -- --test-threads=1 옵션으로 병렬 실행을 막을 수 있습니다. 데이터베이스 같은 공유 리소스를 사용하는 테스트에 유용합니다.

💡 cargo test --doc으로 문서의 코드 예제도 테스트하세요. README나 API 문서의 예제가 실제로 작동하는지 자동으로 검증됩니다.

💡 CI에서는 cargo test --all-features로 모든 feature 조합을 테스트하세요. 특정 feature 조합에서만 발생하는 버그를 잡을 수 있습니다.

💡 테스트 커버리지를 측정하고 싶다면 cargo-tarpaulin이나 cargo-llvm-cov를 사용하세요. 어떤 코드가 테스트되지 않았는지 시각화됩니다.

💡 공통 테스트 유틸리티는 별도의 test-utils 크레이트로 분리하고, dev-dependencies로 참조하세요. 테스트 픽스처와 목 객체를 재사용할 수 있습니다.


8. 릴리스 관리

시작하며

여러분이 워크스페이스의 여러 크레이트를 배포하려고 할 때 이런 혼란을 겪어본 적 있나요? "api-server는 버전 0.3.0인데 common은 0.2.1이고...

이들을 어떻게 함께 릴리스해야 하지?" 버전 관리가 복잡해집니다. 이런 문제는 워크스페이스가 커질수록 심각해집니다.

각 크레이트의 버전을 독립적으로 관리하면 호환성 문제가 생기고, 모두 동일한 버전을 쓰면 작은 변경에도 전체 버전이 올라가는 비효율이 발생합니다. 특히 일부 크레이트를 crates.io에 배포할 때는 더 복잡해집니다.

바로 이럴 때 필요한 것이 체계적인 릴리스 관리 전략입니다. 버저닝 정책과 자동화 도구를 활용하면 배포 과정이 안전하고 예측 가능해집니다.

개요

간단히 말해서, 릴리스 관리는 워크스페이스 내 크레이트들의 버전을 일관되게 유지하고, 변경 사항을 추적하며, 배포 프로세스를 자동화하는 일련의 활동입니다. 왜 이것이 중요한지 실무 관점에서 설명하자면, 버전 관리는 사용자와의 약속입니다.

예를 들어, API 크레이트가 breaking change를 포함하는지, 단순 버그 수정인지를 버전 번호로 알려줘야 합니다. Semantic Versioning(SemVer)을 따르면 사용자가 안전하게 업데이트할 수 있습니다.

기존에는 각 Cargo.toml의 version 필드를 수동으로 수정하고, git tag를 만들고, crates.io에 배포하는 과정을 일일이 했다면, 이제는 cargo-release 같은 도구로 이 모든 과정을 자동화할 수 있습니다. 릴리스 관리의 핵심 요소는 첫째, 통일된 버저닝 전략 (모노버전 vs 독립버전), 둘째, CHANGELOG 자동 생성, 셋째, 태그와 브랜치 전략, 넷째, CI/CD 파이프라인과의 통합입니다.

이러한 요소들이 배포를 안전하고 추적 가능하게 만듭니다.

코드 예제

// 워크스페이스 루트 Cargo.toml - 통일된 버전 관리
[workspace.package]
version = "0.3.0"  // 워크스페이스 전체 버전
edition = "2021"

[workspace.metadata.release]
# cargo-release 설정
sign-commit = true       // 커밋에 GPG 서명
push-remote = "origin"   // 푸시할 리모트
allow-branch = ["main"]  // 릴리스 가능한 브랜치

# 멤버 크레이트 - 버전 상속
[package]
name = "api-server"
version.workspace = true  // 워크스페이스 버전 사용

# 릴리스 명령 예시
# cargo release patch  // 패치 버전 올리기 (0.3.0 -> 0.3.1)
# cargo release minor  // 마이너 버전 올리기 (0.3.1 -> 0.4.0)
# cargo release major  // 메이저 버전 올리기 (0.4.0 -> 1.0.0)

설명

이것이 하는 일: 릴리스 관리 시스템은 워크스페이스의 여러 크레이트를 조화롭게 진화시키는 오케스트레이션 메커니즘입니다. 변경 사항을 추적하고 일관된 배포를 보장합니다.

첫 번째로, [workspace.package]의 version = "0.3.0" 설정이 워크스페이스 전체의 단일 버전을 정의합니다. 이것을 "모노버전" 전략이라고 하는데, 모든 멤버 크레이트가 동일한 버전 번호를 공유합니다.

이 방식의 장점은 간단함입니다 - "이 프로젝트의 버전은 0.3.0이다"라고 명확하게 말할 수 있습니다. 각 멤버 크레이트에서 version.workspace = true로 이 버전을 상속받으므로, 버전을 올릴 때 한 곳만 수정하면 됩니다.

그 다음으로, [workspace.metadata.release] 섹션은 cargo-release 도구의 설정입니다. sign-commit = true는 릴리스 커밋에 GPG 서명을 추가하여 보안과 진위성을 보장합니다.

allow-branch = ["main"]은 실수로 feature 브랜치에서 릴리스하는 것을 방지합니다. 이러한 안전장치들이 릴리스 과정의 실수를 줄여줍니다.

세 번째로, cargo release 명령어가 릴리스 프로세스를 자동화합니다. cargo release patch를 실행하면, 도구가 다음 작업을 순차적으로 수행합니다: (1) 버전 번호를 0.3.0에서 0.3.1로 업데이트, (2) 변경사항을 커밋, (3) git 태그 생성(v0.3.1), (4) 원격 저장소에 푸시, (5) 선택적으로 crates.io에 배포.

이 모든 과정이 한 번의 명령으로 완료됩니다. 네 번째로, Semantic Versioning 규칙이 적용됩니다.

patch는 버그 수정과 하위 호환 가능한 작은 변경, minor는 새로운 기능 추가(하위 호환 유지), major는 breaking change입니다. 예를 들어, 공개 API의 함수 시그니처를 변경했다면 major 버전을 올려야 하고, 새로운 유틸리티 함수를 추가한 것뿐이라면 minor로 충분합니다.

이 규칙을 따르면 사용자가 안전하게 업데이트할 수 있습니다. 다섯 번째로, CHANGELOG.md 파일을 자동으로 생성하거나 업데이트할 수 있습니다.

git-cliff나 conventional commits 스타일을 사용하면, 커밋 메시지에서 자동으로 변경 내역을 추출하여 문서화합니다. "Added: 새로운 기능", "Fixed: 버그 수정", "Breaking: 호환성 깨지는 변경" 같은 섹션으로 구조화된 릴리스 노트가 생성됩니다.

여러분이 이 시스템을 사용하면 릴리스가 두렵지 않게 됩니다. 수동 단계가 줄어들어 실수 가능성이 낮아지고, 모든 릴리스가 일관된 프로세스를 거치므로 신뢰성이 높아집니다.

CI/CD 파이프라인에서 cargo release를 자동화하면, main 브랜치에 머지되면 자동으로 새 버전이 배포되는 완전 자동화도 가능합니다. 사용자들도 명확한 버전 번호와 CHANGELOG를 통해 무엇이 변경되었는지 쉽게 파악할 수 있습니다.

실전 팁

💡 릴리스 전에 cargo release --dry-run으로 시뮬레이션하세요. 실제 변경 없이 어떤 작업이 수행될지 미리 볼 수 있습니다.

💡 pre-release 버전(0.3.0-alpha.1)을 사용하여 테스트 배포를 할 수 있습니다. cargo release --pre-release alpha로 생성할 수 있습니다.

💡 일부 크레이트만 배포하고 싶다면 모노버전 대신 독립 버전 전략을 고려하세요. 각 크레이트가 자신의 version 필드를 가지면 됩니다.

💡 GitHub Actions나 GitLab CI에서 릴리스를 자동화할 때는 CARGO_REGISTRY_TOKEN을 시크릿으로 저장하여 crates.io 배포를 인증하세요.

💡 workspace.dependencies에 version을 명시했다면, 릴리스 시 내부 의존성 버전도 자동으로 업데이트됩니다. cargo-release가 이것을 처리하므로 걱정하지 마세요.


#Rust#Workspace#Monorepo#CargoToml#CrateManagement