[2026] cargo workspace 모노레포 | Cargo.toml 구조·멤버·공통 의존성·빌드 최적화
이 글의 핵심
Rust Cargo workspace로 크레이트를 한 저장소에서 묶는 법: 루트 매니페스트, 멤버 추가, workspace.dependencies, 빌드 캐시까지 실무 팁을 정리합니다.
들어가며
Cargo workspace는 여러 패키지(크레이트)를 하나의 저장소에서 함께 빌드·테스트하기 위한 공식적인 모노레포 패턴입니다. Cargo.lock은 워크스페이스 루트에 하나만 두는 것이 일반적이며, 공유 target/ 덕분에 의존성 그래프가 겹칠 때 컴파일량을 줄일 수 있습니다.
CLI·라이브러리·프로토콜 크레이트를 나눈 팀, 혹은 내부 공유 크레이트를 여러 서비스가 참조하는 팀에서 특히 자주 씁니다. 이 글은 cargo workspace 모노레포 키워드에 맞춰 Cargo.toml 구조, 멤버 관리, 공통 의존성, 빌드 최적화를 실무 관점에서 정리합니다.
이 글을 읽으면
- Cargo workspace의 루트 매니페스트 구조를 이해합니다
- 멤버 추가·공통 의존성 설정 방법을 익힙니다
- 빌드 최적화·CI 전략을 적용할 수 있습니다
실무에서 마주한 현실
개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.
목차
- 개념: workspace 루트와 멤버
- 실전: 최소 레이아웃과 명령
- 고급: workspace.dependencies·패치
- 비교: 단일 크레이트·멀티 레포
- 실무 사례
- 트러블슈팅
- 마무리
개념: workspace 루트와 멤버
Workspace란?
- 루트
Cargo.toml에[workspace]섹션이 있고,members에 하위 크레이트 경로가 나열됩니다. - 워크스페이스에 속한 패키지들은 동일한
Cargo.lock정책 아래에서 의존성 버전을 맞추기 쉽습니다. - 기본 패키지(root package)를 두지 않고 메타만 있는 virtual manifest로 두는 팀도 많습니다.
구조 다이어그램
다음은 code를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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) 워크스페이스 생성
디렉터리 구조 생성
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 프로젝트 루트 생성
mkdir my-workspace
cd my-workspace
# 멤버 크레이트 생성
cargo new --lib crates/core
cargo new --bin crates/api
cargo new --lib crates/utils
루트 Cargo.toml 작성
다음은 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
아래 코드는 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
다음은 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
아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
[package]
name = "utils"
version.workspace = true
edition.workspace = true
[dependencies]
tracing.workspace = true
3) 코드 예제
crates/core/src/lib.rs
아래 코드는 rust를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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
아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
use tracing::info;
pub fn log_startup(service_name: &str) {
info!("Starting service: {}", service_name);
}
crates/api/src/main.rs
다음은 rust를 활용한 상세한 구현 코드입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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) 자주 쓰는 명령
빌드 및 실행
다음은 bash를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 전체 워크스페이스 검사
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
의존성 관리
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 전체 의존성 트리 확인
cargo tree --workspace
# 특정 크레이트 의존성
cargo tree -p core
# 중복 의존성 확인
cargo tree --duplicates
# 의존성 업데이트
cargo update
# 특정 의존성만 업데이트
cargo update -p serde
정리 및 최적화
아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 빌드 캐시 정리
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
아래 코드는 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
아래 코드는 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) 프로파일 공유
아래 코드는 toml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 루트 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: 공유 도메인 로직 + 여러 바이너리
구조: 아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 아래 코드는 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: 아래 코드는 toml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 실행 예제
[package]
name = "api"
version = "0.1.0"
edition = "2021"
[dependencies]
core.workspace = true
tokio.workspace = true
axum = "0.7"
crates/worker/Cargo.toml: 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
[package]
name = "worker"
version = "0.1.0"
edition = "2021"
[dependencies]
core.workspace = true
tokio.workspace = true
장점:
core변경 시 모든 바이너리 자동 재빌드- 의존성 버전 일관성
- 공유
target/로 빌드 시간 단축
사례 2: FFI 바인딩 (sys + safe wrapper)
구조: 아래 코드는 code를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 실행 예제
[package]
name = "libfoo-sys"
version = "0.1.0"
edition = "2021"
links = "foo"
[build-dependencies]
cc = "1.0"
libfoo-sys/build.rs: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
// 함수 정의 및 구현
fn main() {
cc::Build::new()
.file("vendor/libfoo.c")
.compile("foo");
}
libfoo/Cargo.toml: 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 실행 예제
[package]
name = "libfoo"
version = "0.1.0"
edition = "2021"
[dependencies]
libfoo-sys = { path = "../libfoo-sys" }
libfoo/src/lib.rs: 다음은 rust를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 마이크로서비스 모노레포
구조: 아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
services/
├── Cargo.toml
├── crates/
│ ├── shared/ (공통 타입, 유틸)
│ ├── auth-service/
│ ├── user-service/
│ ├── order-service/
│ └── gateway/
루트 Cargo.toml: 아래 코드는 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: 다음은 rust를 활용한 상세한 구현 코드입니다. 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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: 아래 코드는 rust를 사용한 구현 예제입니다. 함수를 통해 로직을 구현합니다, 비동기 처리를 통해 효율적으로 작업을 수행합니다, 에러 처리를 통해 안정성을 확보합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
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 빌드 최적화: 다음은 yaml를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# .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 (금지) 해결: 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 루트 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 버전 지정 해결: 아래 코드는 toml를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 루트 Cargo.toml
[workspace.dependencies]
serde = "1.0" # 버전 통일
# 멤버 Cargo.toml
[dependencies]
serde.workspace = true # 루트 버전 사용
문제 3: 빌드 캐시 비대
증상: target/ 디렉터리가 수 GB
원인:
- 불필요한 feature 활성화
- 여러 프로파일 빌드
- 미사용 의존성 해결: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 정기적 정리
cargo clean
# 특정 프로파일만 정리
cargo clean --release
# 미사용 의존성 제거
cargo machete
sccache 사용: 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 설치
cargo install sccache
# 환경 변수 설정
export RUSTC_WRAPPER=sccache
# 빌드 (캐시 공유)
cargo build --workspace
문제 4: 테스트 시간 과다
증상: cargo test --workspace 실행 시 수 분 소요
해결 1: nextest 사용
아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 설치
cargo install cargo-nextest
# 병렬 테스트
cargo nextest run --workspace
해결 2: 변경된 크레이트만 테스트 아래 코드는 bash를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
# 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 순환
해결: 공통 타입을 세 번째 크레이트로 추출
아래 코드는 code를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
Before:
api → core → api (순환!)
After:
api → core → shared
↓
shared
예시: 아래 코드는 toml를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.
# 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/의 이점을 경험하면 다시 돌아가기 어렵습니다.