[2026] cargo workspace 모노레포 | Cargo.toml 구조·멤버·공통 의존성·빌드 최적화

[2026] cargo workspace 모노레포 | Cargo.toml 구조·멤버·공통 의존성·빌드 최적화

이 글의 핵심

Rust Cargo workspace로 크레이트를 한 저장소에서 묶는 법: 루트 매니페스트, 멤버 추가, workspace.dependencies, 빌드 캐시까지 실무 팁을 정리합니다.

들어가며

Cargo workspace는 여러 패키지(크레이트)를 하나의 저장소에서 함께 빌드·테스트하기 위한 공식적인 모노레포 패턴입니다. Cargo.lock은 워크스페이스 루트에 하나만 두는 것이 일반적이며, 공유 target/ 덕분에 의존성 그래프가 겹칠 때 컴파일량을 줄일 수 있습니다. CLI·라이브러리·프로토콜 크레이트를 나눈 팀, 혹은 내부 공유 크레이트를 여러 서비스가 참조하는 팀에서 특히 자주 씁니다. 이 글은 cargo workspace 모노레포 키워드에 맞춰 Cargo.toml 구조, 멤버 관리, 공통 의존성, 빌드 최적화를 실무 관점에서 정리합니다.

이 글을 읽으면

  • Cargo workspace의 루트 매니페스트 구조를 이해합니다
  • 멤버 추가·공통 의존성 설정 방법을 익힙니다
  • 빌드 최적화·CI 전략을 적용할 수 있습니다

실무에서 마주한 현실

개발을 배울 때는 모든 게 깔끔하고 이론적입니다. 하지만 실무는 다릅니다. 레거시 코드와 씨름하고, 급한 일정에 쫓기고, 예상치 못한 버그와 마주합니다. 이 글에서 다루는 내용도 처음엔 이론으로 배웠지만, 실제 프로젝트에 적용하면서 “아, 이래서 이렇게 설계하는구나” 하고 깨달은 것들입니다. 특히 기억에 남는 건 첫 프로젝트에서 겪은 시행착오입니다. 책에서 배운 대로 했는데 왜 안 되는지 몰라 며칠을 헤맸죠. 결국 선배 개발자의 코드 리뷰를 통해 문제를 발견했고, 그 과정에서 많은 걸 배웠습니다. 이 글에서는 이론뿐 아니라 실전에서 마주칠 수 있는 함정들과 해결 방법을 함께 다루겠습니다.

목차

  1. 개념: workspace 루트와 멤버
  2. 실전: 최소 레이아웃과 명령
  3. 고급: workspace.dependencies·패치
  4. 비교: 단일 크레이트·멀티 레포
  5. 실무 사례
  6. 트러블슈팅
  7. 마무리

개념: 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.tomlworkspace.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 빌드만으로도 운영 체감이 크게 좋아집니다.

핵심 요약

  1. 구조
    • 루트에 [workspace] 선언
    • 멤버 크레이트는 members 배열에 나열
    • 공유 Cargo.locktarget/
  2. 의존성 관리
    • workspace.dependencies로 버전 통일
    • 멤버는 .workspace = true로 상속
    • [patch]로 긴급 수정
  3. 빌드 최적화
    • -p 옵션으로 특정 크레이트만 빌드
    • sccache로 캐시 공유
    • cargo nextest로 병렬 테스트
  4. CI/CD
    • 변경된 크레이트만 빌드
    • 공유 캐시 키 사용
    • Clippy·fmt 전체 실행

다음 단계

... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3