cargo workspace 모노레포 | Cargo.toml 구조·멤버·공통 의존성·빌드 최적화
이 글의 핵심
Rust Cargo workspace로 크레이트를 한 저장소에서 묶는 법: 루트 매니페스트, 멤버 추가, workspace.dependencies, 빌드 캐시까지 실무 팁을 정리합니다.
들어가며
Cargo workspace는 여러 패키지(크레이트)를 하나의 저장소에서 함께 빌드·테스트하기 위한 공식적인 모노레포 패턴입니다. Cargo.lock은 워크스페이스 루트에 하나만 두는 것이 일반적이며, 공유 target/ 덕분에 의존성 그래프가 겹칠 때 컴파일량을 줄일 수 있습니다.
CLI·라이브러리·프로토콜 크레이트를 나눈 팀, 혹은 내부 공유 크레이트를 여러 서비스가 참조하는 팀에서 특히 자주 씁니다. 이 글은 cargo workspace 모노레포 키워드에 맞춰 Cargo.toml 구조, 멤버 관리, 공통 의존성, 빌드 최적화를 실무 관점에서 정리합니다.
이 글을 읽으면
- Cargo workspace의 루트 매니페스트 구조를 이해합니다
- 멤버 추가·공통 의존성 설정 방법을 익힙니다
- 빌드 최적화·CI 전략을 적용할 수 있습니다
개념: workspace 루트와 멤버
Workspace란?
- 루트
Cargo.toml에[workspace]섹션이 있으며,members에 하위 크레이트 경로가 나열됩니다. - 워크스페이스에 속한 패키지들은 동일한
Cargo.lock정책 아래에서 의존성 버전을 맞추기 쉽습니다. - 기본 패키지(root package)를 두지 않고 메타만 있는 virtual manifest로 두는 팀도 많습니다.
구조 다이어그램
my-workspace/
├── Cargo.toml (workspace 루트)
├── Cargo.lock (공유)
├── target/ (공유 빌드 출력)
├── crates/
│ ├── api/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── main.rs
│ ├── core/
│ │ ├── Cargo.toml
│ │ └── src/
│ │ └── lib.rs
│ └── utils/
│ ├── Cargo.toml
│ └── src/
│ └── lib.rs
└── README.md
핵심 개념
| 항목 | 설명 |
|---|---|
| Virtual Manifest | 루트에 [package] 없이 [workspace]만 있는 구조 |
| Members | 워크스페이스에 속한 크레이트 목록 |
| Shared Lock | 모든 멤버가 동일한 Cargo.lock 사용 |
| Shared Target | 컴파일 결과물을 target/에 공유 |
| Workspace Dependencies | 공통 의존성 버전 관리 |
실전: 최소 레이아웃과 명령
1) 워크스페이스 생성
디렉터리 구조 생성
# 프로젝트 루트 생성
mkdir my-workspace
cd my-workspace
# 멤버 크레이트 생성
cargo new --lib crates/core
cargo new --bin crates/api
cargo new --lib crates/utils
루트 Cargo.toml 작성
[workspace]
resolver = "2"
members = [
"crates/api",
"crates/core",
"crates/utils"
]
[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Your Name <you@example.com>"]
license = "MIT"
repository = "https://github.com/org/my-workspace"
[workspace.dependencies]
# 공통 의존성
serde = { version = "1.0", features = [derive] }
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
anyhow = "1.0"
tracing = "0.1"
2) 멤버 크레이트 설정
crates/core/Cargo.toml
[package]
name = "core"
version.workspace = true
edition.workspace = true
authors.workspace = true
license.workspace = true
[dependencies]
serde.workspace = true
anyhow.workspace = true
crates/api/Cargo.toml
[package]
name = "api"
version.workspace = true
edition.workspace = true
[dependencies]
# 워크스페이스 내부 크레이트
core = { path = "../core" }
utils = { path = "../utils" }
# 워크스페이스 공통 의존성
tokio.workspace = true
serde.workspace = true
anyhow.workspace = true
# api 전용 의존성
axum = "0.7"
tower = "0.4"
crates/utils/Cargo.toml
[package]
name = "utils"
version.workspace = true
edition.workspace = true
[dependencies]
tracing.workspace = true
3) 코드 예제
crates/core/src/lib.rs
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct User {
pub id: u64,
pub name: String,
pub email: String,
}
impl User {
pub fn new(id: u64, name: String, email: String) -> Self {
Self { id, name, email }
}
}
crates/utils/src/lib.rs
use tracing::info;
pub fn log_startup(service_name: &str) {
info!("Starting service: {}", service_name);
}
crates/api/src/main.rs
use axum::{
routing::get,
Router,
Json,
};
use core::User;
use utils::log_startup;
#[tokio::main]
async fn main() {
log_startup("API Server");
let app = Router::new()
.route("/users", get(get_users));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.unwrap();
axum::serve(listener, app).await.unwrap();
}
async fn get_users() -> Json<Vec<User>> {
let users = vec![
User::new(1, "Alice".to_string(), "alice@example.com".to_string()),
User::new(2, "Bob".to_string(), "bob@example.com".to_string()),
];
Json(users)
}
4) 자주 쓰는 명령
빌드 및 실행
# 전체 워크스페이스 검사
cargo check --workspace
# 전체 빌드
cargo build --workspace
# 전체 테스트
cargo test --workspace
# 특정 크레이트만 빌드
cargo build -p api
# 특정 크레이트만 테스트
cargo test -p core
# 특정 크레이트 실행
cargo run -p api
# 릴리스 빌드
cargo build -p api --release
의존성 관리
# 전체 의존성 트리 확인
cargo tree --workspace
# 특정 크레이트 의존성
cargo tree -p core
# 중복 의존성 확인
cargo tree --duplicates
# 의존성 업데이트
cargo update
# 특정 의존성만 업데이트
cargo update -p serde
정리 및 최적화
# 빌드 캐시 정리
cargo clean
# 특정 크레이트만 정리
cargo clean -p api
# 미사용 의존성 확인
cargo machete
# 포맷팅
cargo fmt --all
# Clippy (린터)
cargo clippy --workspace -- -D warnings
고급: workspace.dependencies·패치
1) 버전 단일화
workspace.dependencies에 올려 두고 멤버에서는 dep.workspace = true만 선언하면 serde·tokio 버전 불일치를 PR 단계에서 줄입니다.
루트 Cargo.toml
[workspace.dependencies]
# 버전 한 곳에서 관리
serde = { version = "1.0", features = [derive] }
tokio = { version = "1", features = ["rt-multi-thread", "macros", "net"] }
anyhow = "1.0"
thiserror = "1.0"
# 내부 크레이트도 정의 가능
core = { path = "crates/core" }
utils = { path = "crates/utils" }
멤버 Cargo.toml
[dependencies]
# workspace에서 상속
serde.workspace = true
tokio.workspace = true
# 내부 크레이트
core.workspace = true
utils.workspace = true
# 멤버 전용 의존성
axum = "0.7"
2) [patch] - 사내 포크 또는 긴급 패치
사내 포크 사용
# 루트 Cargo.toml
[patch.crates-io]
serde = { git = "https://github.com/company/serde-fork", branch = "custom-feature" }
효과: 모든 멤버가 자동으로 패치된 버전 사용
로컬 패치
[patch.crates-io]
tokio = { path = "../tokio-local" }
사용 시나리오
-
업스트림 버그 긴급 수정
-
사내 커스터마이징
-
의존성 디버깅 주의사항
-
유지보수 부담 증가
-
업스트림 업데이트 추적 필요
-
기한 설정 권장
3) default-members
[workspace]
members = [crates/*]
default-members = [crates/api]
효과
cargo run시 기본 타깃 지정cargo build(인자 없음) 시 default-members만 빌드
4) exclude - 멤버 제외
[workspace]
members = [crates/*]
exclude = ["crates/experimental", "crates/deprecated"]
사용 시나리오
- 실험적 크레이트 제외
- 레거시 코드 격리
- 빌드 시간 단축
5) 프로파일 공유
# 루트 Cargo.toml
[profile.release]
opt-level = 3
lto = "fat"
codegen-units = 1
strip = true
[profile.dev]
opt-level = 0
debug = true
[profile.dev.package."*"]
opt-level = 2 # 의존성만 최적화
효과: 모든 멤버가 동일한 프로파일 사용
비교: 단일 크레이트·멀티 레포
| 항목 | 단일 크레이트 | Workspace | 멀티 레포 |
|---|---|---|---|
| 경계 | 모듈로 분리 | 크레이트로 분리 | 레포로 분리 |
| 빌드 캐시 | 단일 target | 공유 target | 레포별 독립 |
| 의존성 관리 | 단일 Cargo.toml | workspace.dependencies | 레포별 독립 |
| 버전 관리 | 단일 버전 | 멤버별 또는 공유 | 레포별 독립 |
| 권한 관리 | 레포 단위 | 레포 단위 | 레포별 세밀 |
| 릴리스 | 단일 릴리스 | 멤버별 또는 통합 | 레포별 독립 |
| 학습 곡선 | 낮음 | 중간 | 높음 |
선택 가이드
단일 크레이트 선택
-
✅ 작은 프로젝트 (< 10,000 LOC)
-
✅ 모듈 경계가 명확하지 않음
-
✅ 팀 규모 작음 (1-3명) Workspace 선택:
-
✅ 중대형 프로젝트
-
✅ 명확한 경계 (CLI, 라이브러리, 서버)
-
✅ 공통 코드 재사용
-
✅ 원자적 변경 필요 멀티 레포 선택
-
✅ 독립적 릴리스 주기
-
✅ 레포별 권한 분리
-
✅ 외부 공개 크레이트
실무 사례
사례 1: 공유 도메인 로직 + 여러 바이너리
구조:
my-app/
├── Cargo.toml
├── crates/
│ ├── core/ (공유 도메인 로직)
│ │ └── src/lib.rs
│ ├── api/ (REST API 서버)
│ │ └── src/main.rs
│ ├── worker/ (백그라운드 워커)
│ │ └── src/main.rs
│ └── cli/ (CLI 도구)
│ └── src/main.rs
루트 Cargo.toml:
# 실행 예제
[workspace]
members = [crates/*]
[workspace.dependencies]
core = { path = "crates/core" }
serde = { version = "1.0", features = [derive] }
tokio = { version = "1", features = [full] }
crates/api/Cargo.toml:
# 실행 예제
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
core.workspace = true
tokio.workspace = true
axum = "0.7"
crates/worker/Cargo.toml:
[package]
name = "worker"
version = "0.1.0"
edition = "2021"
[dependencies]
core.workspace = true
tokio.workspace = true
장점
core변경 시 모든 바이너리 자동 재빌드- 의존성 버전 일관성
- 공유
target/로 빌드 시간 단축
사례 2: FFI 바인딩 (sys + safe wrapper)
구조:
libfoo-rs/
├── Cargo.toml
├── crates/
│ ├── libfoo-sys/ (C 라이브러리 바인딩)
│ │ ├── Cargo.toml
│ │ ├── build.rs
│ │ └── src/lib.rs
│ └── libfoo/ (안전한 Rust 래퍼)
│ ├── Cargo.toml
│ └── src/lib.rs
libfoo-sys/Cargo.toml:
# 실행 예제
[package]
name = "libfoo-sys"
version = "0.1.0"
edition = "2021"
links = "foo"
[build-dependencies]
cc = "1.0"
libfoo-sys/build.rs:
// 함수 정의 및 구현
fn main() {
cc::Build::new()
.file("vendor/libfoo.c")
.compile("foo");
}
libfoo/Cargo.toml:
# 실행 예제
[package]
name = "libfoo"
version = "0.1.0"
edition = "2021"
[dependencies]
libfoo-sys = { path = "../libfoo-sys" }
libfoo/src/lib.rs:
use libfoo_sys::*;
pub struct Foo {
inner: *mut FooHandle,
}
impl Foo {
pub fn new() -> Self {
unsafe {
Self {
inner: foo_create(),
}
}
}
pub fn process(&self, data: &[u8]) -> Vec<u8> {
unsafe {
// 안전한 래퍼 제공
let result = foo_process(self.inner, data.as_ptr(), data.len());
// ...
vec![]
}
}
}
impl Drop for Foo {
fn drop(&mut self) {
unsafe {
foo_destroy(self.inner);
}
}
}
장점
-sys와 안전 래퍼를 함께 버전업- 내부 API 변경 시 원자적 커밋
사례 3: 마이크로서비스 모노레포
구조:
services/
├── Cargo.toml
├── crates/
│ ├── shared/ (공통 타입, 유틸)
│ ├── auth-service/
│ ├── user-service/
│ ├── order-service/
│ └── gateway/
루트 Cargo.toml:
[workspace]
members = [
"crates/shared",
"crates/auth-service",
"crates/user-service",
"crates/order-service",
"crates/gateway"
]
[workspace.dependencies]
shared = { path = "crates/shared" }
tokio = { version = "1", features = [full] }
tonic = "0.11"
prost = "0.12"
shared/src/lib.rs:
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserId(pub u64);
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct OrderId(pub u64);
pub mod error {
use thiserror::Error;
#[derive(Debug, Error)]
pub enum ServiceError {
#[error("Not found: {0}")]
NotFound(String),
#[error("Internal error: {0}")]
Internal(String),
}
}
auth-service/src/main.rs:
use shared::{UserId, error::ServiceError};
#[tokio::main]
async fn main() {
println!("Auth service started");
}
async fn authenticate(token: &str) -> Result<UserId, ServiceError> {
// 인증 로직
Ok(UserId(123))
}
CI 빌드 최적화:
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- uses: Swatinem/rust-cache@v2
with:
shared-key: "workspace"
- name: Check all
run: cargo check --workspace
- name: Test all
run: cargo test --workspace
- name: Clippy
run: cargo clippy --workspace -- -D warnings
트러블슈팅
문제 1: 멤버가 인식 안 됨
증상:
cargo build -p api
# error: package `api` is not a member of the workspace
원인
members경로 오타- 중첩 workspace (금지) 해결:
# 루트 Cargo.toml
[workspace]
members = [
"crates/api", # 경로 확인
"crates/core"
]
확인:
# 워크스페이스 멤버 목록
cargo metadata --no-deps | jq '.workspace_members'
문제 2: 의존성 버전 충돌
증상:
error: failed to select a version for `serde`
원인: 멤버들이 서로 다른 major 버전 지정 해결:
# 루트 Cargo.toml
[workspace.dependencies]
serde = "1.0" # 버전 통일
# 멤버 Cargo.toml
[dependencies]
serde.workspace = true # 루트 버전 사용
문제 3: 빌드 캐시 비대
증상: target/ 디렉터리가 수 GB
원인
- 불필요한 feature 활성화
- 여러 프로파일 빌드
- 미사용 의존성 해결:
# 정기적 정리
cargo clean
# 특정 프로파일만 정리
cargo clean --release
# 미사용 의존성 제거
cargo machete
sccache 사용:
# 설치
cargo install sccache
# 환경 변수 설정
export RUSTC_WRAPPER=sccache
# 빌드 (캐시 공유)
cargo build --workspace
문제 4: 테스트 시간 과다
증상: cargo test --workspace 실행 시 수 분 소요
해결 1: nextest 사용
# 설치
cargo install cargo-nextest
# 병렬 테스트
cargo nextest run --workspace
해결 2: 변경된 크레이트만 테스트
# Git diff 기반
CHANGED=$(git diff --name-only HEAD~1 | grep '^crates/' | cut -d'/' -f2 | sort -u)
for crate in $CHANGED; do
cargo test -p $crate
done
문제 5: 순환 의존성
증상:
error: cyclic package dependency: package `api` depends on itself
원인: A → B → A 순환
해결: 공통 타입을 세 번째 크레이트로 추출
Before:
api → core → api (순환!)
After:
api → core → shared
↓
shared
예시:
# shared/Cargo.toml
[package]
name = "shared"
# core/Cargo.toml
[dependencies]
shared = { path = "../shared" }
# api/Cargo.toml
[dependencies]
shared = { path = "../shared" }
core = { path = "../core" }
마무리
Cargo workspace는 Rust에서 모노레포를 공식 패턴으로 가져가는 방법이며, workspace.dependencies와 좁은 -p 빌드만으로도 운영 체감이 크게 좋아집니다.
핵심 요약
- 구조
- 루트에
[workspace]선언 - 멤버 크레이트는
members배열에 나열 - 공유
Cargo.lock과target/
- 루트에
- 의존성 관리
workspace.dependencies로 버전 통일- 멤버는
.workspace = true로 상속 [patch]로 긴급 수정
- 빌드 최적화
-p옵션으로 특정 크레이트만 빌드sccache로 캐시 공유cargo nextest로 병렬 테스트
- CI/CD
- 변경된 크레이트만 빌드
- 공유 캐시 키 사용
- Clippy·fmt 전체 실행
다음 단계
- Rust 입문: Rust 시작하기
- 타입 시스템: String vs &str
- 프로젝트 구조: Rust 프로젝트 구조 가이드
Cargo workspace는 Rust 모노레포의 표준입니다. 처음에는 복잡해 보이지만,
workspace.dependencies와 공유target/의 이점을 경험하면 다시 돌아가기 어렵습니다.
자주 묻는 질문 (FAQ)
Q. 이 내용을 실무에서 언제 쓰나요?
A. Rust Cargo workspace로 크레이트를 한 저장소에서 묶는 법: 루트 매니페스트, 멤버 추가, workspace.dependencies, 빌드 캐시까지 실무 팁을 정리합니다. Start now. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.
Q. 선행으로 읽으면 좋은 글은?
A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.
Q. 더 깊이 공부하려면?
A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.
같이 보면 좋은 글 (내부 링크)
이 주제와 연결되는 다른 글입니다.
- CMake 에러 완벽 해결 가이드 | 빌드 실패·의존성·링커 에러 트러블슈팅
- C++ Conan 완벽 가이드 | 현대적인 C++ 패키지 관리
- C++ 링킹 완벽 가이드 | 정적/동적 링킹과 에러 해결 (undefined reference)
이 글에서 다루는 키워드 (관련 검색어)
Rust, Cargo, Workspace, 모노레포, 의존성 관리, 빌드 등으로 검색하시면 이 글이 도움이 됩니다.