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

Rust 엔지니어링 관행 — cargo build 그 이상을 향해

저자 소개

  • Microsoft SCHIE(Silicon and Cloud Hardware Infrastructure Engineering) 팀의 수석 펌웨어 아키텍트
  • 보안, 시스템 프로그래밍(펌웨어, 운영체제, 하이퍼바이저), CPU 및 플랫폼 아키텍처, C++ 시스템 분야의 업계 전문가
  • 2017년(@AWS EC2)부터 Rust로 프로그래밍을 시작했으며, 이후 이 언어의 매력에 빠져 활동 중

많은 팀이 너무 늦게 발견하곤 하는 Rust 툴체인 기능들에 대한 실무 가이드입니다. 빌드 스크립트, 교차 컴파일(cross-compilation), 벤치마킹, 코드 커버리지, 그리고 Miri와 Valgrind를 이용한 안전성 검증을 다룹니다. 각 장에서는 실제 하드웨어 진단 코드베이스(대규모 멀티 크레이트 워크스페이스)에서 추출한 구체적인 예제를 사용하여, 모든 기술을 실제 운영 코드에 직접 적용할 수 있도록 구성했습니다.

이 책의 활용 방법

이 책은 자기 주도 학습 또는 팀 워크숍을 위해 설계되었습니다. 각 장은 대부분 독립적으로 구성되어 있으므로 순서대로 읽거나 필요한 주제를 골라서 학습하실 수 있습니다.

난이도 범례

기호레벨의미
🟢입문 (Starter)명확한 패턴을 가진 직관적인 도구 — 첫날부터 바로 활용 가능
🟡중급 (Intermediate)툴체인 내부 구조나 플랫폼 개념에 대한 이해가 필요함
🔴고급 (Advanced)깊이 있는 툴체인 지식, 나이틀리(nightly) 기능 또는 다중 도구 오케스트레이션 필요

학습 권장 시간

파트예상 시간핵심 성과
I — 빌드 및 배포01–02장3–4시간빌드 메타데이터, 교차 컴파일, 정적 바이너리
II — 측정 및 검증03–05장4–5시간통계적 벤치마킹, 커버리지 게이트, Miri/새니타이저
III — 강화 및 최적화06–10장6–8시간공급망 보안, 릴리스 프로필, 컴파일 타임 도구, no_std, Windows
IV — 통합11–13장3–4시간운영 환경용 CI/CD 파이프라인, 실전 팁, 종합 실습
16–21시간전체 운영 엔지니어링 파이프라인 완성

실습 진행 방법

각 장에는 난이도 표시가 있는 🏋️ 실습이 포함되어 있습니다. 솔루션은 확장 가능한 <details> 블록에 제공되니, 먼저 직접 실습해 본 후 결과를 확인하시기 바랍니다.

  • 🟢 실습은 대개 10~15분 내에 완료할 수 있습니다.
  • 🟡 실습은 20~40분 정도 소요되며, 로컬에서 도구를 실행해야 할 수도 있습니다.
  • 🔴 실습은 상당한 설정과 실험이 필요합니다 (1시간 이상).

선수 지식

개념학습 위치
Cargo 워크스페이스 레이아웃Rust Book 14.3장
기능 플래그 (Feature flags)Cargo Reference — Features
#[cfg(test)] 및 기본 테스트Rust Patterns 12장
unsafe 블록 및 FFI 기초Rust Patterns 10장

장별 의존성 맵

                 ┌──────────┐
                 │ ch00     │
                 │   소개   │
                 └────┬─────┘
        ┌─────┬───┬──┴──┬──────┬──────┐
        ▼     ▼   ▼     ▼      ▼      ▼
      ch01  ch03 ch04  ch05   ch06   ch09
      Build Bench Cov  Miri   Deps   no_std
        │     │    │    │      │      │
        │     └────┴────┘      │      ▼
        │          │           │    ch10
        ▼          ▼           ▼   Windows
       ch02      ch07        ch07    │
       Cross    RelProf     RelProf  │
        │          │           │     │
        │          ▼           │     │
        │        ch08          │     │
        │      CompTime        │     │
        └──────────┴───────────┴─────┘
                   │
                   ▼
                 ch11
               CI/CD 파이프라인
                   │
                   ▼
                ch12 ─── ch13
              실전 팁   빠른 참조

순서 상관없이 읽기 가능: 01, 03, 04, 05, 06, 09장은 서로 독립적입니다. 선행 장 학습 후 읽기 권장: 02장(01장 필요), 07~08장(03~06장 선학습 시 유리), 10장(09장 선학습 시 유리). 마지막에 읽기 권장: 11장(모든 내용을 통합), 12장(실전 팁), 13장(참조).

주석이 달린 목차

파트 I — 빌드 및 배포 (Build & Ship)

#난이도설명
1빌드 스크립트 — build.rs 심층 분석🟢컴파일 타임 상수, C 코드 컴파일, Protobuf 생성, 시스템 라이브러리 링크, 안티 패턴
2교차 컴파일 — 하나의 소스, 다양한 타겟🟡타겟 트리플(Target triples), musl 정적 바이너리, ARM 교차 컴파일, cross 도구, cargo-zigbuild, GitHub Actions

파트 II — 측정 및 검증 (Measure & Verify)

#난이도설명
3벤치마킹 — 중요한 지표 측정하기🟡Criterion.rs, Divan, perf 플레임그래프, PGO, CI에서의 지속적인 벤치마킹
4코드 커버리지 — 테스트가 놓치는 부분 확인하기🟢cargo-llvm-cov, cargo-tarpaulin, grcov, Codecov/Coveralls CI 통합
5Miri, Valgrind 및 새니타이저🔴MIR 인터프리터, Valgrind memcheck/Helgrind, ASan/MSan/TSan, cargo-fuzz, loom

파트 III — 강화 및 최적화 (Harden & Optimize)

#난이도설명
6의존성 관리 및 공급망 보안🟢cargo-audit, cargo-deny, cargo-vet, cargo-outdated, cargo-semver-checks
7릴리스 프로필 및 바이너리 크기🟡릴리스 프로필 구조, LTO 트레이드오프, cargo-bloat, cargo-udeps
8컴파일 시간 및 개발자 도구🟡sccache, mold, cargo-nextest, cargo-expand, cargo-geiger, 워크스페이스 린트, MSRV
9no_std 및 기능 검증🔴cargo-hack, core/alloc/std 계층, 커스텀 패닉 핸들러, no_std 코드 테스트
10Windows 및 조건부 컴파일🟡#[cfg] 패턴, windows-sys/windows 크레이트, cargo-xwin, 플랫폼 추상화

파트 IV — 통합 (Integrate)

#난이도설명
11종합 정리 — 운영 환경용 CI/CD 파이프라인🟡GitHub Actions 워크플로, cargo-make, pre-commit 훅, cargo-dist, 캡스톤 프로젝트
12실전 팁과 요령🟡검증된 10가지 패턴: deny(warnings) 함정, 캐시 튜닝, 의존성 중복 제거, RUSTFLAGS 등
13빠른 참조 카드주요 명령어 요약, 60개 이상의 의사결정 테이블 항목, 추가 학습 링크

빌드 스크립트 — build.rs 심층 분석 🟢

학습 내용:

  • build.rs가 Cargo 빌드 파이프라인의 어디에 위치하며 언제 실행되는지
  • 5가지 실전 패턴: 컴파일 타임 상수, C/C++ 컴파일, Protobuf 코드 생성, pkg-config 링크, 기능 감지
  • 빌드 속도를 늦추거나 교차 컴파일을 망가뜨리는 안티 패턴
  • 추적 가능성(traceability)과 재현 가능한 빌드(reproducible builds) 사이의 균형 잡기

참조: 교차 컴파일에서는 타겟 인식 빌드를 위해 빌드 스크립트를 사용합니다 · no_std 및 기능은 여기서 설정된 cfg 플래그를 확장합니다 · CI/CD 파이프라인은 자동화 과정에서 빌드 스크립트를 조율합니다.

모든 Cargo 패키지는 크레이트 루트에 build.rs라는 파일을 포함할 수 있습니다. Cargo는 크레이트를 컴파일하기 에 이 파일을 컴파일하고 실행합니다. 빌드 스크립트는 표준 출력(stdout)에 println! 지시어를 출력하여 Cargo와 통신합니다.

build.rs란 무엇이며 언제 실행되는가

┌─────────────────────────────────────────────────────────┐
│                    Cargo 빌드 파이프라인                  │
│                                                         │
│  1. 의존성 해결 (Resolve dependencies)                   │
│  2. 크레이트 다운로드                                     │
│  3. build.rs 컴파일  ← 일반적인 Rust 코드, 호스트에서 실행   │
│  4. build.rs 실행    ← stdout → Cargo 지시어 전달         │
│  5. 크레이트 컴파일  (4단계의 지시어 사용)                 │
│  6. 링크 (Link)                                         │
└─────────────────────────────────────────────────────────┘

주요 특징:

  • build.rs는 타겟이 아닌 호스트(host) 머신에서 실행됩니다. 교차 컴파일 시에도 최종 바이너리가 다른 아키텍처를 타겟으로 하더라도 빌드 스크립트는 개발 머신에서 실행됩니다.
  • 빌드 스크립트의 범위는 해당 패키지로 제한됩니다. 다른 크레이트의 컴파일 방식에는 영향을 줄 수 없습니다. 단, 패키지가 Cargo.tomllinks 키를 선언한 경우 cargo::metadata=KEY=VALUE를 통해 의존하는 크레이트에 메타데이터를 전달할 수 있습니다.
  • Cargo가 변경 사항을 감지할 때마다 매번 실행됩니다. 실행 횟수를 제한하려면 cargo::rerun-if-changed 지시어를 사용해야 합니다.

참고 (Rust 1.71+): Rust 1.71부터 Cargo는 컴파일된 build.rs 바이너리의 지문을 기록합니다. 바이너리가 동일하면 소스 타임스탬프가 변경되었더라도 다시 실행하지 않습니다. 하지만 cargo::rerun-if-changed=build.rs는 여전히 유용합니다. 지시어가 전혀 없으면 Cargo는 패키지의 어떤 파일이라도 변경될 때마다(단순히 build.rs뿐만 아니라) build.rs를 다시 실행하기 때문입니다. cargo::rerun-if-changed=build.rs를 명시하면 build.rs 자체가 변경될 때만 다시 실행하도록 제한하여 대규모 크레이트에서 컴파일 시간을 크게 단축할 수 있습니다.

  • 메인 크레이트에서 사용할 수 있는 cfg 플래그, 환경 변수, 링커 인자, 파일 경로 등을 내보낼 수 있습니다.

가장 기본적인 Cargo.toml 설정:

[package]
name = "my-crate"
version = "0.1.0"
edition = "2021"
build = "build.rs"       # 기본값 — Cargo는 자동으로 build.rs를 찾습니다.
# build = "src/build.rs" # 다른 위치에 둘 수도 있습니다.

Cargo 지시어 프로토콜 (The Cargo Instruction Protocol)

빌드 스크립트는 표준 출력에 지시어를 출력하여 Cargo와 통신합니다. Rust 1.77부터는 cargo:: 접두사를 사용하는 것이 권장됩니다 (기존의 cargo: 방식 대체).

지시어목적
cargo::rerun-if-changed=PATHPATH가 변경될 때만 build.rs 재실행
cargo::rerun-if-env-changed=VAR환경 변수 VAR이 변경될 때만 재실행
cargo::rustc-link-lib=NAME네이티브 라이브러리 NAME과 링크
cargo::rustc-link-search=PATH라이브러리 검색 경로에 PATH 추가
cargo::rustc-cfg=KEY조건부 컴파일을 위한 #[cfg(KEY)] 플래그 설정
cargo::rustc-cfg=KEY="VALUE"#[cfg(KEY = "VALUE")] 플래그 설정
cargo::rustc-env=KEY=VALUEenv!()로 접근 가능한 환경 변수 설정
cargo::rustc-cdylib-link-arg=FLAGcdylib 타겟의 링커에 FLAG 전달
cargo::warning=MESSAGE컴파일 중 경고 메시지 표시
cargo::metadata=KEY=VALUE의존 크레이트에서 읽을 수 있는 메타데이터 저장
// build.rs — 최소 예제
fn main() {
    // build.rs 자체가 변경될 때만 재실행
    println!("cargo::rerun-if-changed=build.rs");

    // 컴파일 타임 환경 변수 설정
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=BUILD_TIMESTAMP={timestamp}");
}

패턴 1: 컴파일 타임 상수 (Compile-Time Constants)

가장 일반적인 사용 사례는 바이너리에 빌드 메타데이터(git 해시, 빌드 날짜, CI 작업 ID 등)를 포함하여 런타임에 보고할 수 있게 하는 것입니다.

// build.rs
use std::process::Command;

fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");

    // Git 커밋 해시
    let output = Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .expect("git을 찾을 수 없습니다");
    let git_hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
    println!("cargo::rustc-env=GIT_HASH={git_hash}");

    // 빌드 프로필 (debug 또는 release)
    let profile = std::env::var("PROFILE").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_PROFILE={profile}");

    // 타겟 트리플 (Target triple)
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=BUILD_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/main.rs — 빌드 타임 값 사용하기
fn print_version() {
    println!(
        "{} {} (git:{} target:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_TARGET"),
        env!("BUILD_PROFILE"),
    );
}
}

Cargo 기본 환경 변수: 별도의 build.rs 없이도 사용할 수 있는 환경 변수들이 있습니다: CARGO_PKG_NAME, CARGO_PKG_VERSION, CARGO_PKG_AUTHORS, CARGO_PKG_DESCRIPTION, CARGO_MANIFEST_DIR. 전체 목록을 확인해 보세요.

패턴 2: cc 크레이트를 이용한 C/C++ 코드 컴파일

Rust 크레이트가 C 라이브러리를 래핑하거나 작은 C 헬퍼 함수가 필요한 경우(하드웨어 인터페이스에서 흔함), cc 크레이트를 사용하면 build.rs 내에서 컴파일을 간편하게 처리할 수 있습니다.

# Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=csrc/");

    cc::Build::new()
        .file("csrc/ipmi_raw.c")
        .file("csrc/smbios_parser.c")
        .include("csrc/include")
        .flag("-Wall")
        .flag("-Wextra")
        .opt_level(2)
        .compile("diag_helpers");
    // 이 과정에서 libdiag_helpers.a가 생성되며, 적절한
    // cargo::rustc-link-lib 및 cargo::rustc-link-search 지시어가 출력됩니다.
}
#![allow(unused)]
fn main() {
// src/lib.rs — 컴파일된 C 코드에 대한 FFI 바인딩
extern "C" {
    fn ipmi_raw_command(
        netfn: u8,
        cmd: u8,
        data: *const u8,
        data_len: usize,
        response: *mut u8,
        response_len: *mut usize,
    ) -> i32;
}

/// 로우(raw) IPMI 커맨드 인터페이스에 대한 안전한 래퍼 함수.
/// 가정: enum IpmiError { CommandFailed(i32), ... }
pub fn send_ipmi_command(netfn: u8, cmd: u8, data: &[u8]) -> Result<Vec<u8>, IpmiError> {
    let mut response = vec![0u8; 256];
    let mut response_len: usize = response.len();

    // SAFETY: response 버퍼가 충분히 크고 response_len이 올바르게 초기화되었습니다.
    let rc = unsafe {
        ipmi_raw_command(
            netfn,
            cmd,
            data.as_ptr(),
            data.len(),
            response.as_mut_ptr(),
            &mut response_len,
        )
    };

    if rc != 0 {
        return Err(IpmiError::CommandFailed(rc));
    }
    response.truncate(response_len);
    Ok(response)
}
}

C++ 코드의 경우 .cpp(true).flag("-std=c++17")을 사용합니다:

// build.rs — C++ 버전
fn main() {
    println!("cargo::rerun-if-changed=cppsrc/");

    cc::Build::new()
        .cpp(true)
        .file("cppsrc/vendor_parser.cpp")
        .flag("-std=c++17")
        .flag("-fno-exceptions")    // Rust의 무예외(no-exception) 모델과 일치시킴
        .compile("vendor_helpers");
}

패턴 3: 프로토콜 버퍼 및 코드 생성 (Codegen)

빌드 스크립트는 .proto, .fbs, .json 스키마 파일을 컴파일 타임에 Rust 소스 코드로 변환하는 코드 생성 작업에 탁월합니다. 다음은 prost-build를 사용한 Protobuf 패턴입니다:

# Cargo.toml
[build-dependencies]
prost-build = "0.13"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=proto/");

    prost_build::compile_protos(
        &["proto/diagnostics.proto", "proto/telemetry.proto"],
        &["proto/"],
    )
    .expect("Protobuf 정의 컴파일 실패");
}
#![allow(unused)]
fn main() {
// src/lib.rs — 생성된 코드 포함시키기
pub mod diagnostics {
    include!(concat!(env!("OUT_DIR"), "/diagnostics.rs"));
}

pub mod telemetry {
    include!(concat!(env!("OUT_DIR"), "/telemetry.rs"));
}
}

OUT_DIR: 빌드 스크립트가 생성된 파일을 저장해야 하는 Cargo 제공 디렉토리입니다. 각 크레이트는 target/ 아래에 자신만의 OUT_DIR을 가집니다.

패턴 4: pkg-config를 이용한 시스템 라이브러리 링크

.pc 파일을 제공하는 시스템 라이브러리(systemd, OpenSSL, libpci 등)의 경우, pkg-config 크레이트를 사용하여 시스템을 탐색하고 적절한 링크 지시어를 내보낼 수 있습니다.

# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    // libpci 탐색 (PCIe 장치 열거에 사용됨)
    pkg_config::Config::new()
        .atleast_version("3.6.0")
        .probe("libpci")
        .expect("libpci >= 3.6.0을 찾을 수 없습니다 — pciutils-dev를 설치하세요");

    // libsystemd 탐색 (선택 사항 — sd_notify 통합용)
    if pkg_config::probe_library("libsystemd").is_ok() {
        println!("cargo::rustc-cfg=has_systemd");
    }
}
#![allow(unused)]
fn main() {
// src/lib.rs — pkg-config 탐색 결과에 따른 조건부 컴파일
#[cfg(has_systemd)]
mod systemd_notify {
    extern "C" {
        fn sd_notify(unset_environment: i32, state: *const std::ffi::c_char) -> i32;
    }

    pub fn notify_ready() {
        let state = std::ffi::CString::new("READY=1").unwrap();
        // SAFETY: state는 유효한 널 종료 C 문자열입니다.
        unsafe { sd_notify(0, state.as_ptr()) };
    }
}

#[cfg(not(has_systemd))]
mod systemd_notify {
    pub fn notify_ready() {
        // systemd가 없는 시스템에서는 아무 작업도 하지 않음
    }
}
}

패턴 5: 기능 감지 및 조건부 컴파일 (Feature Detection)

빌드 스크립트는 컴파일 환경을 탐색하고 메인 크레이트에서 조건부 코드 경로를 결정하는 데 사용할 cfg 플래그를 설정할 수 있습니다.

CPU 아키텍처 및 OS 감지 (컴파일 타임 상수이므로 안전함):

// build.rs — CPU 기능 및 OS 역량 감지
fn main() {
    println!("cargo::rerun-if-changed=build.rs");

    let target = std::env::var("TARGET").unwrap();
    let target_os = std::env::var("CARGO_CFG_TARGET_OS").unwrap();

    // x86_64에서 AVX2 최적화 경로 활성화
    if target.starts_with("x86_64") {
        println!("cargo::rustc-cfg=has_x86_64");
    }

    // aarch64에서 ARM NEON 경로 활성화
    if target.starts_with("aarch64") {
        println!("cargo::rustc-cfg=has_aarch64");
    }

    // /dev/ipmi0 사용 가능 여부 확인 (빌드 타임 확인)
    if target_os == "linux" && std::path::Path::new("/dev/ipmi0").exists() {
        println!("cargo::rustc-cfg=has_ipmi_device");
    }
}

⚠️ 안티 패턴 사례 — 아래 코드는 유혹적이지만 문제가 있는 접근 방식입니다. 실제 운영 환경에서는 사용하지 마세요.

// build.rs — 나쁨: 빌드 타임에 실행 중인 머신의 하드웨어 감지
fn main() {
    // 안티 패턴: 바이너리가 '빌드' 머신의 하드웨어에 고착됩니다.
    // GPU가 있는 머신에서 빌드하고 GPU가 없는 머신에 배포하면,
    // 바이너리는 GPU가 있다고 잘못 가정하게 됩니다.
    if std::process::Command::new("accel-query")
        .arg("--query-gpu=name")
        .arg("--format=csv,noheader")
        .output()
        .is_ok()
    {
        println!("cargo::rustc-cfg=has_accel_device");
    }
}
#![allow(unused)]
fn main() {
// src/gpu.rs — 빌드 타임 감지 결과에 따라 동작하는 코드
pub fn query_gpu_info() -> GpuResult {
    #[cfg(has_accel_device)]
    {
        run_accel_query()
    }

    #[cfg(not(has_accel_device))]
    {
        GpuResult::NotAvailable("빌드 타임에 accel-query를 찾을 수 없습니다".into())
    }
}
}

⚠️ 이것이 잘못된 이유: 선택적인 하드웨어의 경우 빌드 타임 감지보다는 런타임 장치 감지가 거의 항상 더 낫습니다. 위 방식으로 생성된 바이너리는 빌드 머신의 하드웨어 구성에 종속되어 배포 타겟에서 다르게 동작할 수 있습니다. 빌드 타임 감지는 아키텍처, OS, 라이브러리 가용성 등 컴파일 타임에 확실히 고정된 요소에만 사용하세요. GPU와 같은 하드웨어는 런타임에 which accel-queryaccel-mgmt 등으로 감지하십시오.

안티 패턴 및 주의사항 (Anti-Patterns and Pitfalls)

안티 패턴문제점해결책
rerun-if-changed 부재빌드할 때마다 build.rs가 실행되어 반복 작업 속도 저하최소한 cargo::rerun-if-changed=build.rs라도 명시
build.rs 내 네트워크 호출오프라인 빌드 실패, 재현성 저하파일을 벤더링(vendoring)하거나 별도의 fetch 단계 사용
src/에 파일 쓰기Cargo는 빌드 중 소스가 변경되는 것을 예상하지 않음OUT_DIR에 쓰고 include!() 사용
과도한 계산 작업모든 cargo build 속도를 늦춤결과를 OUT_DIR에 캐싱하고 rerun-if-changed로 제어
교차 컴파일 무시$CC를 존중하지 않고 Command::new("gcc") 직접 사용교차 컴파일 툴체인을 올바르게 처리하는 cc 크레이트 사용
맥락 없는 패닉 (unwrap)unwrap()은 원인을 알 수 없는 "build script failed" 오류만 발생시킴.expect("상세 메시지")를 사용하거나 cargo::warning= 출력

적용 사례: 빌드 메타데이터 포함하기

현재 프로젝트는 버전 보고를 위해 env!("CARGO_PKG_VERSION")를 사용하고 있습니다. 빌드 스크립트를 사용하면 더 풍부한 메타데이터를 추가할 수 있습니다:

// build.rs — 추가 제안
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=.git/refs");
    println!("cargo::rerun-if-changed=build.rs");

    // 진단 보고서의 추적성을 위해 git 해시 포함
    if let Ok(output) = std::process::Command::new("git")
        .args(["rev-parse", "--short=10", "HEAD"])
        .output()
    {
        let hash = String::from_utf8_lossy(&output.stdout).trim().to_string();
        println!("cargo::rustc-env=APP_GIT_HASH={hash}");
    } else {
        println!("cargo::rustc-env=APP_GIT_HASH=unknown");
    }

    // 보고서 상관 관계 분석을 위해 빌드 타임스탬프 포함
    let timestamp = std::time::SystemTime::now()
        .duration_since(std::time::UNIX_EPOCH)
        .map(|d| d.as_secs().to_string())
        .unwrap_or_else(|_| "0".into());
    println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");

    // 타겟 트리플 출력 — 다중 아키텍처 배포 시 유용
    let target = std::env::var("TARGET").unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=APP_TARGET={target}");
}
#![allow(unused)]
fn main() {
// src/version.rs — 메타데이터 사용하기
pub struct BuildInfo {
    pub version: &'static str,
    pub git_hash: &'static str,
    pub build_epoch: &'static str,
    pub target: &'static str,
}

pub const BUILD_INFO: BuildInfo = BuildInfo {
    version: env!("CARGO_PKG_VERSION"),
    git_hash: env!("APP_GIT_HASH"),
    build_epoch: env!("APP_BUILD_EPOCH"),
    target: env!("APP_TARGET"),
};

impl BuildInfo {
    /// 필요한 경우 런타임에 에포크(epoch) 파싱 (안정 버전 Rust에서는
    /// const &str를 u64로 파싱하는 const fn이 없습니다).
    pub fn build_epoch_secs(&self) -> u64 {
        self.build_epoch.parse().unwrap_or(0)
    }
}

impl std::fmt::Display for BuildInfo {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(
            f,
            "DiagTool v{} (git:{} target:{})",
            self.version, self.git_hash, self.target
        )
    }
}
}

프로젝트의 핵심 통찰: 이 프로젝트의 수많은 크레이트 전반에 build.rs 파일이 하나도 없는 이유는 C 의존성, 코드 생성, 시스템 라이브러리 링크가 필요 없는 순수 Rust로 작성되었기 때문입니다. 이러한 것들이 필요할 때 build.rs가 훌륭한 도구가 되지만, "그냥" 추가하지는 마세요. 대규모 코드베이스에서 빌드 스크립트가 없다는 것은 결핍이 아니라 깨끗한 아키텍처의 긍정적인 신호입니다. 프로젝트가 커스텀 빌드 로직 없이 공급망을 관리하는 방법은 의존성 관리를 참조하세요.

직접 해보기

  1. Git 메타데이터 포함: APP_GIT_HASHAPP_BUILD_EPOCH를 환경 변수로 내보내는 build.rs를 만들어 보세요. main.rs에서 env!()로 이를 사용해 빌드 정보를 출력하고, 커밋 후 해시가 변경되는지 확인하세요.

  2. 시스템 라이브러리 탐색: pkg-config를 사용하여 libz (zlib)를 찾는 build.rs를 작성해 보세요. 찾았을 경우 cargo::rustc-cfg=has_zlib를 내보내고, main.rs에서 cfg 플래그에 따라 "zlib 가용" 또는 "zlib 없음"을 출력해 보세요.

  3. 의도적인 빌드 실패 유도: build.rs에서 rerun-if-changed 라인을 제거하고 cargo buildcargo test 중에 얼마나 자주 재실행되는지 관찰해 보세요. 그 후 다시 라인을 추가하고 비교해 보세요.

재현 가능한 빌드 (Reproducible Builds)

1장에서 가르치는 타임스탬프와 git 해시를 바이너리에 포함하는 것은 추적성에는 좋지만, 재현 가능한 빌드와 충돌합니다. 재현 가능한 빌드란 동일한 소스에서 빌드하면 항상 동일한 바이너리가 생성되는 성질을 말합니다.

딜레마:

목표성과비용
추적 가능성바이너리에 APP_BUILD_EPOCH 포함모든 빌드가 고유해짐 — 무결성 검증 불가
재현성cargo build --locked가 항상 동일 결과 생성빌드 타임 메타데이터 부재

실용적인 해결책:

# 1. CI에서는 항상 --locked 사용 (Cargo.lock 준수 보장)
cargo build --release --locked
# Cargo.lock이 없거나 최신이 아니면 실패 — "내 컴퓨터에선 되는데" 상황 방지

# 2. 재현성이 중요한 빌드에서는 SOURCE_DATE_EPOCH 설정
SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) cargo build --release --locked
# "현재" 대신 마지막 커밋 타임스탬프 사용 — 동일 커밋 = 동일 바이너리
#![allow(unused)]
fn main() {
// build.rs 내에서: 재현성을 위해 SOURCE_DATE_EPOCH 준수
let timestamp = std::env::var("SOURCE_DATE_EPOCH")
    .unwrap_or_else(|_| {
        std::time::SystemTime::now()
            .duration_since(std::time::UNIX_EPOCH)
            .map(|d| d.as_secs().to_string())
            .unwrap_or_else(|_| "0".into())
    });
println!("cargo::rustc-env=APP_BUILD_EPOCH={timestamp}");
}

모범 사례: 빌드 스크립트에서 SOURCE_DATE_EPOCH를 사용하면 릴리스 빌드의 재현성을 유지하면서(git-hash + locked 의존성 + 결정적 타임스탬프 = 동일 바이너리), 개발 빌드에서는 편의를 위해 실제 타임스탬프를 얻을 수 있습니다.

빌드 파이프라인 의사결정 다이어그램

flowchart TD
    START["컴파일 타임 작업이 필요한가?"] -->|아니요| SKIP["build.rs 불필요"]
    START -->|예| WHAT{"어떤 종류인가?"}
    
    WHAT -->|"메타데이터 포함"| P1["패턴 1\n컴파일 타임 상수"]
    WHAT -->|"C/C++ 컴파일"| P2["패턴 2\ncc 크레이트"]
    WHAT -->|"코드 생성"| P3["패턴 3\nprost-build / tonic-build"]
    WHAT -->|"시스템 라이브러리 링크"| P4["패턴 4\npkg-config"]
    WHAT -->|"기능 감지"| P5["패턴 5\ncfg 플래그"]
    
    P1 --> RERUN["항상 지시어 출력\ncargo::rerun-if-changed"]
    P2 --> RERUN
    P3 --> RERUN
    P4 --> RERUN
    P5 --> RERUN
    
    style SKIP fill:#91e5a3,color:#000
    style RERUN fill:#ffd43b,color:#000
    style P1 fill:#e3f2fd,color:#000
    style P2 fill:#e3f2fd,color:#000
    style P3 fill:#e3f2fd,color:#000
    style P4 fill:#e3f2fd,color:#000
    style P5 fill:#e3f2fd,color:#000

🏋️ 실습

🟢 실습 1: 버전 스탬프

현재 git 해시와 빌드 프로필을 환경 변수로 포함하는 build.rs를 가진 최소 크레이트를 만들어 보세요. main()에서 이를 출력하고, 디버그 빌드와 릴리스 빌드 사이에서 출력이 변경되는지 확인하세요.

솔루션
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=.git/HEAD");
    println!("cargo::rerun-if-changed=build.rs");

    let hash = std::process::Command::new("git")
        .args(["rev-parse", "--short", "HEAD"])
        .output()
        .map(|o| String::from_utf8_lossy(&o.stdout).trim().to_string())
        .unwrap_or_else(|_| "unknown".into());
    println!("cargo::rustc-env=GIT_HASH={hash}");
    println!("cargo::rustc-env=BUILD_PROFILE={}", std::env::var("PROFILE").unwrap_or_default());
}
// src/main.rs
fn main() {
    println!("{} v{} (git:{} profile:{})",
        env!("CARGO_PKG_NAME"),
        env!("CARGO_PKG_VERSION"),
        env!("GIT_HASH"),
        env!("BUILD_PROFILE"),
    );
}
cargo run          # profile:debug 표시
cargo run --release # profile:release 표시

🟡 실습 2: 조건부 시스템 라이브러리

pkg-config를 사용하여 libzlibpci를 모두 탐색하는 build.rs를 작성하세요. 각 라이브러리를 찾았을 때 cfg 플래그를 내보내고, main.rs에서 빌드 타임에 어떤 라이브러리가 감지되었는지 출력하세요.

솔루션
# Cargo.toml
[build-dependencies]
pkg-config = "0.3"
// build.rs
fn main() {
    println!("cargo::rerun-if-changed=build.rs");
    if pkg_config::probe_library("zlib").is_ok() {
        println!("cargo::rustc-cfg=has_zlib");
    }
    if pkg_config::probe_library("libpci").is_ok() {
        println!("cargo::rustc-cfg=has_libpci");
    }
}
// src/main.rs
fn main() {
    #[cfg(has_zlib)]
    println!("✅ zlib 감지됨");
    #[cfg(not(has_zlib))]
    println!("❌ zlib 찾을 수 없음");

    #[cfg(has_libpci)]
    println!("✅ libpci 감지됨");
    #[cfg(not(has_libpci))]
    println!("❌ libpci 찾을 수 없음");
}

핵심 요약

  • build.rs는 컴파일 타임에 호스트에서 실행됩니다. 불필요한 재빌드를 방지하기 위해 항상 cargo::rerun-if-changed를 출력하세요.
  • C/C++ 컴파일 시에는 직접 gcc 명령을 실행하지 말고 cc 크레이트를 사용하세요. 교차 컴파일 툴체인을 올바르게 처리해 줍니다.
  • 생성된 파일은 OUT_DIR에 작성하고 src/에는 절대 작성하지 마세요. Cargo는 빌드 중 소스 변경을 원치 않습니다.
  • 선택적인 하드웨어의 경우 빌드 타임보다는 런타임 감지를 선호하세요.
  • 타임스탬프를 포함할 때는 재현 가능한 빌드를 위해 SOURCE_DATE_EPOCH를 활용하세요.

교차 컴파일 — 하나의 소스, 다양한 타겟 🟡

학습 내용:

  • Rust 타겟 트리플(target triples)의 작동 원리와 rustup으로 추가하는 방법
  • 컨테이너/클라우드 배포를 위한 정적 musl 바이너리 빌드
  • 네이티브 툴체인, cross, cargo-zigbuild를 이용한 ARM(aarch64) 교차 컴파일
  • 다중 아키텍처 CI를 위한 GitHub Actions 매트릭스 빌드 설정

참조: 빌드 스크립트 — 교차 컴파일 중 build.rs는 호스트(HOST)에서 실행됩니다 · 릴리스 프로필 — 교차 컴파일된 릴리스 바이너리를 위한 LTO 및 strip 설정 · Windows — Windows 교차 컴파일 및 no_std 타겟

교차 컴파일(Cross-compilation)이란 한 머신(호스트)에서 다른 머신(타겟)에서 실행될 실행 파일을 빌드하는 것을 의미합니다. 호스트는 여러분의 x86_64 노트북일 수 있고, 타겟은 ARM 서버, musl 기반 컨테이너, 또는 Windows 머신일 수 있습니다. Rust는 rustc 자체가 이미 교차 컴파일러로 설계되었기 때문에 이 과정이 매우 수월합니다. 적절한 타겟 라이브러리와 호환되는 링커만 있으면 됩니다.

타겟 트리플의 구조 (The Target Triple Anatomy)

모든 Rust 컴파일 타겟은 타겟 트리플(이름과 달리 보통 4개 부분으로 구성됨)로 식별됩니다:

<아키텍처>-<벤더>-<운영체제>-<환경>

예시:
  x86_64  - unknown - linux  - gnu      ← 표준 Linux (glibc)
  x86_64  - unknown - linux  - musl     ← 정적 Linux (musl libc)
  aarch64 - unknown - linux  - gnu      ← ARM 64비트 Linux
  x86_64  - pc      - windows- msvc     ← MSVC를 사용하는 Windows
  aarch64 - apple   - darwin             ← Apple 실리콘 기반 macOS
  x86_64  - unknown - none              ← 베어 메탈 (OS 없음)

사용 가능한 모든 타겟 목록 확인:

# rustc가 컴파일할 수 있는 모든 타겟 표시 (~250개)
rustc --print target-list | wc -l

# 시스템에 설치된 타겟 표시
rustup target list --installed

# 현재 기본 타겟 표시
rustc -vV | grep host

rustup을 이용한 툴체인 설치

# 타겟 라이브러리 추가 (해당 타겟용 Rust std)
rustup target add x86_64-unknown-linux-musl
rustup target add aarch64-unknown-linux-gnu

# 이제 교차 컴파일이 가능합니다:
cargo build --target x86_64-unknown-linux-musl
cargo build --target aarch64-unknown-linux-gnu  # 링커가 필요함 — 아래 내용 참조

rustup target add가 제공하는 것: 해당 타겟용으로 미리 컴파일된 std, core, alloc 라이브러리입니다. C 링커나 C 라이브러리는 제공하지 않습니다. C 툴체인이 필요한 타겟(대부분의 gnu 타겟)의 경우 별도로 설치해야 합니다.

# Ubuntu/Debian — aarch64용 교차 링커 설치
sudo apt install gcc-aarch64-linux-gnu

# Ubuntu/Debian — 정적 빌드를 위한 musl 툴체인 설치
sudo apt install musl-tools

# Fedora
sudo dnf install gcc-aarch64-linux-gnu

.cargo/config.toml — 타겟별 설정

매번 명령을 내릴 때마다 --target을 전달하는 대신, 프로젝트 루트나 홈 디렉토리의 .cargo/config.toml에서 기본값을 설정할 수 있습니다.

# .cargo/config.toml

# 이 프로젝트의 기본 타겟 (선택 사항 — 네이티브 기본값을 유지하려면 생략)
# [build]
# target = "x86_64-unknown-linux-musl"

# aarch64 교차 컴파일을 위한 링커
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
rustflags = ["-C", "target-feature=+crc"]

# musl 정적 빌드를 위한 링커 (보통 시스템 gcc가 작동함)
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"
rustflags = ["-C", "target-feature=+crc,+aes"]

# ARM 32비트 (라즈베리 파이, 임베디드)
[target.armv7-unknown-linux-gnueabihf]
linker = "arm-linux-gnueabihf-gcc"

# 모든 타겟에 대한 환경 변수
[env]
# 예: 커스텀 시스루트(sysroot) 설정
# SYSROOT = "/opt/cross/sysroot"

설정 파일 검색 순서 (가장 먼저 발견되는 파일 적용):

  1. <project>/.cargo/config.toml
  2. <project>/../.cargo/config.toml (상위 디렉토리로 올라가며 검색)
  3. $CARGO_HOME/config.toml (보통 ~/.cargo/config.toml)

musl을 이용한 정적 바이너리 (Static Binaries with musl)

최소한의 컨테이너(Alpine, scratch Docker 이미지)나 glibc 버전을 제어할 수 없는 시스템에 배포할 때는 musl을 사용하여 빌드하세요:

# musl 타겟 설치
rustup target add x86_64-unknown-linux-musl
sudo apt install musl-tools  # musl-gcc 제공

# 완전한 정적 바이너리 빌드
cargo build --release --target x86_64-unknown-linux-musl

# 정적 빌드 여부 확인
file target/x86_64-unknown-linux-musl/release/diag_tool
# → ELF 64-bit LSB executable, x86-64, statically linked

ldd target/x86_64-unknown-linux-musl/release/diag_tool
# → not a dynamic executable (동적 실행 파일이 아님)

정적 빌드 vs 동적 빌드 트레이드오프:

특성glibc (동적)musl (정적)
바이너리 크기작음 (공유 라이브러리 사용)큼 (~5-15 MB 증가)
이식성일치하는 glibc 버전 필요모든 Linux에서 실행 가능
DNS 해결nsswitch 완전 지원기본 리졸버 (mDNS 미지원)
배포시스루트나 컨테이너 필요의존성 없는 단일 바이너리
성능malloc이 약간 더 빠름malloc이 약간 더 느림
dlopen() 지원지원함지원 안 함

프로젝트 적용: 호스트 OS 버전을 보장할 수 없는 다양한 서버 하드웨어에 배포할 때 정적 musl 빌드가 이상적입니다. 단일 바이너리 배포 모델은 "내 컴퓨터에선 되는데" 문제를 제거합니다.

ARM (aarch64) 교차 컴파일

데이터 센터에서 ARM 서버(AWS Graviton, Ampere Altra, Grace 등) 사용이 점점 늘고 있습니다. x86_64 호스트에서 aarch64용으로 교차 컴파일하는 방법은 다음과 같습니다:

# 1단계: 타겟 및 교차 링커 설치
rustup target add aarch64-unknown-linux-gnu
sudo apt install gcc-aarch64-linux-gnu

# 2단계: .cargo/config.toml에 링커 설정 (위 내용 참조)

# 3단계: 빌드
cargo build --release --target aarch64-unknown-linux-gnu

# 4단계: 바이너리 확인
file target/aarch64-unknown-linux-gnu/release/diag_tool
# → ELF 64-bit LSB executable, ARM aarch64

타겟 아키텍처용 테스트 실행을 위해서는 다음 중 하나가 필요합니다:

  • 실제 ARM 머신
  • QEMU 유저 모드 에뮬레이션
# QEMU 유저 모드 설치 (x86_64에서 ARM 바이너리 실행)
sudo apt install qemu-user qemu-user-static binfmt-support

# 이제 cargo test가 QEMU를 통해 교차 컴파일된 테스트를 실행할 수 있습니다.
cargo test --target aarch64-unknown-linux-gnu
# (속도가 느림 — 각 테스트 바이너리가 에뮬레이션됨. 매일 하는 개발용이 아닌 CI 검증용으로 사용하세요.)

.cargo/config.toml에서 QEMU를 테스트 러너로 설정:

[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"
runner = "qemu-aarch64-static -L /usr/aarch64-linux-gnu"

cross 도구 — Docker 기반 교차 컴파일

cross 도구는 미리 설정된 Docker 이미지를 사용하여 설정 과정이 필요 없는 교차 컴파일 경험을 제공합니다.

# cross 설치 (crates.io의 안정 버전)
cargo install cross
# 또는 최신 기능을 위해 git에서 설치:
# cargo install cross --git https://github.com/cross-rs/cross

# 교차 컴파일 — 툴체인 설정이 필요 없습니다!
cross build --release --target aarch64-unknown-linux-gnu
cross build --release --target x86_64-unknown-linux-musl
cross build --release --target armv7-unknown-linux-gnueabihf

# 교차 테스트 — Docker 이미지에 QEMU가 포함되어 있음
cross test --target aarch64-unknown-linux-gnu

작동 원리: crosscargo를 대신하여 올바른 교차 컴파일 툴체인이 미리 설치된 Docker 컨테이너 내에서 빌드를 실행합니다. 여러분의 소스 코드가 컨테이너에 마운트되고, 결과물은 평소와 같이 target/ 디렉토리에 저장됩니다.

Cross.toml을 이용한 Docker 이미지 커스터마이징:

# Cross.toml
[target.aarch64-unknown-linux-gnu]
# 추가 시스템 라이브러리가 포함된 커스텀 Docker 이미지 사용
image = "my-registry/cross-aarch64:latest"

# 빌드 전 시스템 패키지 설치
pre-build = [
    "dpkg --add-architecture arm64",
    "apt-get update && apt-get install -y libpci-dev:arm64"
]

[target.aarch64-unknown-linux-gnu.env]
# 컨테이너에 환경 변수 전달
passthrough = ["CI", "GITHUB_TOKEN"]

cross는 Docker(또는 Podman)가 필요하지만, 교차 컴파일러, 시스루트, QEMU를 수동으로 설치할 필요를 없애줍니다. CI 환경에서 권장되는 방식입니다.

Zig를 교차 컴파일 링커로 사용하기

Zig는 C 컴파일러와 약 40개 타겟에 대한 교차 컴파일 시스루트를 약 40MB 용량의 단일 다운로드 파일에 포함하고 있습니다. 덕분에 Rust를 위한 매우 편리한 교차 링커로 활용될 수 있습니다.

# Zig 설치 (단일 바이너리, 패키지 매니저 불필요)
# https://ziglang.org/download/ 에서 다운로드
# 또는 패키지 매니저 이용:
sudo snap install zig --classic --beta  # Ubuntu
brew install zig                          # macOS

# cargo-zigbuild 설치
cargo install cargo-zigbuild

왜 Zig인가? 핵심 장점은 glibc 버전 타겟팅입니다. Zig를 사용하면 링크할 glibc 버전을 정확히 지정할 수 있어, 오래된 Linux 배포판에서도 바이너리가 실행되도록 보장할 수 있습니다.

# glibc 2.17 타겟 빌드 (CentOS 7 / RHEL 7 호환성)
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17

# glibc 2.28을 사용하는 aarch64 빌드 (Ubuntu 18.04+)
cargo zigbuild --release --target aarch64-unknown-linux-gnu.2.28

# musl 빌드 (완전 정적)
cargo zigbuild --release --target x86_64-unknown-linux-musl

.2.17 접미사는 Zig의 확장 기능입니다. Zig 링커에 glibc 2.17 심볼 버전을 사용하도록 지시하여, 결과 바이너리가 CentOS 7 이상에서 실행될 수 있게 합니다. Docker, 시스루트 관리, 교차 컴파일러 설치가 모두 필요 없습니다.

비교: cross vs cargo-zigbuild vs 수동 방식

기능수동 방식crosscargo-zigbuild
설정 노력높음 (타겟별 툴체인 설치)낮음 (Docker 필요)낮음 (단일 바이너리)
Docker 필요 여부아니요아니요
glibc 버전 타겟팅불가 (호스트 glibc 사용)불가 (컨테이너 glibc 사용)가능 (정확한 버전 지정)
테스트 실행QEMU 필요포함되어 있음QEMU 필요
macOS → Linux어려움쉬움쉬움
Linux → macOS매우 어려움지원 안 함제한적 지원
바이너리 크기 오버헤드없음없음없음

CI 파이프라인: GitHub Actions 매트릭스

여러 타겟을 빌드하는 운영 환경 수준의 CI 워크플로 예시입니다:

# .github/workflows/cross-build.yml
name: Cross-Platform Build

on: [push, pull_request]

env:
  CARGO_TERM_COLOR: always

jobs:
  build:
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-x86_64
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
            name: linux-x86_64-static
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            name: linux-aarch64
            use_cross: true
          - target: x86_64-pc-windows-msvc
            os: windows-latest
            name: windows-x86_64

    runs-on: ${{ matrix.os }}
    name: Build (${{ matrix.name }})

    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}

      - name: musl 도구 설치
        if: matrix.target == 'x86_64-unknown-linux-musl'
        run: sudo apt-get install -y musl-tools

      - name: cross 설치
        if: matrix.use_cross
        run: cargo install cross

      - name: 빌드 (네이티브)
        if: "!matrix.use_cross"
        run: cargo build --release --target ${{ matrix.target }}

      - name: 빌드 (cross)
        if: matrix.use_cross
        run: cross build --release --target ${{ matrix.target }}

      - name: 테스트 실행
        if: "!matrix.use_cross"
        run: cargo test --target ${{ matrix.target }}

      - name: 아티팩트 업로드
        uses: actions/upload-artifact@v4
        with:
          name: diag_tool-${{ matrix.name }}
          path: target/${{ matrix.target }}/release/diag_tool*

적용 사례: 다중 아키텍처 서버 빌드

현재 바이너리에는 교차 컴파일 설정이 없습니다. 다양한 서버 집합에 배포되는 하드웨어 진단 도구의 경우, 다음을 추가하는 것이 권장됩니다:

my_workspace/
├── .cargo/
│   └── config.toml          ← 타겟별 링커 설정
├── Cross.toml                ← cross 도구 설정
└── .github/workflows/
    └── cross-build.yml       ← 3개 타겟을 위한 CI 매트릭스

권장 .cargo/config.toml:

# 프로젝트를 위한 .cargo/config.toml

# 릴리스 프로필 최적화 (이미 Cargo.toml에 있음, 참조용)
# [profile.release]
# lto = true
# codegen-units = 1
# panic = "abort"
# strip = true

# ARM 서버용 aarch64 (Graviton, Ampere, Grace)
[target.aarch64-unknown-linux-gnu]
linker = "aarch64-linux-gnu-gcc"

# 이식성 있는 정적 바이너리를 위한 musl
[target.x86_64-unknown-linux-musl]
linker = "musl-gcc"

권장 빌드 타겟:

타겟사용 사례배포 대상
x86_64-unknown-linux-gnu기본 네이티브 빌드표준 x86 서버
x86_64-unknown-linux-musl정적 바이너리, 모든 배포판컨테이너, 최소 설치 호스트
aarch64-unknown-linux-gnuARM 서버Graviton, Ampere, Grace

핵심 통찰: 워크스페이스 루트의 Cargo.toml에 있는 [profile.release]에는 이미 lto = true, codegen-units = 1, panic = "abort", strip = true가 설정되어 있습니다. 이는 교차 컴파일된 배포용 바이너리에 이상적인 릴리스 프로필입니다(릴리스 프로필 장의 영향도 표 참조). musl과 결합하면 런타임 의존성이 없는 약 10MB 크기의 단일 정적 바이너리가 생성됩니다.

교차 컴파일 문제 해결 (Troubleshooting)

증상원인해결책
linker 'aarch64-linux-gnu-gcc' not found교차 링커 툴체인 누락sudo apt install gcc-aarch64-linux-gnu
cannot find -lssl (musl 타겟)시스템 OpenSSL이 glibc에 링크됨vendored 기능 사용: openssl = { version = "0.10", features = ["vendored"] }
build.rs가 잘못된 바이너리 실행build.rs는 타겟이 아닌 호스트에서 실행됨build.rs 내에서 cfg!(target_os) 대신 CARGO_CFG_TARGET_OS 확인
로컬 테스트는 통과하나 cross에서 실패Docker 이미지에 테스트 파일 누락Cross.toml을 통해 테스트 데이터 마운트: [build.env] volumes = ["./TestArea:/TestArea"]
undefined reference to __cxa_thread_atexit_impl타겟의 glibc 버전이 너무 낮음cargo-zigbuild로 명시적 glibc 버전 사용: --target x86_64-unknown-linux-gnu.2.17
ARM에서 바이너리 세그폴트 발생잘못된 ARM 변종으로 컴파일됨타겟 트리플이 하드웨어와 일치하는지 확인: 64비트 ARM은 aarch64-unknown-linux-gnu
런타임에 GLIBC_2.XX not found 발생빌드 머신의 glibc가 더 최신임정적 빌드를 위해 musl을 사용하거나, cargo-zigbuild로 glibc 버전 고정

교차 컴파일 의사결정 트리

flowchart TD
    START["교차 컴파일이 필요한가?"] --> STATIC{"정적 바이너리인가?"}
    
    STATIC -->|예| MUSL["musl 타겟\n--target x86_64-unknown-linux-musl"]
    STATIC -->|아니요| GLIBC{"오래된 glibc가 필요한가?"}
    
    GLIBC -->|예| ZIG["cargo-zigbuild\n--target x86_64-unknown-linux-gnu.2.17"]
    GLIBC -->|아니요| ARCH{"타겟 아키텍처?"}
    
    ARCH -->|"동일 아키텍처"| NATIVE["네이티브 툴체인\nrustup target add + 링커"]
    ARCH -->|"ARM/기타"| DOCKER{"Docker 사용 가능한가?"}
    
    DOCKER -->|예| CROSS["cross build\nDocker 기반, 설정 불필요"]
    DOCKER -->|아니요| MANUAL["수동 시스루트 설정\napt install gcc-aarch64-linux-gnu"]
    
    style MUSL fill:#91e5a3,color:#000
    style ZIG fill:#91e5a3,color:#000
    style CROSS fill:#91e5a3,color:#000
    style NATIVE fill:#e3f2fd,color:#000
    style MANUAL fill:#ffd43b,color:#000

🏋️ 실습

🟢 실습 1: 정적 musl 바이너리

아무 Rust 바이너리나 x86_64-unknown-linux-musl용으로 빌드해 보세요. fileldd를 사용하여 정적으로 링크되었는지 확인하세요.

솔루션
rustup target add x86_64-unknown-linux-musl
cargo new hello-static && cd hello-static
cargo build --release --target x86_64-unknown-linux-musl

# 확인
file target/x86_64-unknown-linux-musl/release/hello-static
# 출력: ... statically linked ...

ldd target/x86_64-unknown-linux-musl/release/hello-static
# 출력: not a dynamic executable (동적 실행 파일이 아님)

🟡 실습 2: GitHub Actions 교차 빌드 매트릭스

x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl, aarch64-unknown-linux-gnu 세 가지 타겟을 빌드하는 GitHub Actions 워크플로를 매트릭스 전략을 사용하여 작성해 보세요.

솔루션
name: Cross-build
on: [push]
jobs:
  build:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        target:
          - x86_64-unknown-linux-gnu
          - x86_64-unknown-linux-musl
          - aarch64-unknown-linux-gnu
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          targets: ${{ matrix.target }}
      - name: cross 설치
        run: cargo install cross --locked
      - name: 빌드
        run: cross build --release --target ${{ matrix.target }}
      - uses: actions/upload-artifact@v4
        with:
          name: binary-${{ matrix.target }}
          path: target/${{ matrix.target }}/release/my-binary

핵심 요약

  • Rust의 rustc는 이미 교차 컴파일러입니다. 적절한 타겟과 링커만 준비하면 됩니다.
  • musl은 런타임 의존성이 전혀 없는 완전한 정적 바이너리를 생성하며, 컨테이너 환경에 이상적입니다.
  • **cargo-zigbuild**는 기업용 Linux 타겟을 위한 "glibc 버전" 문제를 해결해 줍니다.
  • **cross**는 ARM이나 기타 특이한 타겟을 위한 가장 쉬운 방법입니다. Docker가 시스루트를 알아서 처리합니다.
  • 항상 fileldd를 사용하여 바이너리가 배포 타겟과 일치하는지 테스트하세요.

벤치마킹 — 중요한 지표 측정하기 🟡

학습 내용:

  • Instant::now()를 이용한 단순한 시간 측정이 신뢰할 수 없는 이유
  • Criterion.rs와 더 가벼운 대안인 Divan을 이용한 통계적 벤치마킹
  • perf, 플레임그래프(flamegraphs), PGO를 이용한 핫스팟(hot spots) 프로파일링
  • 성능 저하를 자동으로 감지하기 위한 CI에서의 지속적인 벤치마킹 설정

참조: 릴리스 프로필 — 핫스팟을 찾았다면 바이너리를 최적화하세요 · CI/CD 파이프라인 — 파이프라인 내의 벤치마크 작업 · 코드 커버리지 — 커버리지는 무엇이 테스트되었는지 알려주고, 벤치마크는 무엇이 빠른지 알려줍니다.

"우리는 약 97%의 시간에 대해 사소한 효율성에 대해서는 잊어야 합니다. 섣부른 최적화(premature optimization)는 모든 악의 뿌리입니다. 하지만 우리는 결정적인 3%의 기회를 놓쳐서는 안 됩니다." — 도널드 커누스(Donald Knuth)

어려운 것은 벤치마크를 작성하는 것이 아니라, 의미 있고 재현 가능하며 실행 가능한 수치를 산출하는 벤치마크를 작성하는 것입니다. 이 장에서는 "빠른 것 같다"는 느낌에서 벗어나 "PR #347이 파싱 처리량을 4.2% 저하시켰다는 통계적 증거가 있다"고 말할 수 있게 해주는 도구와 기술을 다룹니다.

std::time::Instant를 사용하면 안 될까요?

흔히 하는 실수:

// ❌ 단순한 벤치마킹 — 신뢰할 수 없는 결과
use std::time::Instant;

fn main() {
    let start = Instant::now();
    let result = parse_device_query_output(&sample_data);
    let elapsed = start.elapsed();
    println!("파싱 소요 시간: {:?}", elapsed);
    // 문제 1: 컴파일러가 `result`를 최적화로 제거할 수 있음 (데드 코드 제거)
    // 문제 2: 단일 샘플 — 통계적 유의성 없음
    // 문제 3: CPU 주파수 스케일링, 서멀 쓰로틀링, 다른 프로세스의 영향
    // 문제 4: 콜드 캐시(cold cache) vs 웜 캐시(warm cache) 제어 불가
}

수동 시간 측정의 문제점:

  1. 데드 코드 제거 (Dead code elimination) — 결과값이 사용되지 않으면 컴파일러가 계산 전체를 건너뛸 수 있습니다.
  2. 워밍업 부재 (No warm-up) — 첫 번째 실행에는 캐시 미스, OS 페이지 폴트, 지연 초기화(lazy initialization) 등이 포함됩니다.
  3. 통계 분석 부재 — 단 한 번의 측정으로는 분산, 이상치(outliers) 또는 신뢰 구간에 대해 아무것도 알 수 없습니다.
  4. 회귀 감지 불가 — 이전 실행 결과와 정밀하게 비교할 수 없습니다.

Criterion.rs — 통계적 벤치마킹

Criterion.rs는 Rust 마이크로 벤치마크의 표준 도구입니다. 통계적 방법을 사용하여 신뢰할 수 있는 측정값을 생성하고 성능 회귀(performance regressions)를 자동으로 감지합니다.

설정:

# Cargo.toml
[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports", "cargo_bench_support"] }

[[bench]]
name = "parsing_bench"
harness = false  # 내장 테스트 하네스 대신 Criterion의 하네스 사용

전체 벤치마크 예제:

#![allow(unused)]
fn main() {
// benches/parsing_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion, BenchmarkId};

/// 파싱된 GPU 정보를 담는 데이터 타입
#[derive(Debug, Clone)]
struct GpuInfo {
    index: u32,
    name: String,
    temp_c: u32,
    power_w: f64,
}

/// 테스트 대상 함수 — 장치 쿼리 CSV 출력을 파싱하는 시뮬레이션
fn parse_gpu_csv(input: &str) -> Vec<GpuInfo> {
    input
        .lines()
        .filter(|line| !line.starts_with('#'))
        .filter_map(|line| {
            let fields: Vec<&str> = line.split(", ").collect();
            if fields.len() >= 4 {
                Some(GpuInfo {
                    index: fields[0].parse().ok()?,
                    name: fields[1].to_string(),
                    temp_c: fields[2].parse().ok()?,
                    power_w: fields[3].parse().ok()?,
                })
            } else {
                None
            }
        })
        .collect()
}

fn bench_parse_gpu_csv(c: &mut Criterion) {
    // 대표 테스트 데이터
    let small_input = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                       1, Acme Accel-V1-80GB, 34, 67.2\n";

    let large_input = (0..64)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect::<String>();

    c.bench_function("parse_2_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(small_input)))
    });

    c.bench_function("parse_64_gpus", |b| {
        b.iter(|| parse_gpu_csv(black_box(&large_input)))
    });
}

criterion_group!(benches, bench_parse_gpu_csv);
criterion_main!(benches);
}

실행 및 결과 읽기:

# 모든 벤치마크 실행
cargo bench

# 특정 벤치마크 이름으로 실행
cargo bench -- parse_64

# 출력 결과:
# parse_2_gpus        time:   [1.2345 µs  1.2456 µs  1.2578 µs]
#                      ▲            ▲           ▲
#                      │          신뢰 구간
#                   하위 95%      중앙값      상위 95%
#
# parse_64_gpus       time:   [38.123 µs  38.456 µs  38.812 µs]
#                     change: [-1.2345% -0.5678% +0.1234%] (p = 0.12 > 0.05)
#                     성능 변화가 감지되지 않았습니다. (No change in performance detected.)

black_box()의 역할: 컴파일러가 벤치마크 대상을 최적화로 제거하거나 과도하게 상수를 폴딩(folding)하는 것을 방지하는 힌트입니다. 컴파일러는 black_box 내부를 들여다볼 수 없으므로 실제로 계산을 수행해야만 합니다.

매개변수화된 벤치마크 및 벤치마크 그룹

여러 구현 방식이나 입력 크기를 비교할 때 유용합니다:

#![allow(unused)]
fn main() {
// benches/comparison_bench.rs
use criterion::{criterion_group, criterion_main, Criterion, BenchmarkId, Throughput};

fn bench_parsing_strategies(c: &mut Criterion) {
    let mut group = c.benchmark_group("csv_parsing");

    // 다양한 입력 크기에 대해 테스트
    for num_gpus in [1, 8, 32, 64, 128] {
        let input = generate_gpu_csv(num_gpus);

        // 초당 바이트 수 보고를 위한 처리량 설정
        group.throughput(Throughput::Bytes(input.len() as u64));

        group.bench_with_input(
            BenchmarkId::new("split_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_split(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("regex_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_regex(input)),
        );

        group.bench_with_input(
            BenchmarkId::new("nom_based", num_gpus),
            &input,
            |b, input| b.iter(|| parse_nom(input)),
        );
    }
    group.finish();
}

criterion_group!(benches, bench_parsing_strategies);
criterion_main!(benches);
}

결과 확인: Criterion은 target/criterion/report/index.html에 바이올린 플롯(violin plots), 비교 차트, 회귀 분석 결과가 포함된 HTML 보고서를 생성합니다. 브라우저에서 열어보세요.

Divan — 더 가벼운 대안

Divan은 Criterion의 복잡한 매크로 DSL 대신 속성(attribute) 매크로를 사용하는 최신 벤치마킹 프레임워크입니다.

# Cargo.toml
[dev-dependencies]
divan = "0.1"

[[bench]]
name = "parsing_bench"
harness = false
// benches/parsing_bench.rs
use divan::black_box;

const SMALL_INPUT: &str = "0, Acme Accel-V1-80GB, 32, 65.5\n\
                          1, Acme Accel-V1-80GB, 34, 67.2\n";

fn generate_gpu_csv(n: usize) -> String {
    (0..n)
        .map(|i| format!("{i}, Acme Accel-X1-80GB, {}, {:.1}\n", 30 + i % 20, 60.0 + i as f64))
        .collect()
}

fn main() {
    divan::main();
}

#[divan::bench]
fn parse_2_gpus() -> Vec<GpuInfo> {
    parse_gpu_csv(black_box(SMALL_INPUT))
}

#[divan::bench(args = [1, 8, 32, 64, 128])]
fn parse_n_gpus(n: usize) -> Vec<GpuInfo> {
    let input = generate_gpu_csv(n);
    parse_gpu_csv(black_box(&input))
}

// Divan 출력 결과는 깔끔한 표 형식입니다:
// ╰─ parse_2_gpus   fastest  │ slowest  │ median   │ mean     │ samples │ iters
//                   1.234 µs │ 1.567 µs │ 1.345 µs │ 1.350 µs │ 100     │ 1600

Criterion 대신 Divan을 선택하는 경우:

  • 더 단순한 API (속성 매크로 사용, 보일러플레이트 적음)
  • 더 빠른 컴파일 속도 (의존성 적음)
  • 개발 중 빠른 성능 확인에 적합

Criterion을 선택하는 경우:

  • 실행 간의 통계적 회귀 감지 필요
  • 차트가 포함된 HTML 보고서 필요
  • 확립된 에코시스템 및 더 많은 CI 통합 사례

perf와 플레임그래프(Flamegraph)를 이용한 프로파일링

벤치마크가 얼마나 빠른지 알려준다면, 프로파일링은 시간이 어디에 쓰이는지 알려줍니다.

# 1단계: 디버그 정보와 함께 빌드 (릴리스 속도 유지, 디버그 심볼 포함)
cargo build --release
# 디버그 정보가 포함되도록 일시적으로 설정:
# [profile.release]
# debug = true          # 프로파일링을 위해 임시로 추가

# 2단계: perf로 기록
perf record --call-graph=dwarf ./target/release/diag_tool --run-diagnostics

# 3단계: 플레임그래프 생성
# 설치: cargo install flamegraph
# 설치: cargo install addr2line --features=bin (선택 사항, 속도 향상용)
cargo flamegraph --root -- --run-diagnostics
# 대화형 SVG 플레임그래프가 생성됩니다.

플레임그래프 읽는 법:

  • 너비(Width) = 해당 함수에서 소비된 시간 (넓을수록 느림)
  • 높이(Height) = 콜 스택 깊이 (높다고 느린 것이 아니라 호출 단계가 깊은 것임)
  • 하단(Bottom) = 엔트리 포인트(시작점), 상단(Top) = 실제 작업을 수행하는 리프(leaf) 함수
  • 상단에서 넓게 퍼진 평평한 부분(plateaus)을 찾으세요 — 그곳이 바로 핫스팟입니다.

프로필 기반 최적화 (Profile-Guided Optimization, PGO):

# 1단계: 계측(instrumentation)과 함께 빌드
RUSTFLAGS="-Cprofile-generate=/tmp/pgo-data" cargo build --release

# 2단계: 대표적인 워크로드 실행
./target/release/diag_tool --run-full   # 프로파일링 데이터 생성

# 3단계: 프로파일링 데이터 병합
# rustc의 LLVM 버전과 일치하는 llvm-profdata 사용:
# $(rustc --print sysroot)/lib/rustlib/x86_64-unknown-linux-gnu/bin/llvm-profdata
# 또는 llvm-tools 설치: rustup component add llvm-tools
llvm-profdata merge -o /tmp/pgo-data/merged.profdata /tmp/pgo-data/

# 4단계: 프로파일링 피드백과 함께 재빌드
RUSTFLAGS="-Cprofile-use=/tmp/pgo-data/merged.profdata" cargo build --release
# 연산 집약적인 코드(파싱, 암호화, 코드 생성 등)에서 보통 5-20% 성능 향상을 보입니다.
# I/O 위주나 시스템 콜이 많은 코드에서는 큰 효과를 보기 어려울 수 있습니다.

: PGO에 시간을 들이기 전에, 릴리스 프로필에서 LTO가 활성화되어 있는지 먼저 확인하세요. 보통 LTO가 노력 대비 더 큰 성능 향상을 제공합니다.

hyperfine — 빠른 전체 실행 시간 측정

hyperfine은 개별 함수가 아닌 전체 명령의 실행 시간을 벤치마킹합니다. 바이너리의 전체적인 성능을 측정할 때 완벽합니다.

# 설치
cargo install hyperfine

# 기본 벤치마크
hyperfine './target/release/diag_tool --run-diagnostics'

# 두 가지 구현 비교
hyperfine './target/release/diag_tool_v1 --run-diagnostics' \
          './target/release/diag_tool_v2 --run-diagnostics'

# 워밍업 실행 + 최소 반복 횟수 지정
hyperfine --warmup 3 --min-runs 10 './target/release/diag_tool --run-all'

# CI 비교를 위해 결과를 JSON으로 내보내기
hyperfine --export-json bench.json './target/release/diag_tool --run-all'

Criterion vs hyperfine 사용 시점:

  • hyperfine: 전체 바이너리 실행 시간, 리팩토링 전후 비교, I/O 위주 워크로드
  • Criterion: 개별 함수의 마이크로 벤치마킹, 통계적 회귀 감지

CI에서의 지속적인 벤치마킹

배포 전에 성능 저하를 자동으로 감지하세요:

# .github/workflows/bench.yml
name: Benchmarks

on:
  pull_request:
    paths: ['**/*.rs', 'Cargo.toml', 'Cargo.lock']

jobs:
  benchmark:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: dtolnay/rust-toolchain@stable

      - name: Run benchmarks
        run: cargo bench -- --output-format bencher | tee bench_output.txt

      - name: Store benchmark result
        uses: benchmark-action/github-action-benchmark@v1
        with:
          tool: 'cargo'
          output-file-path: bench_output.txt
          github-token: ${{ secrets.GITHUB_TOKEN }}
          auto-push: true
          alert-threshold: '120%'    # 20% 느려지면 알림
          comment-on-alert: true
          fail-on-alert: true        # 회귀 감지 시 PR 차단

CI 설정 시 주의사항:

  • 일관된 결과를 위해 공유 CI가 아닌 전용 벤치마크 러너를 사용하세요.
  • 클라우드 CI를 사용한다면 특정 머신 타입을 고정하세요.
  • 과거 데이터를 저장하여 점진적인 성능 저하를 감지하세요.
  • 워크로드의 허용 오차에 따라 임계값(threshold)을 설정하세요 (실행 경로에 따라 5~20%).

적용 사례: 파싱 성능

이 프로젝트에는 벤치마킹을 통해 성능을 개선할 수 있는 여러 파싱 경로가 있습니다:

파싱 핫스팟크레이트중요 이유
가속기 쿼리 CSV/XML 출력device_diagGPU당 호출됨, 실행당 최대 8회
센서 이벤트 파싱event_log바쁜 서버에서 수천 개의 레코드 처리
PCIe 토폴로지 JSONtopology_lib복잡한 중첩 구조, 골든 파일(golden-file)로 검증됨
보고서 JSON 직렬화diag_framework최종 보고서 출력, 크기에 민감함
설정 JSON 로딩config_loader시작 지연 시간(startup latency)

첫 번째 권장 벤치마크 — 이미 골든 파일 테스트 데이터가 있는 토폴로지 파서:

#![allow(unused)]
fn main() {
// topology_lib/benches/parse_bench.rs (제안)
use criterion::{criterion_group, criterion_main, Criterion, Throughput};
use std::fs;

fn bench_topology_parse(c: &mut Criterion) {
    let mut group = c.benchmark_group("topology_parse");

    for golden_file in ["S2001", "S1015", "S1035", "S1080"] {
        let path = format!("tests/test_data/{golden_file}.json");
        let data = fs::read_to_string(&path).expect("골든 파일을 찾을 수 없습니다");
        group.throughput(Throughput::Bytes(data.len() as u64));

        group.bench_function(golden_file, |b| {
            b.iter(|| {
                topology_lib::TopologyProfile::from_json_str(
                    criterion::black_box(&data)
                )
            });
        });
    }
    group.finish();
}

criterion_group!(benches, bench_topology_parse);
criterion_main!(benches);
}

직접 해보기

  1. Criterion 벤치마크 작성: 코드베이스에서 파싱 함수를 하나 골라 benches/ 디렉토리를 만들고, 초당 바이트 수 처리량을 측정하는 Criterion 벤치마크를 설정해 보세요. cargo bench를 실행하고 HTML 보고서를 확인해 보세요.

  2. 플레임그래프 생성: [profile.release]debug = true를 추가하고 프로젝트를 빌드한 후, cargo flamegraph -- <인자>를 실행해 보세요. 플레임그래프 상단에서 가장 넓은 스택 3개를 찾아보세요. 그곳이 바로 여러분의 핫스팟입니다.

  3. hyperfine으로 비교: hyperfine을 설치하고 바이너리의 전체 실행 시간을 다양한 플래그와 함께 측정해 보세요. 이를 Criterion의 함수 단위 시간과 비교해 보세요. Criterion이 보지 못하는 시간(I/O, 시스템 콜, 프로세스 시작 등)은 어디서 소모되나요?

벤치마킹 도구 선택 가이드

flowchart TD
    START["성능을 측정하고 싶나요?"] --> WHAT{"어떤 수준인가요?"}

    WHAT -->|"개별 함수"| CRITERION["Criterion.rs\n통계적, 회귀 감지"]
    WHAT -->|"빠른 함수 확인"| DIVAN["Divan\n가벼움, 속성 매크로"]
    WHAT -->|"전체 바이너리"| HYPERFINE["hyperfine\n엔드투엔드, 실제 시간"]
    WHAT -->|"핫스팟 찾기"| PERF["perf + flamegraph\nCPU 샘플링 프로파일러"]

    CRITERION --> CI_BENCH["GitHub Actions에서의\n지속적인 벤치마킹"]
    PERF --> OPTIMIZE["프로필 기반\n최적화 (PGO)"]

    style CRITERION fill:#91e5a3,color:#000
    style DIVAN fill:#91e5a3,color:#000
    style HYPERFINE fill:#e3f2fd,color:#000
    style PERF fill:#ffd43b,color:#000
    style CI_BENCH fill:#e3f2fd,color:#000
    style OPTIMIZE fill:#ffd43b,color:#000

🏋️ 실습

🟢 실습 1: 첫 번째 Criterion 벤치마크

10,000개의 무작위 요소를 가진 Vec<u64>를 정렬하는 함수를 가진 크레이트를 만드세요. 이에 대한 Criterion 벤치마크를 작성한 후, .sort_unstable()로 바꾸고 HTML 보고서에서 성능 차이를 관찰해 보세요.

솔루션
# Cargo.toml
[[bench]]
name = "sort_bench"
harness = false

[dev-dependencies]
criterion = { version = "0.5", features = ["html_reports"] }
rand = "0.8"
#![allow(unused)]
fn main() {
// benches/sort_bench.rs
use criterion::{black_box, criterion_group, criterion_main, Criterion};
use rand::Rng;

fn generate_data(n: usize) -> Vec<u64> {
    let mut rng = rand::thread_rng();
    (0..n).map(|_| rng.gen()).collect()
}

fn bench_sort(c: &mut Criterion) {
    let mut group = c.benchmark_group("sort-10k");

    group.bench_function("stable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.bench_function("unstable", |b| {
        b.iter_batched(
            || generate_data(10_000),
            |mut data| { data.sort_unstable(); black_box(&data); },
            criterion::BatchSize::SmallInput,
        )
    });

    group.finish();
}

criterion_group!(benches, bench_sort);
criterion_main!(benches);
}
cargo bench
open target/criterion/sort-10k/report/index.html

🟡 실습 2: 플레임그래프 핫스팟

[profile.release]debug = true를 설정하고 빌드한 후 플레임그래프를 생성하세요. 가장 너비가 넓은 스택 3개를 식별해 보세요.

솔루션
# Cargo.toml
[profile.release]
debug = true  # 플레임그래프를 위해 심볼 유지
cargo install flamegraph
cargo flamegraph --release -- <인자>
# 브라우저에서 flamegraph.svg가 열립니다.
# 상단에서 가장 넓은 스택들이 바로 핫스팟입니다.

핵심 요약

  • 절대로 Instant::now()로 벤치마킹하지 마세요. 통계적 엄밀함과 회귀 감지를 위해 Criterion.rs를 사용하세요.
  • black_box()는 컴파일러가 벤치마크 대상을 최적화로 제거하는 것을 방지합니다.
  • hyperfine은 전체 바이너리의 실행 시간을 측정하고, Criterion은 개별 함수를 측정합니다. 두 도구를 적절히 병용하세요.
  • 플레임그래프는 시간이 어디서 쓰이는지 보여주고, 벤치마크는 시간이 얼마나 쓰이는지 보여줍니다.
  • CI에서의 지속적인 벤치마킹은 성능 저하가 배포되기 전에 잡아낼 수 있게 해줍니다.

코드 커버리지 — 테스트가 놓치는 부분 확인하기 🟢

학습 내용:

  • cargo-llvm-cov를 이용한 소스 기반 커버리지 (가장 정확한 Rust 커버리지 도구)
  • cargo-tarpaulin 및 Mozilla의 grcov를 이용한 빠른 커버리지 확인
  • Codecov 및 Coveralls를 이용한 CI 커버리지 게이트(coverage gates) 설정
  • 고위험 사각지대를 우선시하는 커버리지 기반 테스트 전략

참조: Miri 및 새니타이저 — 커버리지는 테스트되지 않은 코드를 찾고, Miri는 테스트된 코드 내의 미정의 동작(UB)을 찾습니다 · 벤치마킹 — 커버리지는 무엇이 테스트되었는지 보여주고, 벤치마크는 무엇이 빠른지 보여줍니다 · CI/CD 파이프라인 — 파이프라인 내의 커버리지 게이트

코드 커버리지는 테스트가 실제로 어떤 라인, 브랜치 또는 함수를 실행하는지 측정합니다. 커버리지가 높다고 해서 코드의 정답률이 보장되는 것은 아니지만(실행된 라인에도 버그는 있을 수 있음), 테스트가 전혀 닿지 않는 **사각지대(blind spots)**를 확실하게 드러내 줍니다.

수많은 크레이트에 걸쳐 1,000개가 넘는 테스트가 있는 이 프로젝트에서 커버리지 분석은 다음 질문에 답해줍니다: "테스트에 대한 투자가 정말 중요한 코드에 도달하고 있는가?"

llvm-cov를 이용한 소스 기반 커버리지

Rust는 LLVM을 사용하며, LLVM은 가장 정확한 커버리지 측정 방식인 소스 기반 커버리지 계측(instrumentation)을 지원합니다. 권장되는 도구는 cargo-llvm-cov입니다.

# 설치
cargo install cargo-llvm-cov

# 또는 rustup 컴포넌트를 통해 (로우 LLVM 도구용)
rustup component add llvm-tools-preview

기본 사용법:

# 테스트를 실행하고 파일별 커버리지 요약 표시
cargo llvm-cov

# HTML 보고서 생성 (라인별 하이라이트가 포함된 브라우저용 보고서)
cargo llvm-cov --html
# 결과물 위치: target/llvm-cov/html/index.html

# LCOV 형식 생성 (CI 통합용)
cargo llvm-cov --lcov --output-path lcov.info

# 워크스페이스 전체 커버리지 (모든 크레이트)
cargo llvm-cov --workspace

# 특정 패키지만 포함
cargo llvm-cov --package accel_diag --package topology_lib

# 문서 테스트(doc tests) 포함 커버리지
cargo llvm-cov --doctests

HTML 보고서 읽기:

target/llvm-cov/html/index.html
├── 파일명             │ 함수     │ 라인     │ 브랜치   │ 영역
├─ accel_diag/src/lib.rs │  78.5%  │  82.3%  │  61.2%  │  74.1%
├─ sel_mgr/src/parse.rs│  95.2%  │  96.8%  │  88.0%  │  93.5%
├─ topology_lib/src/.. │  91.0%  │  93.4%  │  79.5%  │  89.2%
└─ ...

초록색 = 커버됨    빨간색 = 커버 안 됨    노란색 = 부분적으로 커버됨 (브랜치)

커버리지 유형 설명:

유형측정 대상의미
라인 커버리지어떤 소스 라인이 실행되었는가"이 코드에 도달했는가?"라는 기본 측정
브랜치 커버리지if/match의 어떤 분기가 실행되었는가테스트되지 않은 조건문을 찾아냄
함수 커버리지어떤 함수가 호출되었는가데드 코드(사용되지 않는 코드) 식별
영역 커버리지어떤 코드 영역(하위 표현식)이 실행되었는가가장 세밀한 단위의 측정

cargo-tarpaulin — 빠른 확인을 위한 대안

cargo-tarpaulin은 설정이 더 간단한(LLVM 컴포넌트 불필요) Linux 전용 커버리지 도구입니다.

# 설치
cargo install cargo-tarpaulin

# 기본 커버리지 보고서
cargo tarpaulin

# HTML 출력
cargo tarpaulin --out Html

# 상세 옵션 사용
cargo tarpaulin \
    --workspace \
    --timeout 120 \
    --out Xml Html \
    --output-dir coverage/ \
    --exclude-files "*/tests/*" "*/benches/*" \
    --ignore-panics

# 특정 크레이트 제외
cargo tarpaulin --workspace --exclude diag_tool  # 바이너리 크레이트 제외

tarpaulin vs llvm-cov 비교:

기능cargo-llvm-covcargo-tarpaulin
정확도소스 기반 (가장 정확함)Ptrace 기반 (때때로 과다 측정)
플랫폼모든 플랫폼 (LLVM 기반)Linux 전용
브랜치 커버리지지원함제한적 지원
문서 테스트지원함지원 안 함
설정llvm-tools-preview 필요단독 실행 가능
속도빠름 (컴파일 타임 계측)느림 (ptrace 오버헤드)
안정성매우 안정적임때때로 가양성(false positives) 발생

권장 사항: 정확도가 중요하다면 cargo-llvm-cov를 사용하세요. LLVM 도구 설치 없이 빠르게 확인하고 싶을 때는 cargo-tarpaulin을 사용하세요.

grcov — Mozilla의 커버리지 도구

grcov는 Mozilla에서 만든 커버리지 집계 도구입니다. 로우(raw) LLVM 프로파일링 데이터를 읽어 다양한 형식의 보고서를 생성합니다.

# 설치
cargo install grcov

# 1단계: 커버리지 계측과 함께 빌드
export RUSTFLAGS="-Cinstrument-coverage"
export LLVM_PROFILE_FILE="target/coverage/%p-%m.profraw"
cargo build --tests

# 2단계: 테스트 실행 (.profraw 파일 생성됨)
cargo test

# 3단계: grcov로 집계
grcov target/coverage/ \
    --binary-path target/debug/ \
    --source-dir . \
    --output-types html,lcov \
    --output-path target/coverage/report \
    --branch \
    --ignore-not-existing \
    --ignore "*/tests/*" \
    --ignore "*/.cargo/*"

# 4단계: 보고서 확인
open target/coverage/report/html/index.html

grcov를 사용하는 경우: 유닛 테스트, 통합 테스트, 퍼즈(fuzz) 테스트 등 여러 테스트 실행 결과를 하나의 보고서로 합쳐야 할 때 가장 유용합니다.

CI 환경의 커버리지: Codecov 및 Coveralls

커버리지 데이터를 추적 서비스에 업로드하여 이력 관리 및 PR 주석 기능을 활용해 보세요.

# .github/workflows/coverage.yml
name: Code Coverage

on: [push, pull_request]

jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview

      - name: cargo-llvm-cov 설치
        uses: taiki-e/install-action@cargo-llvm-cov

      - name: 커버리지 생성
        run: cargo llvm-cov --workspace --lcov --output-path lcov.info

      - name: Codecov에 업로드
        uses: codecov/codecov-action@v4
        with:
          files: lcov.info
          token: ${{ secrets.CODECOV_TOKEN }}
          fail_ci_if_error: true

      # 선택 사항: 최소 커버리지 강제
      - name: 커버리지 임계값 확인
        run: |
          cargo llvm-cov --workspace --fail-under-lines 80
          # 라인 커버리지가 80% 미만이면 빌드 실패 처리

커버리지 게이트 (Coverage gates) — JSON 출력을 읽어 크레이트별 최소치를 강제할 수 있습니다.

# 크레이트별 커버리지를 JSON으로 가져오기
cargo llvm-cov --workspace --json | jq '.data[0].totals.lines.percent'

# 임계값 미달 시 실패 처리
cargo llvm-cov --workspace --fail-under-lines 80
cargo llvm-cov --workspace --fail-under-functions 70
cargo llvm-cov --workspace --fail-under-regions 60

커버리지 기반 테스트 전략 (Coverage-Guided Testing Strategy)

전략 없는 커버리지 수치는 무의미합니다. 커버리지 데이터를 효과적으로 활용하는 방법은 다음과 같습니다:

1단계: 위험도에 따른 분류

높은 커버리지, 높은 위험도   → ✅ 양호 — 상태 유지
높은 커버리지, 낮은 위험도   → 🔄 과잉 테스트 가능성 — 느리다면 테스트 축소 고려
낮은 커버리지, 높은 위험도   → 🔴 즉시 테스트 작성 — 버그가 숨어있을 확률 높음
낮은 커버리지, 낮은 위험도   → 🟡 추적은 하되 당장 조급해할 필요 없음

2단계: 라인이 아닌 브랜치 커버리지에 집중하기

#![allow(unused)]
fn main() {
// 라인 커버리지 100%, 브랜치 커버리지 50% — 여전히 위험합니다!
pub fn classify_temperature(temp_c: i32) -> ThermalState {
    if temp_c > 105 {       // ← temp=110으로 테스트됨 → Critical
        ThermalState::Critical
    } else if temp_c > 85 { // ← temp=90으로 테스트됨 → Warning
        ThermalState::Warning
    } else if temp_c < -10 { // ← 테스트된 적 없음 → 센서 오류 케이스 누락
        ThermalState::SensorError
    } else {
        ThermalState::Normal  // ← temp=25로 테스트됨 → Normal
    }
}
}

3단계: 노이즈 제거

# 테스트 코드를 커버리지에서 제외 (테스트 코드는 항상 "커버"되므로)
cargo llvm-cov --workspace --ignore-filename-regex 'tests?\.rs$|benches/'

# 생성된 코드 제외
cargo llvm-cov --workspace --ignore-filename-regex 'target/'

코드 내에서 테스트 불가능한 섹션 표시하기:

#![allow(unused)]
fn main() {
// 커버리지 도구가 이 패턴을 인식합니다.
#[cfg(not(tarpaulin_include))]  // tarpaulin용
fn unreachable_hardware_path() {
    // 이 경로는 실제 GPU 하드웨어가 있어야만 실행 가능합니다.
}

// llvm-cov의 경우 더 정교한 접근 방식을 사용하세요:
// 일부 경로는 유닛 테스트가 아닌 통합/하드웨어 테스트가 필요함을 인정하고,
// 커버리지 예외 목록으로 관리하십시오.
}

상호 보완적인 테스트 도구

proptest — 속성 기반 테스트 (Property-Based Testing) 는 수동으로 작성한 테스트가 놓치기 쉬운 에지 케이스를 찾아줍니다.

[dev-dependencies]
proptest = "1"
#![allow(unused)]
fn main() {
use proptest::prelude::*;

proptest! {
    #[test]
    fn parse_never_panics(input in "\\PC*") {
        // proptest가 수천 개의 무작위 문자열을 생성합니다.
        // 어떤 입력에 대해서라도 parse_gpu_csv가 패닉을 일으키면 테스트는 실패하며,
        // proptest는 실패 원인이 된 최소한의 입력을 찾아줍니다.
        let _ = parse_gpu_csv(&input);
    }

    #[test]
    fn temperature_roundtrip(raw in 0u16..4096) {
        let temp = Temperature::from_raw(raw);
        let md = temp.millidegrees_c();
        // 속성: 밀리섭씨 온도는 항상 raw 값으로부터 유도 가능해야 함
        assert_eq!(md, (raw as i32) * 625 / 10);
    }
}
}

insta — 스냅샷 테스트 (Snapshot Testing) 는 대규모 구조화된 출력(JSON, 텍스트 보고서 등)을 테스트할 때 유용합니다.

[dev-dependencies]
insta = { version = "1", features = ["json"] }
#![allow(unused)]
fn main() {
#[test]
fn test_der_report_format() {
    let report = generate_der_report(&test_results);
    // 첫 실행 시 스냅샷 파일을 생성하고, 이후 실행 시 결과와 비교합니다.
    // 변경 사항은 `cargo insta review`를 통해 대화형으로 승인할 수 있습니다.
    insta::assert_json_snapshot!(report);
}
}

proptest/insta 도입 시기: 유닛 테스트가 모두 "정상적인 상황"만 다루고 있다면 proptest가 놓친 에지 케이스를 찾아줄 것입니다. JSON 보고서와 같이 큰 출력 형식을 테스트한다면, 직접 단언문(assertion)을 작성하는 것보다 insta 스냅샷이 작성과 유지보수가 훨씬 빠릅니다.

적용 사례: 1,000개 이상의 테스트 커버리지 맵

이 프로젝트에는 1,000개 이상의 테스트가 있지만 커버리지 추적은 이루어지지 않고 있습니다. 커버리지를 추가하면 테스트 투자가 어디에 집중되어 있는지 알 수 있습니다. 커버되지 않은 경로는 Miri 및 새니타이저 검증의 주요 대상이 됩니다.

권장 커버리지 설정:

# 워크스페이스 빠른 커버리지 확인 (제안된 CI 명령어)
cargo llvm-cov --workspace \
    --ignore-filename-regex 'tests?\.rs$' \
    --fail-under-lines 75 \
    --html

# 특정 크레이트의 집중 개선을 위한 커버리지 확인
for crate in accel_diag event_log topology_lib network_diag compute_diag fan_diag; do
    echo "=== $crate ==="
    cargo llvm-cov --package "$crate" --json 2>/dev/null | \
        jq -r '.data[0].totals | "라인: \(.lines.percent | round)%  브랜치: \(.branches.percent | round)%"'
done

커버리지가 높을 것으로 예상되는 크레이트 (테스트 밀도 기반):

  • topology_lib — 922라인의 골든 파일(golden-file) 테스트 스위트 보유
  • event_logcreate_test_record() 헬퍼를 활용한 레지스트리 테스트
  • cable_diagmake_test_event() / make_test_context() 패턴 활용

커버리지 사각지대로 예상되는 부분 (코드 검토 기반):

  • IPMI 통신 경로의 오류 처리 분기
  • 특정 GPU 하드웨어 전용 브랜치 (실제 GPU 필요)
  • dmesg 파싱의 에지 케이스 (플랫폼별 출력 차이)

커버리지의 80/20 법칙: 0%에서 80%까지 커버리지를 올리는 것은 직관적입니다. 하지만 80%에서 95%로 올리려면 점점 더 작위적인 테스트 시나리오가 필요해집니다. 95%에서 100%를 달성하는 것은 #[cfg(not(...))] 제외 처리가 필요하며, 그 노력만큼의 가치를 얻기 힘듭니다. 실무적으로는 **라인 커버리지 80%, 브랜치 커버리지 70%**를 최소 기준으로 삼는 것이 적절합니다.

커버리지 문제 해결 (Troubleshooting)

증상원인해결책
llvm-cov 결과가 모든 파일에서 0%로 나옴계측이 적용되지 않음cargo test 후에 llvm-cov를 따로 실행하지 말고 cargo llvm-cov 명령어를 사용하세요.
unreachable!()이 커버되지 않은 것으로 나옴컴파일된 코드에는 해당 분기가 존재함#[cfg(not(tarpaulin_include))]를 사용하거나 제외 정규표현식에 추가하세요.
커버리지 측정 중 테스트 바이너리가 충돌함계측과 새니타이저의 충돌cargo llvm-cov-Zsanitizer=address를 동시에 실행하지 말고 따로 실행하세요.
llvm-covtarpaulin의 결과가 다름계측 기술의 차이컴파일러 네이티브 방식인 llvm-cov를 기준으로 삼으세요. 차이가 크다면 이슈를 보고하세요.
error: profraw file is malformed 오류테스트 바이너리가 실행 도중 예기치 않게 종료됨먼저 테스트 실패를 해결하세요. 프로세스가 비정상 종료되면 프로파일링 데이터가 손상됩니다.
브랜치 커버리지가 비정상적으로 낮게 나옴최적화 과정에서 매치 팔, unwrap 등에 브랜치가 생성됨실무적인 지표로는 라인 커버리지에 집중하세요. 브랜치 커버리지는 구조상 낮게 측정되는 경향이 있습니다.

직접 해보기

  1. 내 프로젝트 커버리지 측정: cargo llvm-cov --workspace --html을 실행하고 보고서를 열어보세요. 커버리지가 가장 낮은 파일 3개를 찾아보세요. 단순히 테스트가 없는 것인가요, 아니면 하드웨어 의존성 때문에 테스트하기 어려운 코드인가요?

  2. 커버리지 게이트 설정: CI에 cargo llvm-cov --workspace --fail-under-lines 60을 추가해 보세요. 의도적으로 테스트 하나를 주석 처리하고 CI가 실패하는지 확인해 보세요. 그 후 임계값을 실제 커버리지보다 2% 낮은 수준으로 높여보세요.

  3. 브랜치 vs 라인 커버리지: 3개의 분기가 있는 match 함수를 작성하고 2개 분기만 테스트해 보세요. 라인 커버리지(약 66% 예상)와 브랜치 커버리지(약 50% 예상)를 비교해 보세요. 프로젝트에 어떤 지표가 더 유용한가요?

커버리지 도구 선택 가이드

flowchart TD
    START["코드 커버리지가 필요한가?"] --> ACCURACY{"우선순위는?"}
    
    ACCURACY -->|"정확도 최우선"| LLVM["cargo-llvm-cov\n소스 기반, 컴파일러 네이티브"]
    ACCURACY -->|"빠른 확인"| TARP["cargo-tarpaulin\nLinux 전용, 간편함"]
    ACCURACY -->|"여러 실행 결과 집계"| GRCOV["grcov\nMozilla 개발, 프로필 통합"]
    
    LLVM --> CI_GATE["CI 커버리지 게이트\n--fail-under-lines 80"]
    TARP --> CI_GATE
    
    CI_GATE --> UPLOAD{"업로드 대상?"}
    UPLOAD -->|"Codecov"| CODECOV["codecov/codecov-action"]
    UPLOAD -->|"Coveralls"| COVERALLS["coverallsapp/github-action"]
    
    style LLVM fill:#91e5a3,color:#000
    style TARP fill:#e3f2fd,color:#000
    style GRCOV fill:#e3f2fd,color:#000
    style CI_GATE fill:#ffd43b,color:#000

🏋️ 실습

🟢 실습 1: 첫 번째 커버리지 보고서

cargo-llvm-cov를 설치하고 아무 Rust 프로젝트에서나 실행한 후 HTML 보고서를 열어보세요. 라인 커버리지가 가장 낮은 파일 3개를 찾아보세요.

솔루션
cargo install cargo-llvm-cov
cargo llvm-cov --workspace --html --open
# 보고서는 커버리지 순으로 정렬됩니다 — 낮은 것이 아래쪽에 위치합니다.
# 50% 미만인 파일들을 살펴보세요 — 그곳이 여러분의 사각지대입니다.

🟡 실습 2: CI 커버리지 게이트

라인 커버리지가 60% 미만으로 떨어지면 실패하는 커버리지 게이트를 GitHub Actions 워크플로에 추가해 보세요. 테스트 하나를 주석 처리하여 제대로 작동하는지 확인하세요.

솔루션
# .github/workflows/coverage.yml
name: Coverage
on: [push, pull_request]
jobs:
  coverage:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: llvm-tools-preview
      - run: cargo install cargo-llvm-cov
      - run: cargo llvm-cov --workspace --fail-under-lines 60

테스트를 주석 처리하고 푸시한 뒤 워크플로가 실패하는지 확인하세요.

핵심 요약

  • cargo-llvm-cov는 컴파일러 고유의 계측 기능을 사용하는 가장 정확한 Rust 커버리지 도구입니다.
  • 커버리지가 높다고 정답을 보장하지는 않지만, 커버리지가 0%라는 것은 테스트가 전혀 안 되었다는 증거입니다. 사각지대를 찾는 데 활용하세요.
  • CI에 커버리지 게이트(예: --fail-under-lines 80)를 설정하여 성능 저하를 방지하세요.
  • 100% 커버리지에 집착하지 마세요. 고위험 코드 경로(오류 처리, unsafe, 파싱)에 집중하세요.
  • 커버리지 계측과 새니타이저를 같은 실행 주기에서 혼용하지 마세요.

Miri, Valgrind 및 새니타이저 — Unsafe 코드 검증 🔴

학습 내용:

  • MIR 인터프리터로서의 Miri — 감지 가능한 항목(에일리어싱, UB, 누수)과 한계(FFI, 시스템 콜)
  • Valgrind memcheck, Helgrind(데이터 레이스), Callgrind(프로파일링), Massif(힙 메모리)
  • LLVM 새니타이저: 나이틀리(nightly) -Zbuild-std를 이용한 ASan, MSan, TSan, LSan
  • 크래시 발견을 위한 cargo-fuzz와 동시성 모델 체킹을 위한 loom
  • 올바른 검증 도구 선택을 위한 의사결정 트리

참조: 코드 커버리지 — 커버리지는 테스트되지 않은 경로를 찾고, Miri는 테스트된 경로를 검증합니다 · no_std 및 기능no_std 코드는 종종 Miri로 검증 가능한 unsafe를 필요로 합니다 · CI/CD 파이프라인 — 파이프라인 내의 Miri 작업

안전한 Rust(Safe Rust)는 컴파일 타임에 메모리 안전성과 데이터 레이스(data-race) 부재를 보장합니다. 하지만 FFI, 직접 구현한 데이터 구조 또는 성능 최적화를 위해 unsafe 블록을 작성하는 순간, 이러한 보장은 온전히 개발자의 책임이 됩니다. 이 장에서는 여러분의 unsafe 코드가 주장하는 안전성 계약(safety contracts)을 실제로 준수하는지 검증하는 도구들을 다룹니다.

Miri — Unsafe Rust를 위한 인터프리터

Miri는 Rust의 중간 표현인 MIR(Mid-level Intermediate Representation)을 위한 인터프리터입니다. 프로그램을 기계어로 컴파일하는 대신, Miri는 모든 작업에서 미정의 동작(undefined behavior)을 철저히 검사하며 프로그램을 단계별로 실행합니다.

# Miri 설치 (나이틀리 전용 컴포넌트)
rustup +nightly component add miri

# Miri 환경에서 테스트 스위트 실행
cargo +nightly miri test

# Miri 환경에서 특정 바이너리 실행
cargo +nightly miri run

# 특정 테스트 실행
cargo +nightly miri test -- 테스트_이름

Miri의 작동 원리:

소스 코드 → rustc → MIR → Miri가 MIR을 해석 및 실행
                           │
                           ├─ 모든 포인터의 프로버넌스(provenance, 출처) 추적
                           ├─ 모든 메모리 접근 유효성 검사
                           ├─ 모든 역참조 시 정렬(alignment) 상태 확인
                           ├─ Use-after-free(해제 후 사용) 감지
                           ├─ Double free(이중 해제) 감지
                           ├─ 데이터 레이스 감지 (스레드 사용 시)
                           └─ Stacked Borrows / Tree Borrows 규칙 강제

Miri가 감지하는 것 (그리고 감지하지 못하는 것)

Miri가 감지하는 항목:

카테고리예시런타임에 크래시가 발생하는가?
범위를 벗어난 접근할당 범위를 넘어 ptr.add(100).read() 실행때에 따라 다름 (페이지 레이아웃에 의존)
해제 후 사용 (UAF)드롭된 Box를 생포인터(raw pointer)로 읽기때에 따라 다름 (할당기에 의존)
이중 해제drop_in_place를 두 번 호출보통 발생함
정렬되지 않은 접근홀수 주소에서 (ptr as *const u32).read() 실행일부 아키텍처에서 발생함
잘못된 값transmute::<u8, bool>(2) 실행조용히 잘못된 결과 산출
댕글링 참조해제된 위치를 가리키는 &*ptr 생성아니요 (조용한 데이터 오염 발생)
데이터 레이스동기화 없이 두 스레드가 동일 위치에 쓰기간헐적 발생, 재현하기 어려움
Stacked Borrows 위반&mut 참조의 에일리어싱(중복 참조)아니요 (조용한 데이터 오염 발생)

Miri가 감지하지 못하는 한계:

한계점이유
로직 버그Miri는 메모리 안전성을 검사하며, 논리적 정확성은 검사하지 않음
동시성 데드락Miri는 데이터 레이스를 검사하며, 라이브락(livelocks)은 검사하지 않음
성능 문제인터프리터 방식은 네이티브 실행보다 10~100배 느림
OS/하드웨어 상호작용Miri는 시스템 콜이나 장치 I/O를 에뮬레이션할 수 없음
모든 FFI 호출C 코드를 해석할 수 없음 (Rust MIR만 가능)
모든 경로 커버테스트 스위트가 도달하는 경로만 테스트함

구체적인 예시 — 실무에서는 "작동"하지만 불건전한(unsound) 코드 잡아내기:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn test_miri_catches_ub() {
        // 이 코드는 릴리스 빌드에서 "작동"할 수 있지만 미정의 동작입니다.
        let mut v = vec![1, 2, 3];
        let ptr = v.as_ptr();

        // push 작업은 재할당을 유발하여 ptr을 무효화할 수 있습니다.
        v.push(4);

        // ❌ UB: 재할당 후 ptr은 댕글링 포인터가 될 수 있습니다.
        // 할당기가 운 좋게 버퍼를 이동시키지 않았더라도 Miri는 이를 잡아냅니다.
        // let _val = unsafe { *ptr };
        // 에러: Miri는 다음과 같이 보고합니다:
        //   "pointer to alloc1234 was dereferenced after this
        //    allocation got freed"
        
        // ✅ 올바른 방법: 변경 작업 후 새 포인터를 가져옵니다.
        let ptr = v.as_ptr();
        let val = unsafe { *ptr };
        assert_eq!(val, 1);
    }
}
}

실제 크레이트에서 Miri 실행하기

unsafe가 포함된 크레이트를 위한 실무 Miri 워크플로:

# 1단계: 모든 테스트를 Miri 환경에서 실행
cargo +nightly miri test 2>&1 | tee miri_output.txt

# 2단계: 에러가 발생하면 해당 테스트만 격리해서 실행
cargo +nightly miri test -- 실패한_테스트_이름

# 3단계: 진단을 위해 Miri의 백트레이스 활용
MIRIFLAGS="-Zmiri-backtrace=full" cargo +nightly miri test

# 4단계: 빌림(borrow) 모델 선택
# Stacked Borrows (기본값, 더 엄격함):
cargo +nightly miri test

# Tree Borrows (실험적, 더 관대함):
MIRIFLAGS="-Zmiri-tree-borrows" cargo +nightly miri test

일반적인 상황을 위한 Miri 플래그:

# 격리 해제 (파일 시스템 접근, 환경 변수 허용)
MIRIFLAGS="-Zmiri-disable-isolation" cargo +nightly miri test

# Miri에서는 메모리 누수 감지가 기본으로 켜져 있습니다.
# 의도적인 누수 등에 대해 이를 무시하려면:
# MIRIFLAGS="-Zmiri-ignore-leaks" cargo +nightly miri test

# 무작위 테스트의 재현성을 위해 RNG 시드 고정
MIRIFLAGS="-Zmiri-seed=42" cargo +nightly miri test

# 엄격한 프로버넌스(provenance) 검사 활성화
MIRIFLAGS="-Zmiri-strict-provenance" cargo +nightly miri test

# 여러 플래그 동시에 사용
MIRIFLAGS="-Zmiri-disable-isolation -Zmiri-backtrace=full -Zmiri-strict-provenance" \
    cargo +nightly miri test

CI 환경의 Miri:

# .github/workflows/miri.yml
name: Miri
on: [push, pull_request]

jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@nightly
        with:
          components: miri

      - name: Miri 실행
        run: cargo miri test --workspace
        env:
          MIRIFLAGS: "-Zmiri-backtrace=full"
          # 누수 검사는 기본적으로 활성화되어 있습니다.
          # Miri가 처리할 수 없는 시스템 콜(파일 I/O, 네트워크 등)을
          # 사용하는 테스트는 건너뛰도록 설정하세요.

성능 참고: Miri는 네이티브 실행보다 10~100배 느립니다. 네이티브에서 5초 걸리는 테스트가 Miri에서는 5분 이상 걸릴 수 있습니다. CI에서는 unsafe 코드가 포함된 크레이트에 집중해서 실행하세요.

Valgrind와 Rust 통합

Valgrind는 고전적인 C/C++ 메모리 검사 도구입니다. 컴파일된 Rust 바이너리에서도 작동하며 기계어 수준에서 메모리 오류를 검사합니다.

# Valgrind 설치
sudo apt install valgrind  # Debian/Ubuntu
sudo dnf install valgrind  # Fedora

# 디버그 정보와 함께 빌드 (Valgrind는 심볼이 필요함)
cargo build --tests
# 또는 디버그 정보를 포함한 릴리스 빌드:
# cargo build --release
# [profile.release]
# debug = true

# Valgrind 환경에서 특정 테스트 바이너리 실행
valgrind --tool=memcheck \
    --leak-check=full \
    --show-leak-kinds=all \
    --track-origins=yes \
    ./target/debug/deps/my_crate-abc123 --test-threads=1

# 메인 바이너리 실행
valgrind --tool=memcheck \
    --leak-check=full \
    --error-exitcode=1 \
    ./target/debug/diag_tool --run-diagnostics

memcheck 이외의 Valgrind 도구들:

도구명령어감지 대상
Memcheck--tool=memcheck메모리 누수, 해제 후 사용, 버퍼 오버플로
Helgrind--tool=helgrind데이터 레이스 및 락 순서 위반(데드락 위험)
DRD--tool=drd데이터 레이스 (다른 감지 알고리즘 사용)
Callgrind--tool=callgrindCPU 명령 단위 프로파일링 (경로 수준)
Massif--tool=massif시간에 따른 힙 메모리 사용량 프로파일링
Cachegrind--tool=cachegrind캐시 미스 분석

명령어 수준 프로파일링을 위한 Callgrind 사용:

# 명령 횟수 기록 (실제 시간보다 결과가 안정적임)
valgrind --tool=callgrind \
    --callgrind-out-file=callgrind.out \
    ./target/release/diag_tool --run-diagnostics

# KCachegrind로 시각화
kcachegrind callgrind.out
# 또는 텍스트 기반 대안:
callgrind_annotate callgrind.out | head -100

Miri vs Valgrind — 어떤 도구를 사용할까?

비교 항목MiriValgrind
Rust 고유 UB 검사✅ Stacked/Tree Borrows❌ Rust 규칙 인지 못 함
C FFI 코드 검사❌ C 코드 해석 불가✅ 모든 기계어 검사
나이틀리 필요 여부✅ 필요함❌ 필요 없음
속도10~100배 느림10~50배 느림
플랫폼모든 플랫폼 (MIR 해석)Linux, macOS (네이티브 실행)
데이터 레이스 감지✅ 가능✅ 가능 (Helgrind/DRD)
누수 감지✅ 가능✅ 가능 (더 철저함)
가양성(False Positives)매우 드묾가끔 발생 (특히 할당기 관련)

둘 다 활용하세요:

  • Miri는 순수 Rust unsafe 코드(Stacked Borrows, 프로버넌스) 검증에 사용합니다.
  • Valgrind는 FFI 비중이 높은 코드와 전체 프로그램의 누수 분석에 사용합니다.

AddressSanitizer, MemorySanitizer, ThreadSanitizer

LLVM 새니타이저(sanitizers)는 컴파일 타임에 런타임 검사 코드를 삽입하는 방식입니다. Valgrind보다 빠르며(2~5배 오버헤드 vs 10~50배) 서로 다른 클래스의 버그를 잡아냅니다.

# 필수: 새니타이저 계측을 포함해 std를 재빌드하기 위해 Rust 소스 설치
rustup component add rust-src --toolchain nightly

# AddressSanitizer (ASan) — 버퍼 오버플로, 해제 후 사용, 스택 오버플로
RUSTFLAGS="-Zsanitizer=address" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# MemorySanitizer (MSan) — 초기화되지 않은 메모리 읽기
RUSTFLAGS="-Zsanitizer=memory" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ThreadSanitizer (TSan) — 데이터 레이스
RUSTFLAGS="-Zsanitizer=thread" \
    cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# LeakSanitizer (LSan) — 메모리 누수 (ASan에 기본 포함됨)
RUSTFLAGS="-Zsanitizer=leak" \
    cargo +nightly test --target x86_64-unknown-linux-gnu

참고: ASan, MSan, TSan은 -Zbuild-std를 통해 표준 라이브러리를 새니타이저 계측과 함께 재빌드해야 합니다. LSan은 필요하지 않습니다.

새니타이저 비교:

새니타이저오버헤드감지 대상나이틀리 필요?-Zbuild-std 필요?
ASan메모리 2배, CPU 2배버퍼 오버플로, 해제 후 사용, 스택 오버플로
MSan메모리 3배, CPU 3배초기화되지 않은 읽기
TSan메모리 5~10배, CPU 5배데이터 레이스
LSan매우 적음메모리 누수아니요

실전 예제 — TSan으로 데이터 레이스 잡아내기:

#![allow(unused)]
fn main() {
use std::sync::Arc;
use std::thread;

fn racy_counter() -> u64 {
    // ❌ UB: 동기화되지 않은 공유 가변 상태
    let data = Arc::new(std::cell::UnsafeCell::new(0u64));
    let mut handles = vec![];

    for _ in 0..4 {
        let data = Arc::clone(&data);
        handles.push(thread::spawn(move || {
            for _ in 0..1000 {
                // SAFETY: 불건전함(UNSOUND) — 데이터 레이스 발생!
                unsafe {
                    *data.get() += 1;
                }
            }
        }));
    }

    for h in handles {
        h.join().unwrap();
    }

    // 값은 4000이어야 하지만 레이스로 인해 어떤 값이라도 나올 수 있음
    unsafe { *data.get() }
}

// Miri와 TSan 모두 이를 잡아냅니다:
// Miri:  "Data race detected between (1) write and (2) write"
// TSan:  "WARNING: ThreadSanitizer: data race"
//
// 해결책: AtomicU64 또는 Mutex<u64> 사용
}

관련 도구: 퍼징(Fuzzing) 및 동시성 검증

cargo-fuzz — 커버리지 기반 퍼징 (파서 및 디코더의 크래시 발견):

# 설치
cargo install cargo-fuzz

# 퍼즈 타겟 초기화
cargo fuzz init
cargo fuzz add parse_gpu_csv
#![allow(unused)]
fn main() {
// fuzz/fuzz_targets/parse_gpu_csv.rs
#![no_main]
use libfuzzer_sys::fuzz_target;

fuzz_target!(|data: &[u8]| {
    if let Ok(s) = std::str::from_utf8(data) {
        // 퍼저가 수백만 개의 입력을 생성하며 패닉이나 크래시를 찾습니다.
        let _ = diag_tool::parse_gpu_csv(s);
    }
});
}
# 퍼저 실행 (중단되거나 크래시를 찾을 때까지 실행)
cargo +nightly fuzz run parse_gpu_csv -- -max_total_time=300  # 5분 동안 실행

# 크래시 유발 사례 최소화(minimize)
cargo +nightly fuzz tmin parse_gpu_csv artifacts/parse_gpu_csv/crash-...

퍼징 도입 시기: 신뢰할 수 없는 입력(센서 출력, 설정 파일, 네트워크 데이터, JSON/CSV 등)을 파싱하는 모든 함수에 도입하세요. 퍼징은 serde, regex, image 등 거의 모든 주요 Rust 파서 크레이트에서 실제 버그를 찾아낸 바 있습니다.

loom — 동시성 모델 체커 (원자적 연산 순서를 철저히 테스트):

[dev-dependencies]
loom = "0.7"
#![allow(unused)]
fn main() {
#[cfg(loom)]
mod tests {
    use loom::sync::atomic::{AtomicUsize, Ordering};
    use loom::thread;

    #[test]
    fn test_counter_is_atomic() {
        loom::model(|| {
            let counter = loom::sync::Arc::new(AtomicUsize::new(0));
            let c1 = counter.clone();
            let c2 = counter.clone();

            let t1 = thread::spawn(move || { c1.fetch_add(1, Ordering::SeqCst); });
            let t2 = thread::spawn(move || { c2.fetch_add(1, Ordering::SeqCst); });

            t1.join().unwrap();
            t2.join().unwrap();

            // loom은 가능한 모든 스레드 인터리빙(interleaving)을 탐색합니다.
            assert_eq!(counter.load(Ordering::SeqCst), 2);
        });
    }
}
}

loom 도입 시기: 락 프리(lock-free) 데이터 구조나 커스텀 동기화 프리미티브를 구현할 때 사용하세요. loom은 모든 스레드 실행 경로를 샅샅이 뒤지는 모델 체커이지, 단순한 스트레스 테스트 도구가 아닙니다. MutexRwLock 위주의 코드에는 필요하지 않습니다.

상황별 도구 선택 가이드

Unsafe 검증을 위한 의사결정 트리:

FFI 호출이 없는 순수 Rust 코드인가?
├─ 예 → Miri 사용 (Rust 고유 UB, Stacked Borrows 감지)
│      심층 방어를 위해 CI에서 ASan도 함께 실행
└─ 아니오 (FFI를 통해 C/C++ 코드 호출)
   ├─ 메모리 안전성이 걱정되는가?
   │  └─ 예 → Valgrind memcheck 및 ASan 병행 사용
   ├─ 동시성 문제가 걱정되는가?
   │  └─ 예 → TSan(빠름) 또는 Helgrind(더 철저함) 사용
   └─ 메모리 누수가 걱정되는가?
      └─ 예 → Valgrind --leak-check=full 사용

권장 CI 매트릭스:

# 빠른 피드백을 위해 모든 도구를 병렬로 실행
jobs:
  miri:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
        with: { components: miri }
      - run: cargo miri test --workspace

  asan:
    runs-on: ubuntu-latest
    steps:
      - uses: dtolnay/rust-toolchain@nightly
      - run: |
          RUSTFLAGS="-Zsanitizer=address" \
          cargo test -Zbuild-std --target x86_64-unknown-linux-gnu

  valgrind:
    runs-on: ubuntu-latest
    steps:
      - run: sudo apt-get install -y valgrind
      - uses: dtolnay/rust-toolchain@stable
      - run: cargo build --tests
      - run: |
          for test_bin in $(find target/debug/deps -maxdepth 1 -executable -type f ! -name '*.d'); do
            valgrind --error-exitcode=1 --leak-check=full "$test_bin" --test-threads=1
          done

적용 사례: Unsafe 제로와 도입 시점

이 프로젝트는 9만 라인이 넘는 Rust 코드 전반에 걸쳐 unsafe 블록이 단 하나도 없습니다. 이는 시스템 수준의 진단 도구로서 놀라운 성과이며, 다음과 같은 작업에 Safe Rust만으로 충분함을 보여줍니다:

  • IPMI 통신 (ipmitool 서브프로세스를 통한 std::process::Command 사용)
  • GPU 쿼리 (accel-query 서브프로세스 사용)
  • PCIe 토폴로지 파싱 (순수 JSON/텍스트 파싱)
  • SEL 레코드 관리 (순수 데이터 구조)
  • DER 보고서 생성 (JSON 직렬화)

그렇다면 언제 unsafe가 필요할까요?

도입이 예상되는 시나리오는 다음과 같습니다:

시나리오unsafe 도입 이유권장 검증 도구
ioctl 기반 직접 IPMIipmitool 우회, libc::ioctl() 직접 호출Miri + Valgrind
직접적인 GPU 드라이버 쿼리서브프로세스 대신 accel-mgmt FFI 사용Valgrind (C 라이브러리 검사)
메모리 맵 기반 PCIe 설정직접적인 설정 공간 읽기를 위한 mmapASan + Valgrind
락 프리 SEL 버퍼동시 이벤트 수집을 위한 AtomicPtr 활용Miri + TSan
임베디드/no_std 변종베어 메탈을 위한 생포인터 조작Miri

준비 사항: unsafe를 도입하기 전에 CI에 검증 도구를 추가하세요.

# Cargo.toml — unsafe 최적화를 위한 기능 플래그 추가
[features]
default = []
direct-ipmi = []     # ipmitool 대신 ioctl 기반 직접 IPMI 활성화
direct-accel-api = []     # 서브프로세스 대신 accel-mgmt FFI 활성화
#![allow(unused)]
fn main() {
// src/ipmi.rs — 기능 플래그로 보호
#[cfg(feature = "direct-ipmi")]
mod direct {
    //! /dev/ipmi0 ioctl을 이용한 직접 IPMI 장치 접근.
    //!
    //! # 안전성 (Safety)
    //! 이 모듈은 ioctl 시스템 콜을 위해 `unsafe`를 사용합니다.
    //! 검증 도구: Miri(가능한 경우), Valgrind memcheck, ASan.

    use std::os::unix::io::RawFd;

    // ... unsafe ioctl 구현 ...
}

#[cfg(not(feature = "direct-ipmi"))]
mod subprocess {
    //! ipmitool 서브프로세스를 통한 IPMI (기본값, 완전 안전).
    // ... 현재 구현 내용 ...
}
}

핵심 통찰: unsafe 코드는 기능 플래그 뒤에 숨겨서 독립적으로 검증할 수 있게 하세요. CI에서 cargo +nightly miri test --features direct-ipmi를 실행하여 안전한 기본 빌드에 영향을 주지 않고 지속적으로 unsafe 경로를 검증하십시오.

cargo-careful — 안정 버전에서의 추가 UB 검사

cargo-careful은 표준 라이브러리의 추가 검사 기능을 활성화하여 코드를 실행합니다. 나이틀리나 Miri의 극심한 속도 저하 없이 일반 빌드에서 놓치는 일부 미정의 동작을 잡아낼 수 있습니다.

# 설치 (나이틀리가 필요하지만 실행 속도는 네이티브에 가까움)
cargo install cargo-careful

# 추가 UB 검사와 함께 테스트 실행 (초기화되지 않은 메모리, 잘못된 값 감지)
cargo +nightly careful test

# 추가 검사와 함께 바이너리 실행
cargo +nightly careful run -- --run-diagnostics

cargo-careful이 잡아내는 항목:

  • MaybeUninitzeroed()에서의 초기화되지 않은 메모리 읽기
  • transmute를 통한 잘못된 bool, char, 열거형 값 생성
  • 정렬되지 않은 포인터 읽기/쓰기
  • 겹치는 범위에서의 copy_nonoverlapping 실행

검증 단계에서의 위치:

낮은 오버헤드                                              철저한 검증
├─ cargo test ──► cargo careful test ──► Miri ──► ASan ──► Valgrind ─┤
│  (오버헤드 0)   (~1.5배 오버헤드)      (10~100배) (2배)    (10~50배)  │
│  Safe Rust만    일부 UB 감지            순수 Rust  FFI+Rust FFI+Rust  │

권장 사항: CI에 cargo +nightly careful test를 빠른 안전 검사 단계로 추가하세요. Miri와 달리 네이티브에 가까운 속도로 실행되면서 Safe Rust 추상화가 가리고 있는 실제 버그들을 찾아낼 수 있습니다.

Miri 및 새니타이저 문제 해결 (Troubleshooting)

증상원인해결책
Miri does not support FFIMiri는 Rust 인터프리터이며 C 코드를 실행할 수 없음FFI 코드에는 대신 Valgrind나 ASan을 사용하세요.
error: unsupported operation: can't call foreign functionMiri가 extern "C" 호출을 만남FFI 경계를 모킹(mock)하거나 #[cfg(miri)]로 보호하세요.
Stacked Borrows violation에일리어싱 규칙 위반 — 코드가 "작동"하더라도 발생함Miri가 옳습니다. &mut&가 겹치지 않게 리팩토링하세요.
새니타이저가 DEADLYSIGNAL 보고ASan이 버퍼 오버플로 감지배열 인덱싱, 슬라이스 작업, 포인터 연산을 점검하세요.
LeakSanitizer: detected memory leaksBox::leak(), forget() 또는 drop() 누락의도적이라면 __lsan_disable()로 억제하고, 아니라면 누수를 수정하세요.
Miri가 너무 느림컴파일이 아닌 해석 방식의 한계 (10~100배 느림)--lib 테스트만 실행하거나, 무거운 테스트에 #[cfg_attr(miri, ignore)]를 사용하세요.
원자적 연산에서 TSan 가양성 발생TSan이 Rust의 원자적 순서 모델을 완벽히 이해하지 못함특정 억제 규칙을 담은 tsan.supp 파일을 만들어 TSAN_OPTIONS로 지정하세요.

직접 해보기

  1. Miri UB 감지 유도: 동일한 i32 값에 대해 두 개의 &mut 참조를 만드는 unsafe 함수를 작성해 보세요(에일리어싱 위반). cargo +nightly miri test를 실행하고 "Stacked Borrows" 에러를 관찰해 보세요. 이를 UnsafeCell을 사용하거나 할당을 분리하여 수정해 보세요.

  2. ASan으로 버그 확인: 배열 범위를 벗어난 unsafe 접근을 시도하는 테스트를 만드세요. RUSTFLAGS="-Zsanitizer=address"로 빌드하고 ASan의 보고서를 확인해 보세요. 정확히 어떤 라인을 지목하는지 확인하세요.

  3. Miri 오버헤드 측정: 동일한 테스트 스위트에 대해 cargo test --libcargo +nightly miri test --lib의 실행 시간을 비교해 보세요. 감속 비율을 계산하고, 이를 바탕으로 CI에서 어떤 테스트를 Miri로 돌리고 어떤 테스트를 #[cfg_attr(miri, ignore)]로 건너뛸지 결정해 보세요.

안전성 검증 의사결정 트리

flowchart TD
    START["Unsafe 코드가 있는가?"] -->|아니요| SAFE["Safe Rust — 별도\n검증 도구 불필요"]
    START -->|예| KIND{"어떤 종류인가?"}
    
    KIND -->|"순수 Rust unsafe"| MIRI["Miri\nMIR 인터프리터\n에일리어싱, UB, 누수 감지"]
    KIND -->|"FFI / C 연동"| VALGRIND["Valgrind memcheck\n또는 ASan"]
    KIND -->|"동시성 unsafe"| CONC{"락 프리인가?"}
    
    CONC -->|"원자적/락 프리"| LOOM["loom\n원자적 연산 모델 체커"]
    CONC -->|"Mutex/공유 상태"| TSAN["TSan 또는\nMiri -Zmiri-check-number-validity"]
    
    MIRI --> CI_MIRI["CI: cargo +nightly miri test"]
    VALGRIND --> CI_VALGRIND["CI: valgrind --leak-check=full"]
    
    style SAFE fill:#91e5a3,color:#000
    style MIRI fill:#e3f2fd,color:#000
    style VALGRIND fill:#ffd43b,color:#000
    style LOOM fill:#ff6b6b,color:#000
    style TSAN fill:#ffd43b,color:#000

🏋️ 실습

🟡 실습 1: Miri UB 감지 유도

동일한 i32 값에 대해 두 개의 &mut 참조를 만드는 unsafe 함수를 작성하세요(에일리어싱 위반). cargo +nightly miri test를 실행하고 Stacked Borrows 에러를 확인한 후 수정하세요.

솔루션
#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    #[test]
    fn aliasing_ub() {
        let mut x: i32 = 42;
        let ptr = &mut x as *mut i32;
        unsafe {
            // 버그: 동일한 위치에 두 개의 &mut 참조 생성
            let _a = &mut *ptr;
            let _b = &mut *ptr; // Miri: Stacked Borrows 위반 감지!
        }
    }
}
}

수정 방법: 별도의 할당을 사용하거나 UnsafeCell을 활용합니다.

#![allow(unused)]
fn main() {
use std::cell::UnsafeCell;

#[test]
fn no_aliasing_ub() {
    let x = UnsafeCell::new(42);
    unsafe {
        let a = &mut *x.get();
        *a = 100;
    }
}
}

🔴 실습 2: ASan 범위 초과 감지

unsafe를 사용하여 배열 범위를 벗어나는 접근을 시도하는 테스트를 작성하세요. 나이틀리에서 RUSTFLAGS="-Zsanitizer=address"로 빌드하고 ASan의 보고서를 관찰하세요.

솔루션
#![allow(unused)]
fn main() {
#[test]
fn oob_access() {
    let arr = [1u8, 2, 3, 4, 5];
    let ptr = arr.as_ptr();
    unsafe {
        let _val = *ptr.add(10); // 범위를 벗어남!
    }
}
}
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std \
  --target x86_64-unknown-linux-gnu -- oob_access
# ASan 보고서: <정확한 주소>에서 stack-buffer-overflow 발생 지적

핵심 요약

  • Miri는 순수 Rust unsafe를 위한 필수 도구입니다. 에일리어싱 위반, 해제 후 사용, 그리고 테스트를 통과하더라도 숨어있는 누수를 잡아냅니다.
  • Valgrind는 FFI/C 연동을 위한 도구입니다. 재컴파일 없이 최종 바이너리에서 작동합니다.
  • 새니타이저(ASan, TSan, MSan)는 나이틀리가 필요하지만 네이티브에 가까운 속도로 실행되므로 대규모 테스트 스위트에 적합합니다.
  • **loom**은 락 프리 동시성 데이터 구조의 원자적 연산 순서를 검증하기 위해 특화된 도구입니다.
  • CI에서 매 푸시마다 Miri를 실행하고, 메인 파이프라인의 속도 저하를 막기 위해 새니타이저는 야간 빌드(nightly schedule) 등에 배치하세요.

의존성 관리 및 공급망 보안 🟢

학습 내용:

  • cargo-audit을 이용한 알려진 취약점 스캔
  • cargo-deny를 이용한 라이선스, 어드바이저리, 소스 정책 강제
  • Mozilla의 cargo-vet을 이용한 공급망 신뢰 검증
  • 오래된 의존성 추적 및 API의 파괴적 변경(breaking changes) 감지
  • 의존성 그래프 시각화 및 중복 버전 제거

참조: 릴리스 프로필 — 여기서 찾은 미사용 의존성을 cargo-udeps로 제거합니다 · CI/CD 파이프라인 — 파이프라인 내의 audit 및 deny 작업 · 빌드 스크립트build-dependencies 또한 공급망의 일부입니다.

Rust 바이너리는 여러분이 작성한 코드만 포함하는 것이 아니라, Cargo.lock에 명시된 모든 전이 의존성(transitive dependency)을 포함합니다. 이 의존성 트리 어디에라도 취약점, 라이선스 위반 또는 악의적인 크레이트가 있다면 그것은 곧 여러분의 문제가 됩니다. 이 장에서는 의존성 관리를 자동화하고 감사 가능하게 만드는 도구와 기술을 다룹니다.

cargo-audit — 알려진 취약점 스캔

cargo-audit은 공개된 크레이트의 취약점을 추적하는 RustSec Advisory Database를 바탕으로 여러분의 Cargo.lock을 검사합니다.

# 설치
cargo install cargo-audit

# 알려진 취약점 스캔
cargo audit

# 출력 예시:
# Crate:     chrono
# Version:   0.4.19
# Title:     Potential segfault in localtime_r invocations
# Date:      2020-11-10
# ID:        RUSTSEC-2020-0159
# URL:       https://rustsec.org/advisories/RUSTSEC-2020-0159
# Solution:  Upgrade to >= 0.4.20

# 취약점이 발견되면 CI를 실패하게 설정
cargo audit --deny warnings

# 자동화 처리를 위해 JSON 형식으로 출력
cargo audit --json

# Cargo.lock 업데이트를 통해 취약점 수정
cargo audit fix

CI 통합:

# .github/workflows/audit.yml
name: Security Audit
on:
  schedule:
    - cron: '0 0 * * *'  # 매일 확인 — 새로운 취약점 정보는 수시로 업데이트됨
  push:
    paths: ['Cargo.lock']

jobs:
  audit:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: rustsec/audit-check@v2
        with:
          token: ${{ secrets.GITHUB_TOKEN }}

cargo-deny — 종합 정책 강제

cargo-deny는 단순한 취약점 스캔을 넘어 다음 네 가지 차원에서 정책을 강제합니다:

  1. Advisories (어드바이저리) — 알려진 취약점 (cargo-audit과 유사)
  2. Licenses (라이선스) — 허용되거나 금지된 라이선스 목록 관리
  3. Bans (금지) — 특정 크레이트 사용 금지 또는 중복 버전 방지
  4. Sources (소스) — 허용된 레지스트리 및 git 소스 관리
# 설치
cargo install cargo-deny

# 설정 초기화
cargo deny init
# 기본값이 설명된 deny.toml 파일이 생성됨

# 모든 검사 실행
cargo deny check

# 특정 항목만 검사
cargo deny check advisories
cargo deny check licenses
cargo deny check bans
cargo deny check sources

deny.toml 설정 예시:

# deny.toml

[advisories]
vulnerability = "deny"        # 알려진 취약점 발견 시 실패 처리
unmaintained = "warn"         # 유지보수되지 않는 크레이트 경고
yanked = "deny"               # 배포 취소(yanked)된 크레이트 실패 처리
notice = "warn"               # 정보성 어드바이저리 경고

[licenses]
unlicensed = "deny"           # 라이선스가 없는 크레이트 불허
allow = [
    "MIT",
    "Apache-2.0",
    "BSD-2-Clause",
    "BSD-3-Clause",
    "ISC",
    "Unicode-DFS-2016",
]
copyleft = "deny"             # 이 프로젝트에서는 GPL/LGPL/AGPL 불허
default = "deny"              # 명시적으로 허용되지 않은 모든 라이선스 불허

[bans]
multiple-versions = "warn"    # 동일 크레이트가 두 버전 이상 존재할 경우 경고
wildcards = "deny"            # 의존성에 path = "*" 사용 불허
highlight = "all"             # 첫 번째가 아닌 모든 중복 항목 표시

# 특정 문제 크레이트 금지
deny = [
    # openssl-sys는 C 기반 OpenSSL을 끌어옴 — rustls 권장
    { name = "openssl-sys", wrappers = ["native-tls"] },
]

# 피할 수 없는 특정 중복 버전 허용
[[bans.skip]]
name = "syn"
version = "1.0"               # syn 1.x와 2.x는 흔히 공존함

[sources]
unknown-registry = "deny"     # crates.io만 허용
unknown-git = "deny"          # 임의의 git 의존성 불허
allow-registry = ["https://github.com/rust-lang/crates.io-index"]

라이선스 강제 기능은 상업용 프로젝트에서 특히 가치가 높습니다:

# 의존성 트리에 포함된 라이선스 목록 확인
cargo deny list

# 출력 결과 예시:
# MIT          — 127 crates
# Apache-2.0   — 89 crates
# BSD-3-Clause — 12 crates
# MPL-2.0      — 3 crates   ← 법무 검토가 필요할 수 있음
# Unicode-DFS  — 1 crate

cargo-vet — 공급망 신뢰 검증

Mozilla에서 개발한 cargo-vet은 "이 크레이트에 알려진 버그가 있는가?"가 아니라 **"신뢰할 수 있는 사람이 이 코드를 실제로 검토했는가?"**라는 다른 관점의 질문을 다룹니다.

# 설치
cargo install cargo-vet

# 초기화 (supply-chain/ 디렉토리 생성)
cargo vet init

# 검토가 필요한 크레이트 확인
cargo vet

# 크레이트 검토 후 인증(certify) 처리:
cargo vet certify serde 1.0.203
# serde 1.0.203 버전을 우리 팀의 기준에 따라 감사했음을 기록함

# 신뢰할 수 있는 기관의 감사 결과 가져오기
cargo vet import mozilla
cargo vet import google
cargo vet import bytecode-alliance

작동 구조:

supply-chain/
├── audits.toml       ← 우리 팀의 감사 인증 기록
├── config.toml       ← 신뢰 설정 및 기준
└── imports.lock      ← 타 기관에서 가져온 인증 데이터

cargo-vet은 정부, 금융, 인프라 등 공급망 요구사항이 매우 엄격한 조직에 가장 유용합니다. 대부분의 팀에게는 cargo-deny만으로도 충분한 보호가 가능합니다.

cargo-outdated 및 cargo-semver-checks

cargo-outdated — 더 최신 버전이 있는 의존성 찾기:

cargo install cargo-outdated

cargo outdated --workspace
# 출력 결과:
# Name        Project  Compat  Latest   Kind
# serde       1.0.193  1.0.203 1.0.203  Normal
# regex       1.9.6    1.10.4  1.10.4   Normal
# thiserror   1.0.50   1.0.61  2.0.3    Normal  ← 메이저 버전 업데이트 가능

cargo-semver-checks — 배포 전 API의 파괴적 변경 감지. 라이브러리 크레이트 제작 시 필수 도구입니다:

cargo install cargo-semver-checks

# 변경 사항이 유의적 버전(semver)을 준수하는지 확인
cargo semver-checks

# 출력 결과 예시:
# ✗ Function `parse_gpu_csv` is now private (was public)
#   → 이것은 파괴적 변경(BREAKING change)입니다. MAJOR 버전을 올리세요.
#
# ✗ Struct `GpuInfo` has a new required field `power_limit_w`
#   → 이것은 파괴적 변경(BREAKING change)입니다. MAJOR 버전을 올리세요.
#
# ✓ Function `parse_gpu_csv_v2` was added (non-breaking)

cargo-tree — 의존성 시각화 및 중복 제거

cargo tree는 Cargo에 내장된 도구(별도 설치 불필요)로, 의존성 그래프를 파악하는 데 매우 유용합니다.

# 전체 의존성 트리 표시
cargo tree

# 특정 크레이트가 포함된 이유 찾기
cargo tree --invert --package openssl-sys
# 우리 크레이트에서 openssl-sys에 이르는 모든 경로 표시

# 중복 버전 찾기
cargo tree --duplicates
# 출력 예시:
# syn v1.0.109
# └── serde_derive v1.0.193
#
# syn v2.0.48
# ├── thiserror-impl v1.0.56
# └── tokio-macros v2.2.0

# 직접적인 의존성만 표시
cargo tree --depth 1

# 의존성 기능(features) 표시
cargo tree --format "{p} {f}"

# 전체 의존성 개수 확인
cargo tree | wc -l

중복 제거 전략: cargo tree --duplicates가 동일한 크레이트의 두 가지 메이저 버전을 보여준다면, 의존성 체인을 업데이트하여 하나로 합칠 수 있는지 확인하세요. 모든 중복 항목은 컴파일 시간과 바이너리 크기를 증가시킵니다.

적용 사례: 멀티 크레이트 의존성 관리

이 워크스페이스는 버전 관리를 중앙에서 수행하기 위해 [workspace.dependencies]를 사용하고 있으며, 이는 매우 훌륭한 관행입니다. 크기 분석을 위한 cargo tree --duplicates와 결합하면 버전 파편화를 방지하고 바이너리 비대화를 줄일 수 있습니다.

# 루트 Cargo.toml — 모든 버전을 한 곳에서 고정 관리
[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", features = ["preserve_order"] }
regex = "1.10"
thiserror = "1.0"
anyhow = "1.0"
rayon = "1.8"

프로젝트 권장 추가 사항:

# CI 파이프라인에 추가:
cargo deny init              # 최초 1회 설정
cargo deny check             # 모든 PR 시 실행 — 라이선스, 어드바이저리, 금지 항목 검사
cargo audit --deny warnings  # 모든 푸시 시 실행 — 취약점 스캔
cargo outdated --workspace   # 매주 실행 — 업데이트 가능한 항목 추적

프로젝트 권장 deny.toml 설정:

[advisories]
vulnerability = "deny"
yanked = "deny"

[licenses]
allow = ["MIT", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "ISC", "Unicode-DFS-2016"]
copyleft = "deny"     # 하드웨어 진단 도구 — copyleft 라이선스 불허

[bans]
multiple-versions = "warn"   # 중복 추적은 하되, 아직 차단은 하지 않음
wildcards = "deny"

[sources]
unknown-registry = "deny"
unknown-git = "deny"

공급망 감사 파이프라인

flowchart LR
    PR["Pull Request"] --> AUDIT["cargo audit\n알려진 CVE 스캔"]
    AUDIT --> DENY["cargo deny check\n라이선스 + 금지 + 소스 검사"]
    DENY --> OUTDATED["cargo outdated\n주간 스케줄 실행"]
    OUTDATED --> SEMVER["cargo semver-checks\n라이브러리 크레이트 전용"]
    
    AUDIT -->|"실패 시"| BLOCK["❌ 머지 차단"]
    DENY -->|"실패 시"| BLOCK
    SEMVER -->|"파괴적 변경"| BUMP["메이저 버전 상향"]
    
    style BLOCK fill:#ff6b6b,color:#000
    style BUMP fill:#ffd43b,color:#000
    style PR fill:#e3f2fd,color:#000

🏋️ 실습

🟢 실습 1: 의존성 감사해보기

아무 Rust 프로젝트에서 cargo auditcargo deny init && cargo deny check를 실행해 보세요. 몇 개의 어드바이저리가 발견되나요? 의존성 트리에 몇 개의 라이선스 카테고리가 있나요?

솔루션
cargo audit
# 어드바이저리 확인 — 주로 chrono, time 또는 오래된 크레이트에서 발견됨

cargo deny init
cargo deny list
# 라이선스 분포 확인: MIT (N개), Apache-2.0 (N개) 등

cargo deny check
# 네 가지 모든 차원에 대한 전체 감사 결과 확인

🟡 실습 2: 중복 의존성 찾아 제거하기

워크스페이스에서 cargo tree --duplicates를 실행하세요. 두 가지 버전으로 존재하는 크레이트를 찾아보세요. Cargo.toml을 업데이트하여 버전을 하나로 합칠 수 있나요? 컴파일 시간과 바이너리 크기에 어떤 변화가 있는지 측정해 보세요.

솔루션
cargo tree --duplicates
# 흔한 예: syn 1.x와 syn 2.x

# 이전 버전을 사용하는 크레이트 찾기:
cargo tree --invert --package syn@1.0.109
# 출력 결과: serde_derive 1.0.xxx -> syn 1.0.109

# 최신 버전의 serde_derive가 syn 2.x를 사용하는지 확인:
cargo update -p serde_derive
cargo tree --duplicates
# syn 1.x가 사라졌다면 중복 버전 제거에 성공한 것입니다.

# 영향도 측정:
time cargo build --release  # 전후 비교
cargo bloat --release --crates | head -20

핵심 요약

  • cargo audit은 알려진 CVE를 잡아냅니다. 모든 푸시 시점과 매일 정기적으로 실행하세요.
  • cargo deny는 어드바이저리, 라이선스, 금지 항목, 소스의 네 가지 정책을 강제합니다.
  • 멀티 크레이트 워크스페이스에서는 [workspace.dependencies]를 사용하여 버전을 중앙 집중식으로 관리하세요.
  • cargo tree --duplicates는 불필요하게 늘어난 항목을 보여줍니다. 모든 중복은 컴파일 시간과 바이너리 크기를 늘립니다.
  • cargo-vet은 고도의 보안이 필요한 환경을 위한 것이며, 일반적인 팀에게는 cargo-deny로도 충분합니다.

릴리스 프로필 및 바이너리 크기 🟡

학습 내용:

  • 릴리스 프로필 분석: LTO, codegen-units, panic 전략, strip, opt-level
  • Thin vs Fat vs 교차 언어 LTO 트레이드오프
  • cargo-bloat을 이용한 바이너리 크기 분석
  • cargo-udeps, cargo-machete, cargo-shear를 이용한 의존성 정리

교차 참조: 컴파일 타임 도구 — 최적화의 나머지 절반 · 벤치마킹 — 최적화 전 실행 시간 측정 · 의존성 — 의존성 정리는 크기와 컴파일 시간을 모두 줄임

기본적인 cargo build --release 설정도 이미 훌륭합니다. 하지만 운영 환경 배포 — 특히 수천 대의 서버에 배포되는 단일 바이너리 도구의 경우 — "좋음"과 "최적화됨" 사이에는 상당한 차이가 있습니다. 이 장에서는 프로필의 설정값들과 바이너리 크기를 측정하는 도구들을 다룹니다.

릴리스 프로필 분석

Cargo 프로필은 rustc가 코드를 컴파일하는 방식을 제어합니다. 기본값은 보수적으로 설정되어 있어, 최대 성능보다는 광범위한 호환성을 위해 설계되었습니다.

# Cargo.toml — Cargo의 내장 기본값 (아무것도 지정하지 않았을 때의 상태)

[profile.release]
opt-level = 3        # 최적화 수준 (0=없음, 1=기본, 2=좋음, 3=공격적)
lto = false          # 링크 타임 최적화(LTO) 꺼짐
codegen-units = 16   # 병렬 컴파일 유닛 (컴파일은 빠르지만 최적화 기회는 적음)
panic = "unwind"     # 패닉 시 스택 되감기 (바이너리는 커지지만 catch_unwind 작동 가능)
strip = "none"       # 모든 심볼 및 디버그 정보 유지
overflow-checks = false  # 릴리스 빌드에서 정수 오버플로 검사 안 함
debug = false        # 릴리스 빌드에서 디버그 정보 제외

운영 환경에 최적화된 프로필 (이 프로젝트에서 이미 사용 중인 설정):

[profile.release]
lto = true           # 전체 크레이트 간 최적화 활성화
codegen-units = 1    # 단일 코드 생성 유닛 — 최적화 기회 최대화
panic = "abort"      # 되감기 오버헤드 제거 — 더 작고 빠름
strip = true         # 모든 심볼 제거 — 바이너리 크기 감소

각 설정의 영향:

설정기본값 → 최적화바이너리 크기실행 속도컴파일 시간
lto = false → true-10 ~ -20%+5 ~ +20%2-5배 느림
codegen-units = 16 → 1-5 ~ -10%+5 ~ +10%1.5-2배 느림
panic = "unwind" → "abort"-5 ~ -10%무시할 수 있음무시할 수 있음
strip = "none" → true-50 ~ -70%영향 없음영향 없음
opt-level = 3 → "s"-10 ~ -30%-5 ~ -10%비슷함
opt-level = 3 → "z"-15 ~ -40%-10 ~ -20%비슷함

추가적인 프로필 미세 조정:

[profile.release]
# 위의 모든 설정에 더해:
overflow-checks = true    # 릴리스에서도 오버플로 검사 유지 (속도보다 안전)
debug = "line-tables-only" # 전체 DWARF 없이 백트레이스를 위한 최소한의 디버그 정보
rpath = false             # 런타임 라이브러리 경로를 포함하지 않음
incremental = false       # 증분 컴파일 비활성화 (더 깨끗한 빌드)

# 크기 최적화 빌드용 (임베디드, WASM):
# opt-level = "z"         # 공격적인 크기 최적화
# strip = "symbols"       # 심볼은 제거하되 디버그 섹션은 유지

크레이트별 프로필 오버라이드 — 중요한 크레이트만 최적화하고 나머지는 그대로 두기:

# 개발 빌드: 의존성은 최적화하되 내 코드는 빠르게 리컴파일
[profile.dev.package."*"]
opt-level = 2          # 개발 모드에서도 모든 의존성 최적화

# 릴리스 빌드: 특정 크레이트의 최적화 설정 변경
[profile.release.package.serde_json]
opt-level = 3          # JSON 파싱 최적화 최대화
codegen-units = 1

# 테스트 프로필: 정확한 통합 테스트를 위해 릴리스 동작과 일치시킴
[profile.test]
opt-level = 1          # 느린 테스트에서 타임아웃을 방지하기 위한 최소한의 최적화

LTO 심층 분석 — Thin vs Fat vs 교차 언어

링크 타임 최적화(LTO)를 사용하면 LLVM이 크레이트 경계를 넘어 최적화를 수행할 수 있습니다. 예를 들어 serde_json의 함수를 파싱 코드에 인라이닝하거나, regex에서 사용되지 않는 코드를 제거하는 등의 작업이 가능합니다. LTO가 없으면 각 크레이트는 독립된 최적화 섬과 같습니다.

[profile.release]
# 옵션 1: Fat LTO (lto = true일 때의 기본값)
lto = true
# 모든 코드를 하나의 LLVM 모듈로 병합 → 최적화 극대화
# 컴파일은 가장 느리지만, 가장 작고 빠른 바이너리 생성

# 옵션 2: Thin LTO
lto = "thin"
# 크레이트는 분리된 상태를 유지하되 LLVM이 모듈 간 최적화 수행
# Fat LTO보다 컴파일이 빠르며 최적화 효과도 거의 비슷함
# 대부분의 프로젝트에 가장 권장되는 트레이드오프

# 옵션 3: LTO 없음
lto = false
# 크레이트 내부에서만 최적화 수행
# 컴파일이 가장 빠르지만 바이너리가 커짐

# 옵션 4: Off (명시적)
lto = "off"
# false와 동일함

Fat LTO vs Thin LTO:

요인Fat LTO (true)Thin LTO ("thin")
최적화 품질최고Fat의 약 95% 수준
컴파일 시간느림 (모든 코드가 한 모듈에 있음)보통 (병렬 모듈 처리)
메모리 사용량높음 (모든 LLVM IR을 메모리에 로드)낮음 (스트리밍 방식)
병렬성없음 (단일 모듈)좋음 (모듈별 처리)
권장 용도최종 릴리스 빌드CI 빌드, 개발 단계

교차 언어 LTO — Rust와 C 경계를 넘나드는 최적화:

[profile.release]
lto = true

# cc 크레이트를 사용하는 크레이트의 Cargo.toml
[build-dependencies]
cc = "1.0"
// build.rs — 교차 언어(linker-plugin) LTO 활성화
fn main() {
    // cc 크레이트는 환경 변수의 CFLAGS를 따릅니다.
    // 교차 언어 LTO를 위해 C 코드를 다음 설정으로 컴파일합니다:
    //   -flto=thin -O2
    cc::Build::new()
        .file("csrc/fast_parser.c")
        .flag("-flto=thin")
        .opt_level(2)
        .compile("fast_parser");
}
# linker-plugin LTO 활성화 (호환 가능한 LLD 또는 gold 링커 필요)
RUSTFLAGS="-Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld" \
    cargo build --release

교차 언어 LTO를 통해 LLVM은 C 함수를 Rust 호출부로 인라이닝하거나 그 반대의 작업을 수행할 수 있습니다. 이는 작은 C 함수가 빈번하게 호출되는 FFI 비중이 높은 코드(예: IPMI ioctl 래퍼)에서 가장 큰 효과를 발휘합니다.

cargo-bloat을 이용한 바이너리 크기 분석

cargo-bloat은 다음 질문에 답해줍니다: "내 바이너리에서 어떤 함수와 크레이트가 가장 많은 공간을 차지하고 있는가?"

# 설치
cargo install cargo-bloat

# 가장 큰 함수들 표시
cargo bloat --release -n 20
# 출력 예시:
#  File  .text     Size          Crate    Name
#  2.8%   5.1%  78.5KiB  serde_json       serde_json::de::Deserializer::parse_...
#  2.1%   3.8%  58.2KiB  regex_syntax     regex_syntax::ast::parse::ParserI::p...
#  1.5%   2.7%  42.1KiB  accel_diag         accel_diag::vendor::parse_smi_output
#  ...

# 크레이트별 표시 (어떤 의존성이 가장 큰지 확인)
cargo bloat --release --crates
# 출력 예시:
#  File  .text     Size Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex
#  6.2%  11.1%  170KiB std
#  5.1%   9.2%  141KiB accel_diag
#  ...

# 두 빌드 결과 비교 (최적화 전후)
cargo bloat --release --crates > before.txt
# ... 변경 작업 수행 ...
cargo bloat --release --crates > after.txt
diff before.txt after.txt

흔한 비대화 원인 및 해결책:

원인일반적인 크기해결책
regex (전체 엔진)200-400 KB유니코드가 필요 없다면 regex-lite 사용
serde_json (전체)200-350 KB성능이 중요하다면 simd-json 또는 sonic-rs 고려
제네릭 단일화 (Monomorphization)다양함API 경계에서 dyn Trait 사용
포맷팅 엔진 (Display, Debug)50-150 KB거대한 enum의 #[derive(Debug)]는 크기를 키움
패닉 메시지 문자열20-80 KBpanic = "abort"는 되감기를 제거하고, strip은 문자열 제거
미사용 기능(Feature)다양함기본 기능 비활성화: serde = { version = "1", default-features = false }

cargo-udeps를 이용한 의존성 정리

cargo-udepsCargo.toml에는 선언되어 있지만 실제 코드에서는 사용되지 않는 의존성을 찾아줍니다.

# 설치 (nightly 채널 필요)
cargo install cargo-udeps

# 사용되지 않는 의존성 찾기
cargo +nightly udeps --workspace
# 출력 예시:
# unused dependencies:
# `diag_tool v0.1.0`
# └── "tempfile" (dev-dependency)
#
# `accel_diag v0.1.0`
# └── "once_cell"    ← LazyLock 도입 전에는 필요했으나 지금은 사용 안 함

사용되지 않는 의존성은 다음과 같은 문제를 일으킵니다:

  • 컴파일 시간 증가
  • 바이너리 크기 증가
  • 공급망 보안 위험 증가
  • 잠재적인 라이선스 복잡성 추가

대안: cargo-machete — 휴리스틱 기반의 빠른 방식:

cargo install cargo-machete
cargo machete
# 빠르지만 휴리스틱 방식이라 오탐(false positive)이 있을 수 있음

대안: cargo-shearcargo-udepscargo-machete 사이의 절충안:

cargo install cargo-shear
cargo shear --fix
# cargo-machete보다 느리지만 cargo-udeps보다는 훨씬 빠름
# cargo-machete보다 오탐이 훨씬 적음

크기 최적화 의사결정 트리

flowchart TD
    START["바이너리가 너무 큰가?"] --> STRIP{"strip = true 설정됨?"}
    STRIP -->|"아니요"| DO_STRIP["strip = true 추가\n크기 -50 ~ -70%"]
    STRIP -->|"예"| LTO{"LTO 활성화됨?"}
    LTO -->|"아니요"| DO_LTO["lto = true 및\ncodegen-units = 1 추가"]
    LTO -->|"예"| BLOAT["cargo-bloat --crates 실행"]
    BLOAT --> BIG_DEP{"거대한 의존성이 있는가?"}
    BIG_DEP -->|"예"| REPLACE["더 가벼운 대안으로 교체하거나\n기본 기능(feature) 비활성화"]
    BIG_DEP -->|"아니요"| UDEPS["cargo-udeps 실행\n미사용 의존성 제거"]
    UDEPS --> OPT_LEVEL{"더 줄여야 하는가?"}
    OPT_LEVEL -->|"예"| SIZE_OPT["opt-level = 's' 또는 'z' 설정"]

    style DO_STRIP fill:#91e5a3,color:#000
    style DO_LTO fill:#e3f2fd,color:#000
    style REPLACE fill:#ffd43b,color:#000
    style SIZE_OPT fill:#ff6b6b,color:#000

🏋️ 실습

🟢 실습 1: LTO 영향력 측정

기본 릴리스 설정으로 프로젝트를 빌드한 후, lto = true + codegen-units = 1 + strip = true 설정을 적용하여 다시 빌드해 보세요. 바이너리 크기와 컴파일 시간을 비교해 봅니다.

솔루션
# 기본 릴리스 빌드
cargo build --release
ls -lh target/release/my-binary
time cargo build --release  # 시간 기록

# 최적화된 릴리스 — Cargo.toml에 추가:
# [profile.release]
# lto = true
# codegen-units = 1
# strip = true
# panic = "abort"

cargo clean
cargo build --release
ls -lh target/release/my-binary  # 보통 30-50% 작아짐
time cargo build --release       # 보통 컴파일이 2-3배 느려짐

🟡 실습 2: 가장 큰 크레이트 찾기

프로젝트에서 cargo bloat --release --crates를 실행해 보세요. 가장 큰 의존성을 확인합니다. 기본 기능을 비활성화하거나 더 가벼운 대안으로 교체하여 크기를 줄일 수 있을까요?

솔루션
cargo install cargo-bloat
cargo bloat --release --crates
# 출력 예시:
#  File  .text     Size Crate
# 12.3%  22.1%  340KiB serde_json
#  8.7%  15.6%  240KiB regex

# regex의 경우 — 유니코드가 필요 없다면 regex-lite 시도:
# regex-lite = "0.1"  # 전체 regex보다 약 10배 작음

# serde의 경우 — std가 필요 없다면 기본 기능 비활성화:
# serde = { version = "1", default-features = false, features = ["derive"] }

cargo bloat --release --crates  # 변경 후 결과 비교

핵심 요약

  • lto = true + codegen-units = 1 + strip = true + panic = "abort"는 운영 환경용 릴리스 프로필의 표준입니다.
  • Thin LTO (lto = "thin")는 Fat LTO의 장점의 80%를 제공하면서도 컴파일 비용은 훨씬 적습니다.
  • cargo-bloat --crates는 어떤 의존성이 바이너리 공간을 차지하는지 정확히 알려줍니다.
  • cargo-udeps, cargo-machete, cargo-shear는 컴파일 시간과 바이너리 크기를 낭비하는 죽은 의존성을 찾아줍니다.
  • 크레이트별 프로필 오버라이드를 통해 전체 빌드를 느리게 하지 않고도 중요한 크레이트만 최적화할 수 있습니다.

컴파일 시간 및 개발자 도구 🟡

학습 내용:

  • 로컬 및 CI 빌드를 위한 sccache 컴파일 캐싱
  • 기본 링커보다 3~10배 빠른 mold를 이용한 고속 링크
  • cargo-nextest: 더 빠르고 정보가 풍부한 테스트 러너
  • 개발자 가독성 도구: cargo-expand, cargo-geiger, cargo-watch
  • 워크스페이스 린트, MSRV 정책, 문서화를 통한 CI 검증

교차 참조: 릴리스 프로필 — LTO 및 바이너리 크기 최적화 · CI/CD 파이프라인 — 이 도구들을 파이프라인에 통합하는 방법 · 의존성 — 의존성 감소가 컴파일 속도 향상의 지름길

컴파일 시간 최적화: sccache, mold, cargo-nextest

긴 컴파일 시간은 Rust 개발자들이 겪는 가장 큰 고충입니다. 아래 도구들을 조합하면 반복적인 빌드 시간을 50~80%까지 단축할 수 있습니다.

sccache — 공유 컴파일 캐시:

# 설치
cargo install sccache

# Rust 래퍼로 설정
export RUSTC_WRAPPER=sccache

# 또는 .cargo/config.toml에 영구적으로 설정:
# [build]
# rustc-wrapper = "sccache"

# 첫 빌드: 정상 속도 (캐시 생성 중)
cargo build --release  # 3분 소요

# Clean 후 재빌드: 변경되지 않은 크레이트는 캐시 적중(hit)
cargo clean && cargo build --release  # 45초 소요

# 캐시 통계 확인
sccache --show-stats
# Compile requests        1,234
# Cache hits               987 (80%)
# Cache misses             247

sccache는 팀 전체 및 CI에서의 캐시 공유를 위해 클라우드 스토리지(S3, GCS, Azure Blob)를 지원합니다.

mold — 고속 링커:

링크 단계는 흔히 가장 느린 구간입니다. moldlld보다 3~5배, 기본 GNU ld보다는 10~20배 빠릅니다.

# 설치
sudo apt install mold  # Ubuntu 22.04+
# 참고: mold는 ELF 타겟(Linux)용입니다. macOS는 ELF가 아닌 Mach-O를 사용합니다.
# macOS 기본 링커(ld64)는 이미 상당히 빠릅니다. 더 빠른 속도가 필요하다면:
# brew install sold     # sold = Mach-O용 mold (실험적, 덜 성숙함)
# 실제 프로젝트에서 macOS의 링크 시간이 병목이 되는 경우는 드뭅니다.
# 링크에 mold 사용 설정
# .cargo/config.toml
[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
# 참고: https://github.com/rui314/mold/blob/main/docs/mold.md#environment-variables
export MOLD_JOBS=1

# mold가 사용되고 있는지 확인
cargo build -v 2>&1 | grep mold

cargo-nextest — 고속 테스트 러너:

# 설치
cargo install cargo-nextest

# 테스트 실행 (기본적으로 병렬 실행, 테스트별 타임아웃, 재시도 지원)
cargo nextest run

# cargo test 대비 주요 장점:
# - 각 테스트가 별도의 프로세스에서 실행됨 → 격리성 우수
# - 스마트 스케줄링을 통한 병렬 실행
# - 테스트별 타임아웃 (CI 중단 방지)
# - CI용 JUnit XML 출력 지원
# - 실패한 테스트 재시도 가능

# 설정 예시
cargo nextest run --retries 2 --fail-fast

# 테스트 바이너리 아카이브 (CI에서 유용: 한 곳에서 빌드하고 여러 머신에서 테스트)
cargo nextest archive --archive-file tests.tar.zst
cargo nextest run --archive-file tests.tar.zst
# .config/nextest.toml
[profile.default]
retries = 0
slow-timeout = { period = "60s", terminate-after = 3 }
fail-fast = true

[profile.ci]
retries = 2
fail-fast = false
junit = { path = "test-results.xml" }

통합 개발 환경 설정:

# .cargo/config.toml — 개발 반복 주기 최적화
[build]
rustc-wrapper = "sccache"       # 컴파일 결과물 캐싱

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # 고속 링크

# 개발용 프로필: 의존성은 최적화하되 내 코드는 그대로
# (Cargo.toml에 작성)
# [profile.dev.package."*"]
# opt-level = 2

cargo-expand 및 cargo-geiger — 가시성 도구

cargo-expand — 매크로가 생성하는 코드 확인:

cargo install cargo-expand

# 특정 모듈의 모든 매크로 확장
cargo expand --lib accel_diag::vendor

# 특정 derive 확장 확인
# 예: #[derive(Debug, Serialize, Deserialize)]
# cargo expand는 생성된 impl 블록들을 보여줍니다.
cargo expand --lib --tests

#[derive] 매크로 출력, macro_rules! 확장, serde가 타입을 위해 생성하는 코드를 이해하고 디버깅하는 데 매우 유용합니다.

cargo-expand 외에도 rust-analyzer를 사용하여 매크로를 확장할 수 있습니다:

  1. 확인하려는 매크로 위로 커서를 옮깁니다.
  2. 커맨드 팔레트를 엽니다 (VSCode의 경우 F1).
  3. rust-analyzer: Expand macro recursively at caret을 검색하여 실행합니다.

cargo-geiger — 의존성 트리 전체에서 unsafe 사용량 집계:

cargo install cargo-geiger

cargo geiger
# 출력 예시:
# Metric output format: x/y
#   x = 빌드 시 사용된 unsafe 코드
#   y = 크레이트 내에서 발견된 전체 unsafe 코드
#
# Functions  Expressions  Impls  Traits  Methods
# 0/0        0/0          0/0    0/0     0/0      ✅ my_crate
# 0/5        0/23         0/2    0/0     0/3      ✅ serde
# 3/3        14/14        0/0    0/0     2/2      ❗ libc
# 15/15      142/142      4/4    0/0     12/12    ☢️ ring

# 기호 설명:
# ✅ = unsafe 사용 안 함
# ❗ = 일부 unsafe 사용됨
# ☢️ = unsafe 집중 사용됨

프로젝트의 zero-unsafe 정책을 위해, cargo geiger는 의존성이 제공하는 기능 중 실제 호출 그래프 상에서 unsafe 코드가 유입되지 않는지 검증합니다.

워크스페이스 린트 — [workspace.lints]

Rust 1.74부터 Clippy와 컴파일러 린트를 Cargo.toml에서 중앙 집중식으로 관리할 수 있습니다. 더 이상 모든 크레이트 상단에 #![deny(...)]를 적지 않아도 됩니다.

# 루트 Cargo.toml — 모든 크레이트에 대한 린트 설정
[workspace.lints.clippy]
unwrap_used = "warn"         # ? 또는 expect("사유") 선호
dbg_macro = "deny"           # 커밋되는 코드에 dbg!() 금지
todo = "warn"                # 미완성 구현 추적
large_enum_variant = "warn"  # 예기치 않은 크기 비대화 방지

[workspace.lints.rust]
unsafe_code = "deny"         # zero-unsafe 정책 강제
missing_docs = "warn"        # 문서화 장려
# 각 크레이트의 Cargo.toml — 워크스페이스 린트 적용
[lints]
workspace = true

이 방식은 여기저기 흩어져 있던 #![deny(clippy::unwrap_used)] 속성을 대체하며, 워크스페이스 전체에 일관된 정책을 보장합니다.

Clippy 경고 자동 수정:

# Clippy가 자동으로 수정 가능한 제안들을 적용하게 함
cargo clippy --fix --workspace --all-targets --allow-dirty

# 동작을 변경할 수 있는 제안까지 적용 (주의 깊게 검토 필요!)
cargo clippy --fix --workspace --all-targets --allow-dirty -- -W clippy::pedantic

: 커밋하기 전에 cargo clippy --fix를 실행하세요. 일일이 고치기 번거로운 사소한 문제들(미사용 import, 불필요한 clone, 타입 단순화 등)을 알아서 처리해 줍니다.

MSRV 정책과 rust-version

최소 지원 Rust 버전(MSRV)은 크레이트가 오래된 툴체인에서도 컴파일되도록 보장합니다. 이는 Rust 버전이 고정된 시스템에 배포할 때 중요합니다.

# Cargo.toml
[package]
name = "diag_tool"
version = "0.1.0"
rust-version = "1.75"    # 필요한 최소 Rust 버전
# MSRV 준수 여부 확인
cargo +1.75.0 check --workspace

# 자동 MSRV 탐색
cargo install cargo-msrv
cargo msrv find
# 출력: Minimum Supported Rust Version is 1.75.0

# CI에서 검증
cargo msrv verify

CI에서의 MSRV:

jobs:
  msrv:
    name: Check MSRV
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@master
        with:
          toolchain: "1.75.0"    # Cargo.toml의 rust-version과 일치시킴
      - run: cargo check --workspace

MSRV 전략:

  • 바이너리 애플리케이션 (이 프로젝트와 같은 경우): 최신 안정 버전을 사용합니다. 특별한 MSRV가 필요하지 않을 수 있습니다.
  • 라이브러리 크레이트 (crates.io 라이브러리): 사용 중인 모든 기능을 지원하는 가장 오래된 Rust 버전으로 설정합니다. 보통 N-2 (현재 버전보다 2단계 낮음) 방식을 많이 사용합니다.
  • 엔터프라이즈 배포: 보유한 서버들에 설치된 가장 오래된 Rust 버전에 맞춥니다.

적용 사례: 운영용 바이너리 프로필

이 프로젝트는 이미 훌륭한 릴리스 프로필을 갖추고 있습니다:

# 현재 워크스페이스 Cargo.toml
[profile.release]
lto = true           # ✅ 전체 크레이트 간 최적화
codegen-units = 1    # ✅ 최적화 기회 극대화
panic = "abort"      # ✅ 되감기 오버헤드 제거
strip = true         # ✅ 배포를 위한 심볼 제거

[profile.dev]
opt-level = 0        # ✅ 빠른 컴파일
debug = true         # ✅ 전체 디버그 정보 포함

추천 추가 설정:

# 개발 모드에서 의존성 최적화 (테스트 실행 속도 향상)
[profile.dev.package."*"]
opt-level = 2

# 테스트 프로필: 느린 테스트에서 타임아웃을 방지하기 위한 약간의 최적화
[profile.test]
opt-level = 1

# 릴리스 빌드에서 오버플로 검사 유지 (안전성)
[profile.release]
lto = true
codegen-units = 1
panic = "abort"
strip = true
overflow-checks = true    # ← 추가: 정수 오버플로 감지
debug = "line-tables-only" # ← 추가: 전체 DWARF 없이 백트레이스 지원

추천 개발자 도구 설정:

# .cargo/config.toml (제안)
[build]
rustc-wrapper = "sccache"  # 첫 빌드 이후 80% 이상의 캐시 적중률 기대

[target.x86_64-unknown-linux-gnu]
rustflags = ["-C", "link-arg=-fuse-ld=mold"]  # 3~5배 빠른 링크

프로젝트에 기대되는 효과:

지표현재제안 적용 후
릴리스 바이너리약 10 MB (stripped, LTO)동일
개발 빌드 시간약 45초약 25초 (sccache + mold)
재빌드 (파일 1개 변경)약 15초약 5초 (sccache + mold)
테스트 실행cargo testcargo nextest — 2배 빠름
의존성 취약점 스캔없음CI에서 cargo audit 실행
라이선스 준수수동 확인cargo deny 자동화
미사용 의존성 감지수동 확인CI에서 cargo udeps 실행

cargo-watch — 파일 변경 시 자동 재빌드

cargo-watch는 소스 파일이 변경될 때마다 명령어를 다시 실행하여 빠른 피드백 루프를 만들어 줍니다.

# 설치
cargo install cargo-watch

# 저장할 때마다 즉시 check 실행 (빠른 피드백)
cargo watch -x check

# 변경 시 clippy 및 테스트 실행
cargo watch -x 'clippy --workspace --all-targets' -x 'test --workspace --lib'

# 특정 크레이트만 감시 (대규모 워크스페이스에서 유리)
cargo watch -w accel_diag/src -x 'test -p accel_diag'

# 매 실행 사이 화면 지우기
cargo watch -c -x check

: 앞서 소개한 mold + sccache와 조합하면 증분 변경에 대해 1초 미만의 재확인 시간을 확보할 수 있습니다.

cargo doc 및 워크스페이스 문서화

대규모 워크스페이스에서 생성된 문서는 코드 탐색에 필수적입니다. cargo doc은 doc-comment와 타입 시그니처를 바탕으로 HTML 문서를 만듭니다.

# 워크스페이스의 모든 크레이트에 대한 문서 생성 (브라우저에서 열기)
cargo doc --workspace --no-deps --open

# 프라이빗 항목 포함 (개발 중 유용)
cargo doc --workspace --no-deps --document-private-items

# HTML 생성 없이 문서 링크만 확인 (빠른 CI 검사)
cargo doc --workspace --no-deps 2>&1 | grep -E 'warning|error'

문서 내부 링크 (Intra-doc links) — URL 없이 크레이트 간 타입을 연결:

#![allow(unused)]
fn main() {
/// [`GpuConfig`] 설정을 사용하여 GPU 진단을 실행합니다.
///
/// 구현 상세는 [`crate::accel_diag::run_diagnostics`]를 참조하세요.
/// [`DiagResult`]를 반환하며, 이는 [`DerReport`](crate::core_lib::DerReport)
/// 형식으로 직렬화될 수 있습니다.
pub fn run_accel_diag(config: &GpuConfig) -> DiagResult {
    // ...
}
}

문서에 플랫폼 전용 API 표시:

#![allow(unused)]
fn main() {
// Cargo.toml: [package.metadata.docs.rs]
// all-features = true
// rustdoc-args = ["--cfg", "docsrs"]

/// Windows 전용: Win32 API를 통해 배터리 상태를 읽어옵니다.
///
/// `cfg(windows)` 빌드에서만 사용 가능합니다.
#[cfg(windows)]
#[doc(cfg(windows))]  // 문서에 "Available on Windows only" 배지 표시
pub fn get_battery_status() -> Option<u8> {
    // ...
}
}

CI 문서 검사:

# CI 워크플로에 추가
- name: Check documentation
  run: RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps
  # 끊어진 문서 링크를 에러로 처리

프로젝트를 위해: 크레이트가 많으므로 cargo doc --workspace는 새 팀원이 API 구성을 파악하는 가장 좋은 방법입니다. CI에 RUSTDOCFLAGS="-D warnings"를 추가하여 머지 전에 끊어진 링크를 잡아내세요.

컴파일 시간 의사결정 트리

flowchart TD
    START["컴파일이 너무 느린가?"] --> WHERE{"어디서 시간이 걸리는가?"}

    WHERE -->|"변경되지 않은 크레이트의\n반복 컴파일"| SCCACHE["sccache\n공유 컴파일 캐시"]
    WHERE -->|"링크 단계"| MOLD["mold 링커\n3~10배 빠른 링크"]
    WHERE -->|"테스트 실행"| NEXTEST["cargo-nextest\n병렬 테스트 러너"]
    WHERE -->|"전반적인 과정"| COMBO["위의 모든 도구 + \ncargo-udeps 의존성 정리"]

    SCCACHE --> CI_CACHE{"CI 환경인가 로컬인가?"}
    CI_CACHE -->|"CI"| S3["S3/GCS 공유 캐시"]
    CI_CACHE -->|"로컬"| LOCAL["로컬 디스크 캐시\n자동 설정"]

    style SCCACHE fill:#91e5a3,color:#000
    style MOLD fill:#e3f2fd,color:#000
    style NEXTEST fill:#ffd43b,color:#000
    style COMBO fill:#b39ddb,color:#000

🏋️ 실습

🟢 실습 1: sccache + mold 설정하기

sccachemold를 설치하고 .cargo/config.toml에 설정한 뒤, 전체 재빌드 시 컴파일 시간이 얼마나 단축되는지 측정해 보세요.

솔루션
# 설치
cargo install sccache
sudo apt install mold  # Ubuntu 22.04+

# .cargo/config.toml 설정:
cat > .cargo/config.toml << 'EOF'
[build]
rustc-wrapper = "sccache"

[target.x86_64-unknown-linux-gnu]
linker = "clang"
rustflags = ["-C", "link-arg=-fuse-ld=mold"]
EOF

# 첫 빌드 (캐시 생성)
time cargo build --release  # 예: 180초

# Clean 후 재빌드 (캐시 활용)
cargo clean
time cargo build --release  # 예: 45초

sccache --show-stats
# 캐시 적중률이 60~80% 이상인지 확인

🟡 실습 2: cargo-nextest로 전환하기

cargo-nextest를 설치하고 테스트 스위트를 실행해 보세요. cargo test와 전체 실행 시간을 비교했을 때 얼마나 빨라졌나요?

솔루션
cargo install cargo-nextest

# 표준 테스트 러너
time cargo test --workspace 2>&1 | tail -5

# nextest (테스트 바이너리별 병렬 실행)
time cargo nextest run --workspace 2>&1 | tail -5

# 대규모 워크스페이스의 경우 보통 2~5배 빨라집니다.
# nextest는 다음 기능도 제공합니다:
# - 테스트별 실행 시간 표시
# - 불안정한(flaky) 테스트 재시도
# - CI용 JUnit XML 출력
cargo nextest run --workspace --retries 2

핵심 요약

  • S3/GCS 백엔드를 사용하는 sccache는 팀 전체와 CI 간에 컴파일 캐시를 공유합니다.
  • mold는 가장 빠른 ELF 링커로, 링크 시간을 초 단위에서 밀리초 단위로 줄여줍니다.
  • cargo-nextest는 바이너리별로 테스트를 병렬 실행하며, 향상된 출력 정보와 재시도를 지원합니다.
  • cargo-geigerunsafe 사용량을 집계합니다. 새로운 의존성을 추가하기 전에 실행해 보세요.
  • [workspace.lints]는 다중 크레이트 워크스페이스에서 Clippy와 rustc 린트 설정을 중앙 집중화합니다.

no_std 및 기능(Feature) 검증 🔴

학습 내용:

  • cargo-hack을 이용한 기능 조합의 체계적 검증
  • Rust의 세 가지 레이어: core vs alloc vs std 및 각각의 사용 시점
  • 커스텀 패닉 핸들러와 할당자를 포함한 no_std 크레이트 빌드
  • 호스트 및 QEMU에서 no_std 코드 테스트하기

교차 참조: Windows 및 조건부 컴파일 — 이 주제의 플랫폼 측면 · 교차 컴파일 — ARM 및 임베디드 타겟으로의 교차 빌드 설정 · Miri 및 새니타이저no_std 환경에서의 unsafe 코드 검증 · 빌드 스크립트build.rs에서 내보내는 cfg 플래그

Rust는 8비트 마이크로컨트롤러부터 클라우드 서버까지 어디서나 실행됩니다. 이 장에서는 그 기초가 되는 내용을 다룹니다: #![no_std]를 사용하여 표준 라이브러리를 제거하고, 설정한 기능(feature) 조합들이 실제로 올바르게 컴파일되는지 검증하는 방법입니다.

cargo-hack을 이용한 기능 조합 검증

cargo-hack은 모든 기능 조합을 체계적으로 테스트합니다. 이는 #[cfg(...)] 코드가 포함된 크레이트에 필수적입니다.

# 설치
cargo install cargo-hack

# 모든 기능이 개별적으로 컴파일되는지 확인
cargo hack check --each-feature --workspace

# 모든 기능 조합 테스트 (지수적으로 증가하므로 주의!)
# 기능이 8개 미만인 크레이트에만 현실적입니다.
cargo hack check --feature-powerset --workspace

# 현실적인 타협안: 각 기능을 개별적으로 테스트 + 모든 기능 활성화 + 기능 모두 비활성화
cargo hack check --each-feature --workspace --no-dev-deps
cargo check --workspace --all-features
cargo check --workspace --no-default-features

이 프로젝트에서 중요한 이유:

플랫폼 기능(linux, windows, direct-ipmi, direct-accel-api)을 추가할 때, cargo-hack은 컴파일을 깨뜨리는 조합을 잡아냅니다.

# 예: 플랫폼 코드를 제어하는 기능들
[features]
default = ["linux"]
linux = []                          # Linux 전용 하드웨어 접근
windows = ["dep:windows-sys"]       # Windows 전용 API
direct-ipmi = []                    # unsafe IPMI ioctl (5장)
direct-accel-api = []               # unsafe accel-mgmt FFI (5장)
# 모든 기능이 단독으로 그리고 함께 올바르게 컴파일되는지 확인
cargo hack check --each-feature -p diag_tool
# 발견 예시: "feature 'windows'가 'direct-ipmi' 없이 컴파일되지 않음"
# 발견 예시: "#[cfg(feature = \"linux\")]에 오타가 있음 — 'lnux'로 되어 있음"

CI 통합:

# CI 파이프라인에 추가 (단순 컴파일 확인이므로 빠름)
- name: Feature matrix check
  run: cargo hack check --each-feature --workspace --no-dev-deps

권장 사항: 기능이 2개 이상인 크레이트는 CI에서 cargo hack check --each-feature를 실행하세요. --feature-powerset은 기능이 8개 미만인 핵심 라이브러리 크레이트에만 사용하세요 ($2^n$ 조합이므로 지수적으로 늘어납니다).

no_std — 언제 그리고 왜 사용하는가

#![no_std]는 컴파일러에게 "표준 라이브러리를 링크하지 마라"고 지시합니다. 이 경우 크레이트는 core(그리고 선택적으로 alloc)만 사용할 수 있습니다. 왜 이런 작업이 필요할까요?

시나리오no_std를 사용하는 이유
임베디드 펌웨어 (ARM Cortex-M, RISC-V)OS 없음, 힙(heap) 없음, 파일 시스템 없음
UEFI 진단 도구OS API가 없는 부팅 전 환경
커널 모듈커널 공간에서는 유저 공간의 std를 사용할 수 없음
WebAssembly (WASM)바이너리 크기 최소화, OS 의존성 제거
부트로더OS가 존재하기 전에 실행되어야 함
C 인터페이스를 가진 공유 라이브러리호출자에게 Rust 런타임 유입 방지

하드웨어 진단 분야에서 no_std는 다음과 같은 결과물을 빌드할 때 필요합니다:

  • UEFI 기반 부팅 전 진단 도구 (OS 로드 전)
  • BMC 펌웨어 진단 (자원이 제한된 ARM SoC)
  • 커널 레벨 PCIe 진단 (커널 모듈 또는 eBPF 프로브)

core vs alloc vs std — 세 가지 레이어

┌─────────────────────────────────────────────────────────────┐
│ std                                                         │
│  core + alloc의 모든 기능을 포함하며, 추가로 다음을 제공:        │
│  • 파일 I/O (std::fs, std::io)                              │
│  • 네트워킹 (std::net)                                       │
│  • 스레드 (std::thread)                                     │
│  • 시간 (std::time)                                         │
│  • 환경 변수 (std::env)                                      │
│  • 프로세스 (std::process)                                   │
│  • OS 전용 (std::os::unix, std::os::windows)                │
├─────────────────────────────────────────────────────────────┤
│ alloc          (#! [no_std] + extern crate alloc로 사용 가능, │
│                 글로벌 할당자가 있는 경우)                     │
│  • String, Vec, Box, Rc, Arc                                │
│  • BTreeMap, BTreeSet                                       │
│  • format!() 매크로                                          │
│  • 힙 메모리가 필요한 컬렉션 및 스마트 포인터                  │
├─────────────────────────────────────────────────────────────┤
│ core           (#! [no_std]에서도 항상 사용 가능)              │
│  • 기본 타입 (u8, bool, char 등)                             │
│  • Option, Result                                           │
│  • Iterator, slice, array, str (String이 아닌 슬라이스)       │
│  • 트레이트: Clone, Copy, Debug, Display, From, Into          │
│  • 원자적 연산 (core::sync::atomic)                          │
│  • Cell, RefCell (core::cell)  — Pin (core::pin)            │
│  • core::fmt (할당 없는 포맷팅)                               │
│  • core::mem, core::ptr (저수준 메모리 조작)                  │
│  • 수학 연산: core::num, 기본 산술 연산                       │
└─────────────────────────────────────────────────────────────┘

std 없이 잃게 되는 것들:

  • HashMap 없음 (해셔 필요 — allocBTreeMap이나 hashbrown 사용)
  • println!() 없음 (stdout 필요 — 버퍼에 쓰는 core::fmt::Write 사용)
  • std::error::Error 없음 (Rust 1.81부터 core에 편입되었으나, 아직 많은 생태계가 마이너레이션 전임)
  • 파일 I/O, 네트워킹, 스레드 없음 (플랫폼 HAL에서 제공하지 않는 한)
  • Mutex 없음 (spin::Mutex나 플랫폼 전용 락 사용)

no_std 크레이트 구축하기

#![allow(unused)]
fn main() {
// src/lib.rs — no_std 라이브러리 크레이트
#![no_std]

// 선택적으로 힙 할당 사용
extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use core::fmt;

/// 온도 센서의 읽기 값.
/// 이 구조체는 베어메탈부터 Linux까지 모든 환경에서 작동합니다.
#[derive(Clone, Copy, Debug)]
pub struct Temperature {
    /// 원시 센서 값 (일반적인 I2C 센서의 경우 LSB당 0.0625°C)
    raw: u16,
}

impl Temperature {
    pub const fn from_raw(raw: u16) -> Self {
        Self { raw }
    }

    /// 섭씨 온도로 변환 (고정 소수점, FPU 불필요)
    pub const fn millidegrees_c(&self) -> i32 {
        (self.raw as i32) * 625 / 10 // 0.0625°C 해상도
    }

    pub fn degrees_c(&self) -> f32 {
        self.raw as f32 * 0.0625
    }
}

impl fmt::Display for Temperature {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let md = self.millidegrees_c();
        // -0.999°C ~ -0.001°C 사이의 값 처리 (md / 1000이 0이지만 값은 음수인 경우)
        if md < 0 && md > -1000 {
            write!(f, "-0.{:03}°C", (-md) % 1000)
        } else {
            write!(f, "{}.{:03}°C", md / 1000, (md % 1000).abs())
        }
    }
}

/// 공백으로 구분된 온도 값들을 파싱합니다.
/// alloc 사용 — 글로벌 할당자가 필요합니다.
pub fn parse_temperatures(input: &str) -> Vec<Temperature> {
    input
        .split_whitespace()
        .filter_map(|s| s.parse::<u16>().ok())
        .map(Temperature::from_raw)
        .collect()
}

/// 할당 없이 포맷팅 — 버퍼에 직접 씁니다.
/// core 전용 환경(alloc 없음, 힙 없음)에서 작동합니다.
pub fn format_temp_into(temp: &Temperature, buf: &mut [u8]) -> usize {
    use core::fmt::Write;
    struct SliceWriter<'a> {
        buf: &'a mut [u8],
        pos: usize,
    }
    impl<'a> Write for SliceWriter<'a> {
        fn write_str(&mut self, s: &str) -> fmt::Result {
            let bytes = s.as_bytes();
            let remaining = self.buf.len() - self.pos;
            if bytes.len() > remaining {
                // 버퍼 꽉 참 — 자동 절단 대신 에러 신호 전달
                return Err(fmt::Error);
            }
            self.buf[self.pos..self.pos + bytes.len()].copy_from_slice(bytes);
            self.pos += bytes.len();
            Ok(())
        }
    }
    let mut w = SliceWriter { buf, pos: 0 };
    let _ = write!(w, "{}", temp);
    w.pos
}
}
# no_std 크레이트의 Cargo.toml
[package]
name = "thermal-sensor"
version = "0.1.0"
edition = "2021"

[features]
default = ["alloc"]
alloc = []    # Vec, String 등 활성화
std = []      # 전체 std 활성화 (alloc 포함)

[dependencies]
# no_std 호환 크레이트 사용
serde = { version = "1.0", default-features = false, features = ["derive"] }
# ↑ default-features = false로 설정하면 std 의존성이 제거됩니다!

주요 크레이트 패턴: 많은 인기 크레이트(serde, log, rand, embedded-hal)가 default-features = false를 통해 no_std를 지원합니다. no_std 환경에서 사용하기 전에 해당 의존성이 std를 요구하는지 항상 확인하세요. 일부 크레이트(예: regex)는 최소한 alloc이 필요하며 core 전용 환경에서는 작동하지 않습니다.

커스텀 패닉 핸들러 및 할당자

라이브러리가 아닌 #![no_std] 바이너리에서는 패닉 핸들러를 직접 제공해야 하며, 선택적으로 글로벌 할당자도 설정해야 합니다.

// src/main.rs — no_std 바이너리 (예: UEFI 진단 도구)
#![no_std]
#![no_main]

extern crate alloc;

use core::panic::PanicInfo;

// 필수: 패닉 발생 시 수행할 작업 (스택 되감기 사용 불가)
#[panic_handler]
fn panic(info: &PanicInfo) -> ! {
    // 임베디드: LED 깜빡이기, UART에 쓰기, 중단(hang)
    // UEFI: 콘솔에 출력, 중지(halt)
    // 최소 구현: 무한 루프
    loop {
        core::hint::spin_loop();
    }
}

// alloc 사용 시 필수: 글로벌 할당자 제공
use alloc::alloc::{GlobalAlloc, Layout};

struct BumpAllocator {
    // 임베디드/UEFI를 위한 단순한 범프 할당자
    // 실전에서는 linked_list_allocator나 embedded-alloc 크레이트 사용 권장
}

// 주의: 아래는 작동하지 않는 플레이스홀더입니다! alloc() 호출 시 null을 반환하며,
// 이는 즉각적인 정의되지 않은 동작(UB)을 유발합니다 (할당자 규약상 0이 아닌 크기 할당 시
// null이 아닌 값을 반환해야 함). 실제 코드에서는 검증된 할당자 크레이트를 사용하세요:
//   - embedded-alloc (임베디드 타겟)
//   - linked_list_allocator (UEFI / OS 커널)
//   - talc (범용 no_std)
unsafe impl GlobalAlloc for BumpAllocator {
    /// # Safety
    /// Layout의 size는 0이 아니어야 합니다. null을 반환합니다 (플레이스홀더 — 크래시 유발).
    unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
        // 플레이스홀더 — 실제 할당 로직으로 교체 필요!
        core::ptr::null_mut()
    }
    /// # Safety
    /// `_ptr`은 이전에 `alloc`이 호환되는 Layout으로 반환한 값이어야 합니다.
    unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
        // 범프 할당자에서는 아무 작업도 하지 않음
    }
}

#[global_allocator]
static ALLOCATOR: BumpAllocator = BumpAllocator {};

// 진입점 (플랫폼 전용, fn main 아님)
// UEFI의 경우: #[entry] 또는 efi_main
// 임베디드의 경우: #[cortex_m_rt::entry]

no_std 코드 테스트하기

테스트는 std가 있는 호스트 머신에서 실행됩니다. 비결은 라이브러리는 no_std이지만, 테스트 하네스는 std를 사용하게 하는 것입니다.

#![allow(unused)]
fn main() {
// 크레이트: src/lib.rs에 #! [no_std] 선언
// 하지만 테스트는 자동으로 std 환경에서 실행됩니다:

#[cfg(test)]
mod tests {
    use super::*;
    // 여기서는 std를 사용할 수 있습니다 — println!, assert!, Vec 등이 모두 작동합니다.

    #[test]
    fn test_temperature_conversion() {
        let temp = Temperature::from_raw(800); // 50.0°C
        assert_eq!(temp.millidegrees_c(), 50000);
        assert!((temp.degrees_c() - 50.0).abs() < 0.01);
    }

    #[test]
    fn test_format_into_buffer() {
        let temp = Temperature::from_raw(800);
        let mut buf = [0u8; 32];
        let len = format_temp_into(&temp, &mut buf);
        let s = core::str::from_utf8(&buf[..len]).unwrap();
        assert_eq!(s, "50.000°C");
    }
}
}

실제 타겟에서 테스트하기 (std를 전혀 사용할 수 없는 경우):

# 디바이스 테스트를 위해 defmt-test 사용 (임베디드 ARM)
# UEFI 타겟을 위해 uefi-test-runner 사용
# 하드웨어 없이 교차 아키텍처 테스트를 위해 QEMU 사용

# 호스트에서 no_std 라이브러리 테스트 실행 (항상 작동):
cargo test --lib

# no_std 타겟에 대해 컴파일 확인:
cargo check --target thumbv7em-none-eabihf  # ARM Cortex-M
cargo check --target riscv32imac-unknown-none-elf  # RISC-V

no_std 의사결정 트리

flowchart TD
    START["코드가 라이브러리 표준\n라이브러리를 필요로 하는가?"] --> NEED_FS{"파일 시스템,\n네트워크, 스레드?"}
    NEED_FS -->|"예"| USE_STD["std 사용\n일반 애플리케이션"]
    NEED_FS -->|"아니요"| NEED_HEAP{"힙 할당이 필요한가?\nVec, String, Box"}
    NEED_HEAP -->|"예"| USE_ALLOC["#![no_std]\nextern crate alloc"]
    NEED_HEAP -->|"아니요"| USE_CORE["#![no_std]\ncore 전용"]
    
    USE_ALLOC --> VERIFY["cargo-hack\n--each-feature"]
    USE_CORE --> VERIFY
    USE_STD --> VERIFY
    VERIFY --> TARGET{"타겟에 OS가 있는가?"}
    TARGET -->|"예"| HOST_TEST["cargo test --lib\n표준 테스트 방식"]
    TARGET -->|"아니요"| CROSS_TEST["QEMU / defmt-test\n디바이스 테스트"]
    
    style USE_STD fill:#91e5a3,color:#000
    style USE_ALLOC fill:#ffd43b,color:#000
    style USE_CORE fill:#ff6b6b,color:#000

🏋️ 실습

🟡 실습 1: 기능 조합 검증

cargo-hack을 설치하고 여러 기능이 포함된 프로젝트에서 cargo hack check --each-feature --workspace를 실행해 보세요. 잘못된 조합을 찾아내나요?

솔루션
cargo install cargo-hack

# 각 기능을 개별적으로 체크
cargo hack check --each-feature --workspace --no-dev-deps

# 만약 특정 기능 조합이 실패한다면:
# error[E0433]: failed to resolve: use of undeclared crate or module `std`
# → 이는 특정 #[cfg] 가드에 기능 게이트가 누락되었음을 의미합니다.

# 모든 기능 + 기능 없음 + 각 개별 기능을 체크:
cargo hack check --each-feature --workspace
cargo check --workspace --all-features
cargo check --workspace --no-default-features

🔴 실습 2: no_std 라이브러리 만들기

#![no_std] 환경에서 컴파일되는 라이브러리 크레이트를 만들어 보세요. 간단한 스택 기반의 링 버퍼(Ring Buffer)를 구현합니다. thumbv7em-none-eabihf (ARM Cortex-M) 타겟으로 올바르게 컴파일되는지 확인하세요.

솔루션
#![allow(unused)]
fn main() {
// lib.rs
#![no_std]

pub struct RingBuffer<const N: usize> {
    data: [u8; N],
    head: usize,
    len: usize,
}

impl<const N: usize> RingBuffer<N> {
    pub const fn new() -> Self {
        Self { data: [0; N], head: 0, len: 0 }
    }

    pub fn push(&mut self, byte: u8) -> bool {
        if self.len == N { return false; }
        let idx = (self.head + self.len) % N;
        self.data[idx] = byte;
        self.len += 1;
        true
    }

    pub fn pop(&mut self) -> Option<u8> {
        if self.len == 0 { return None; }
        let byte = self.data[self.head];
        self.head = (self.head + 1) % N;
        self.len -= 1;
        Some(byte)
    }
}

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

    #[test]
    fn push_pop() {
        let mut rb = RingBuffer::<4>::new();
        assert!(rb.push(1));
        assert!(rb.push(2));
        assert_eq!(rb.pop(), Some(1));
        assert_eq!(rb.pop(), Some(2));
        assert_eq!(rb.pop(), None);
    }
}
}
rustup target add thumbv7em-none-eabihf
cargo check --target thumbv7em-none-eabihf
# ✅ 베어메탈 ARM용으로 컴파일 성공

핵심 요약

  • 조건부 컴파일을 사용하는 모든 크레이트에는 cargo-hack --each-feature가 필수적입니다 (CI에서 실행 권장).
  • coreallocstd 순으로 계층화되어 있으며, 각 단계마다 기능이 추가되지만 요구되는 런타임 지원도 늘어납니다.
  • 베어메탈 no_std 바이너리에는 커스텀 패닉 핸들러와 할당자가 필요합니다.
  • no_std 라이브러리는 특별한 하드웨어 없이도 호스트에서 cargo test --lib로 테스트할 수 있습니다.
  • --feature-powerset은 기능이 8개 미만인 핵심 라이브러리에만 사용하세요 ($2^n$ 조합).

Windows 및 조건부 컴파일 🟡

학습 내용:

  • Windows 지원 패턴: windows-sys/windows 크레이트, cargo-xwin
  • #[cfg]를 이용한 조건부 컴파일 — 전처리기가 아닌 컴파일러가 검증함
  • 플랫폼 추상화 아키텍처: #[cfg] 블록으로 충분한 경우 vs 트레이트를 사용해야 하는 경우
  • Linux에서 Windows용으로 교차 컴파일하기

교차 참조: no_std 및 기능cargo-hack 및 기능 검증 · 교차 컴파일 — 일반적인 교차 빌드 설정 · 빌드 스크립트build.rs에서 내보내는 cfg 플래그

Windows 지원 — 플랫폼 추상화

Rust의 #[cfg()] 속성과 Cargo 기능을 사용하면 단일 코드베이스로 Linux와 Windows를 모두 깔끔하게 지원할 수 있습니다. 이 프로젝트의 platform::run_command 모듈은 이미 이 패턴을 보여주고 있습니다.

#![allow(unused)]
fn main() {
// 프로젝트의 실제 패턴 — 플랫폼별 쉘(shell) 호출
pub fn exec_cmd(cmd: &str, timeout_secs: Option<u64>) -> Result<CommandResult, CommandError> {
    #[cfg(windows)]
    let mut child = Command::new("cmd")
        .args(["/C", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    #[cfg(not(windows))]
    let mut child = Command::new("sh")
        .args(["-c", cmd])
        .stdout(Stdio::piped())
        .stderr(Stdio::piped())
        .spawn()?;

    // ... 이후 로직은 플랫폼과 독립적입니다 ...
}
}

사용 가능한 cfg 조건:

#![allow(unused)]
fn main() {
// 운영체제
#[cfg(target_os = "linux")]         // Linux 전용
#[cfg(target_os = "windows")]       // Windows 전용
#[cfg(target_os = "macos")]         // macOS 전용
#[cfg(unix)]                        // Linux, macOS, BSD 등
#[cfg(windows)]                     // Windows (단축형)

// 아키텍처
#[cfg(target_arch = "x86_64")]      // x86 64비트
#[cfg(target_arch = "aarch64")]     // ARM 64비트
#[cfg(target_arch = "x86")]         // x86 32비트

// 포인터 너비 (아키텍처 대신 활용 가능한 호환성 옵션)
#[cfg(target_pointer_width = "64")] // 모든 64비트 플랫폼
#[cfg(target_pointer_width = "32")] // 모든 32비트 플랫폼

// 환경 / C 라이브러리
#[cfg(target_env = "gnu")]          // glibc
#[cfg(target_env = "musl")]         // musl libc
#[cfg(target_env = "msvc")]         // Windows의 MSVC 환경

// 엔디안(Endianness)
#[cfg(target_endian = "little")]
#[cfg(target_endian = "big")]

// any(), all(), not() 조합
#[cfg(all(target_os = "linux", target_arch = "x86_64"))]
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[cfg(not(windows))]
}

windows-syswindows 크레이트

Windows API를 직접 호출해야 하는 경우:

# Cargo.toml — 로우(raw) FFI를 위해 windows-sys 사용 (가볍고 추상화 없음)
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [
    "Win32_Foundation",
    "Win32_System_Services",
    "Win32_System_Registry",
    "Win32_System_Power",
] }
# 참고: windows-sys는 유의적 버전(semver) 호환되지 않는 릴리스를 사용합니다 (0.48 → 0.52 → 0.59).
# 특정 마이너 버전에 고정하세요 — 각 릴리스마다 API 바인딩이 제거되거나 이름이 변경될 수 있습니다.
# 새 프로젝트를 시작하기 전에 https://github.com/microsoft/windows-rs 에서 최신 버전을 확인하세요.

# 또는 안전한 래퍼를 위해 windows 크레이트 사용 (더 무겁지만 인체공학적임)
# windows = { version = "0.59", features = [...] }
#![allow(unused)]
fn main() {
// src/platform/windows.rs
#[cfg(windows)]
mod win {
    use windows_sys::Win32::System::Power::{
        GetSystemPowerStatus, SYSTEM_POWER_STATUS,
    };

    pub fn get_battery_status() -> Option<u8> {
        let mut status = SYSTEM_POWER_STATUS::default();
        // SAFETY: GetSystemPowerStatus는 제공된 버퍼에 기록합니다.
        // 버퍼의 크기와 정렬이 올바르게 설정되어 있습니다.
        let ok = unsafe { GetSystemPowerStatus(&mut status) };
        if ok != 0 {
            Some(status.BatteryLifePercent)
        } else {
            None
        }
    }
}
}

windows-sys vs windows 크레이트:

비교 항목windows-syswindows
API 스타일로우 FFI (unsafe 호출)안전한 (Safe) Rust 래퍼
바이너리 크기최소화 (extern 선언만 포함)더 큼 (래퍼 코드 포함)
컴파일 시간빠름느림
사용성C-스타일, 수동 안전성 보장 필요Rust다운(idiomatic) 방식
에러 처리원시 BOOL / HRESULTResult<T, windows::core::Error>
사용 추천 시점성능이 중요하거나 얇은 래퍼 필요 시일반 애플리케이션 코드, 사용 편의성 중시 시

Linux에서 Windows용으로 교차 컴파일하기

# 방법 1: MinGW (GNU ABI)
rustup target add x86_64-pc-windows-gnu
sudo apt install gcc-mingw-w64-x86-64
cargo build --target x86_64-pc-windows-gnu
# .exe 생성 — Windows에서 실행되며 msvcrt와 링크됨

# 방법 2: xwin을 통한 MSVC ABI (완전한 MSVC 호환성)
cargo install cargo-xwin
cargo xwin build --target x86_64-pc-windows-msvc
# Microsoft의 CRT 및 SDK 헤더를 자동으로 다운로드하여 사용함

# 방법 3: Zig 기반 교차 컴파일
cargo zigbuild --target x86_64-pc-windows-gnu

Windows에서의 GNU vs MSVC ABI:

비교 항목x86_64-pc-windows-gnux86_64-pc-windows-msvc
링커MinGW ldMSVC link.exe 또는 lld-link
C 런타임msvcrt.dll (보편적)ucrtbase.dll (최신형)
C++ 상호 운용성GCC ABIMSVC ABI
Linux에서 교차 빌드쉬움 (MinGW 활용)가능 (cargo-xwin 활용)
Windows API 지원전체 지원전체 지원
디버그 정보 형식DWARFPDB
권장 용도단순 도구, CI 빌드완전한 Windows 통합

조건부 컴파일 패턴

패턴 1: 플랫폼별 모듈 선택

#![allow(unused)]
fn main() {
// src/platform/mod.rs — 운영체제별로 다른 모듈 컴파일
#[cfg(target_os = "linux")]
mod linux;
#[cfg(target_os = "linux")]
pub use linux::*;

#[cfg(target_os = "windows")]
mod windows;
#[cfg(target_os = "windows")]
pub use windows::*;

// 두 모듈 모두 동일한 공개(public) API를 구현해야 함:
// pub fn get_cpu_temperature() -> Result<f64, PlatformError>
// pub fn list_pci_devices() -> Result<Vec<PciDevice>, PlatformError>
}

패턴 2: 기능을 이용한 플랫폼 지원 제어

# Cargo.toml
[features]
default = ["linux"]
linux = []              # Linux 전용 하드웨어 접근
windows = ["dep:windows-sys"]  # Windows 전용 API

[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = [...], optional = true }
#![allow(unused)]
fn main() {
// 기능(feature) 없이 Windows용으로 빌드하려고 하면 컴파일 에러 발생:
#[cfg(all(target_os = "windows", not(feature = "windows")))]
compile_error!("Windows용으로 빌드하려면 'windows' 기능을 활성화하세요");
}

패턴 3: 트레이트 기반 플랫폼 추상화

#![allow(unused)]
fn main() {
/// 하드웨어 접근을 위한 플랫폼 독립적 인터페이스.
pub trait HardwareAccess {
    type Error: std::error::Error;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error>;
    fn read_gpu_temperature(&self, gpu_index: u32) -> Result<f64, Self::Error>;
    fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error>;
    fn send_ipmi_command(&self, cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error>;
}

#[cfg(target_os = "linux")]
pub struct LinuxHardware;

#[cfg(target_os = "linux")]
impl HardwareAccess for LinuxHardware {
    type Error = LinuxHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // /sys/class/thermal/thermal_zone0/temp 읽기
        let raw = std::fs::read_to_string("/sys/class/thermal/thermal_zone0/temp")?;
        Ok(raw.trim().parse::<f64>()? / 1000.0)
    }
    // ...
}

#[cfg(target_os = "windows")]
pub struct WindowsHardware;

#[cfg(target_os = "windows")]
impl HardwareAccess for WindowsHardware {
    type Error = WindowsHwError;

    fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
        // WMI (Win32_TemperatureProbe) 또는 Open Hardware Monitor를 통해 읽기
        todo!("WMI 온도 쿼리 구현 필요")
    }
    // ...
}

/// 플랫폼에 맞는 적절한 구현체 생성
pub fn create_hardware() -> impl HardwareAccess {
    #[cfg(target_os = "linux")]
    { LinuxHardware }
    #[cfg(target_os = "windows")]
    { WindowsHardware }
}
}

플랫폼 추상화 아키텍처

여러 플랫폼을 타겟으로 하는 프로젝트의 경우, 코드를 다음의 세 계층으로 구성하세요:

┌──────────────────────────────────────────────────┐
│ 애플리케이션 로직 (플랫폼 독립적)                    │
│  diag_tool, accel_diag, network_diag, event_log 등 │
│  플랫폼 추상화 트레이트만 사용함                      │
├──────────────────────────────────────────────────┤
│ 플랫폼 추상화 계층 (트레이트 정의)                   │
│  trait HardwareAccess { ... }                     │
│  trait CommandRunner { ... }                      │
│  trait FileSystem { ... }                         │
├──────────────────────────────────────────────────┤
│ 플랫폼별 구현 (cfg 게이트 적용)                      │
│  ┌──────────────┐  ┌──────────────┐              │
│  │ Linux 구현    │  │ Windows 구현 │              │
│  │ /sys, /proc  │  │ WMI, 레지스트리│              │
│  │ ipmitool     │  │ ipmiutil     │              │
│  │ lspci        │  │ devcon       │              │
│  └──────────────┘  └──────────────┘              │
└──────────────────────────────────────────────────┘

추상화 테스트: 유닛 테스트를 위해 플랫폼 트레이트를 모킹(Mock)합니다:

#![allow(unused)]
fn main() {
#[cfg(test)]
mod tests {
    use super::*;

    struct MockHardware {
        cpu_temp: f64,
        gpu_temps: Vec<f64>,
    }

    impl HardwareAccess for MockHardware {
        type Error = std::io::Error;

        fn read_cpu_temperature(&self) -> Result<f64, Self::Error> {
            Ok(self.cpu_temp)
        }

        fn read_gpu_temperature(&self, index: u32) -> Result<f64, Self::Error> {
            self.gpu_temps.get(index as usize)
                .copied()
                .ok_or_else(|| std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    format!("GPU {index}를 찾을 수 없음")
                ))
        }

        fn list_pci_devices(&self) -> Result<Vec<PciDevice>, Self::Error> {
            Ok(vec![]) // 모킹된 값 반환
        }

        fn send_ipmi_command(&self, _cmd: &IpmiCmd) -> Result<IpmiResponse, Self::Error> {
            Ok(IpmiResponse::default())
        }
    }

    #[test]
    fn test_thermal_check_with_mock() {
        let hw = MockHardware {
            cpu_temp: 75.0,
            gpu_temps: vec![82.0, 84.0],
        };
        let result = run_thermal_diagnostic(&hw);
        assert!(result.is_ok());
    }
}
}

적용 사례: Linux 우선, Windows 대응 완료

이 프로젝트는 이미 부분적으로 Windows를 지원할 준비가 되어 있습니다. cargo-hack을 사용하여 모든 기능 조합을 검증하고, Linux에서 Windows용으로 교차 컴파일하여 테스트해 보세요.

이미 완료된 작업:

  • platform::run_command에서 쉘 선택을 위해 #[cfg(windows)] 사용
  • 테스트 코드에서 플랫폼별 적합한 명령어를 실행하기 위해 #[cfg(windows)] / #[cfg(not(windows))] 사용

Windows 지원 강화를 위한 권장 단계:

1단계: 플랫폼 추상화 트레이트 추출 (현재 → 2주 소요)
  ├─ core_lib에 HardwareAccess 트레이트 정의
  ├─ 기존 Linux 코드를 LinuxHardware 구현체로 래핑
  └─ 모든 진단 모듈이 Linux 전용 코드 대신 트레이트에 의존하도록 수정

2단계: Windows 스텁(Stub) 추가 (2주 소요)
  ├─ TODO 주석이 포함된 WindowsHardware 구현
  ├─ x86_64-pc-windows-msvc 타겟에 대한 CI 빌드 추가 (컴파일 확인용)
  └─ 모든 플랫폼에서 MockHardware를 사용해 테스트 통과 확인

3단계: 실제 Windows 기능 구현 (진행 중)
  ├─ ipmiutil.exe 또는 OpenIPMI Windows 드라이버를 통한 IPMI 지원
  ├─ accel-mgmt (accel-api.dll)를 통한 GPU 지원 — Linux와 동일한 API 사용
  ├─ Windows Setup API (SetupDiEnumDeviceInfo)를 통한 PCIe 지원
  └─ WMI (Win32_NetworkAdapter)를 통한 NIC 지원

교차 플랫폼 CI 추가:

# CI 매트릭스에 추가
- target: x86_64-pc-windows-msvc
  os: windows-latest
  name: windows-x86_64

이를 통해 실제 Windows 기능 구현이 완료되기 전에도 코드베이스가 Windows에서 컴파일되는지 확인할 수 있으며, cfg 관련 실수를 조기에 발견할 수 있습니다.

핵심 통찰: 추상화가 처음부터 완벽할 필요는 없습니다. 먼저 (이미 exec_cmd가 하는 것처럼) 말단 함수에서 #[cfg] 블록으로 시작하고, 지원하는 플랫폼이 3개 이상으로 늘어나거나 플랫폼별 차이가 커질 때 트레이트로 리팩토링하세요. 성급한 추상화는 단순한 #[cfg] 블록보다 해로울 수 있습니다.

조건부 컴파일 의사결정 트리

flowchart TD
    START["플랫폼 전용 코드가 필요한가?"] --> HOW_MANY{"지원 대상 플랫폼이 몇 개인가?"}
    
    HOW_MANY -->|"2개 (Linux + Windows)"| CFG_BLOCKS["말단 함수에서\n#[cfg] 블록 사용"]
    HOW_MANY -->|"3개 이상"| TRAIT_APPROACH["플랫폼 트레이트\n+ 플랫폼별 구현"]
    
    CFG_BLOCKS --> WINAPI{"Windows API 호출이 필요한가?"}
    WINAPI -->|"최소한의 호출"| WIN_SYS["windows-sys\n로우 FFI 바인딩"]
    WINAPI -->|"다양한 기능 (COM 등)"| WIN_RS["windows 크레이트\n안전하고 Rust다운 래퍼"]
    WINAPI -->|"없음\n(단순 #[cfg]만 필요)"| NATIVE["cfg(windows)\ncfg(unix)"]
    
    TRAIT_APPROACH --> CI_CHECK["cargo-hack\n--each-feature"]
    CFG_BLOCKS --> CI_CHECK
    CI_CHECK --> XCOMPILE["CI에서 교차 컴파일\ncargo-xwin 또는\n네이티브 러너 활용"]
    
    style CFG_BLOCKS fill:#91e5a3,color:#000
    style TRAIT_APPROACH fill:#ffd43b,color:#000
    style WIN_SYS fill:#e3f2fd,color:#000
    style WIN_RS fill:#e3f2fd,color:#000

🏋️ 실습

🟢 실습 1: 플랫폼별 조건부 모듈

#[cfg(unix)]#[cfg(windows)] 환경에서 각각 작동하는 get_hostname() 함수를 구현해 보세요. cargo checkcargo check --target x86_64-pc-windows-msvc를 통해 양쪽 환경 모두에서 컴파일되는지 확인합니다.

솔루션
#![allow(unused)]
fn main() {
// src/hostname.rs
#[cfg(unix)]
pub fn get_hostname() -> String {
    use std::fs;
    fs::read_to_string("/etc/hostname")
        .unwrap_or_else(|_| "unknown".to_string())
        .trim()
        .to_string()
}

#[cfg(windows)]
pub fn get_hostname() -> String {
    use std::env;
    env::var("COMPUTERNAME").unwrap_or_else(|_| "unknown".to_string())
}

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

    #[test]
    fn hostname_is_not_empty() {
        let name = get_hostname();
        assert!(!name.is_empty());
    }
}
}
# Linux 컴파일 확인
cargo check

# Windows 교차 컴파일 확인
rustup target add x86_64-pc-windows-msvc
cargo check --target x86_64-pc-windows-msvc

🟡 실습 2: cargo-xwin으로 Windows용 교차 컴파일

cargo-xwin을 설치하고 Linux에서 x86_64-pc-windows-msvc용 간단한 바이너리를 빌드해 보세요. 출력 결과물이 .exe 파일인지 확인합니다.

솔루션
cargo install cargo-xwin
rustup target add x86_64-pc-windows-msvc

cargo xwin build --release --target x86_64-pc-windows-msvc
# Windows SDK 헤더 및 라이브러리를 자동으로 다운로드함

file target/x86_64-pc-windows-msvc/release/my-binary.exe
# 출력 예시: PE32+ executable (console) x86-64, for MS Windows

# Wine이 설치되어 있다면 테스트 가능:
wine target/x86_64-pc-windows-msvc/release/my-binary.exe

핵심 요약

  • 말단 함수에서는 #[cfg] 블록으로 시작하고, 플랫폼 간 차이가 커질 때 트레이트로 리팩토링하세요.
  • windows-sys는 로우 FFI용이며, windows 크레이트는 안전하고 관용적인 래퍼를 제공합니다.
  • cargo-xwin을 사용하면 Windows 머신 없이도 Linux에서 Windows MSVC ABI로 교차 컴파일할 수 있습니다.
  • Linux 서비스만 제공하더라도 CI에서 --target x86_64-pc-windows-msvc를 주기적으로 체크하여 cfg 관련 실수를 유의하세요.
  • #[cfg]를 Cargo 기능과 조합하여 선택적인 플랫폼 지원(예: feature = "windows")을 구현하세요.

11. 종합 정리 — 운영 환경용 CI/CD 파이프라인 🟡

학습 내용:

  • 다단계 GitHub Actions CI 워크플로 구성 (검사 → 테스트 → 커버리지 → 보안 → 교차 빌드 → 릴리스)
  • rust-cachesave-if 설정을 이용한 캐싱 전략
  • Nightly 스케줄에 따른 Miri 및 새니타이저 실행
  • Makefile.toml과 pre-commit 훅을 이용한 작업 자동화
  • cargo-dist를 이용한 자동 릴리스

참조: 빌드 스크립트 · 교차 컴파일 · 벤치마킹 · 코드 커버리지 · Miri/새니타이저 · 의존성 관리 · 릴리스 프로필 · 컴파일 타임 도구 · no_std · Windows

개별 도구들도 유용하지만, 모든 푸시마다 이들을 자동으로 조율하는 파이프라인은 개발 경험을 혁신적으로 바꿔놓습니다. 이 장에서는 1~10장에서 다룬 도구들을 하나의 응집력 있는 CI/CD 워크플로로 통합합니다.


1. 전체 GitHub Actions 워크플로

모든 검증 단계를 병렬로 실행하는 단일 워크플로 파일 예시입니다.

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

env:
  CARGO_TERM_COLOR: always
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"  # 경고를 에러로 처리 (최상위 크레이트 전용)

jobs:
  # ─── 1단계: 빠른 피드백 (< 2분) ───
  check:
    name: Check + Clippy + Format
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
        with:
          components: clippy, rustfmt
      - uses: Swatinem/rust-cache@v2
      - name: Check compilation
        run: cargo check --workspace --all-targets --all-features
      - name: Clippy lints
        run: cargo clippy --workspace --all-targets --all-features -- -D warnings
      - name: Formatting
        run: cargo fmt --all -- --check

  # ─── 2단계: 테스트 (< 5분) ───
  test:
    name: Test (${{ matrix.os }})
    needs: check
    strategy:
      matrix:
        os: [ubuntu-latest, windows-latest]
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: dtolnay/rust-toolchain@stable
      - uses: Swatinem/rust-cache@v2
      - name: Run tests
        run: cargo test --workspace

  # ─── 3단계: 교차 컴파일 (< 10분) ───
  cross:
    name: Cross (${{ matrix.target }})
    needs: check
    strategy:
      matrix:
        include:
          - target: x86_64-unknown-linux-musl
            os: ubuntu-latest
          - target: aarch64-unknown-linux-gnu
            os: ubuntu-latest
            use_cross: true
    runs-on: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: taiki-e/install-action@cross
      - name: Build
        run: cross build --release --target ${{ matrix.target }}

  # ─── 4단계: 커버리지 (< 10분) ───
  coverage:
    name: Code Coverage
    needs: check
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: taiki-e/install-action@cargo-llvm-cov
      - name: Generate coverage
        run: cargo llvm-cov --workspace --fail-under-lines 75

2. CI 캐싱 전략

Swatinem/rust-cache@v2는 Rust CI의 표준입니다. 대규모 워크스페이스에서는 다음과 같은 튜닝이 필요합니다.

  • save-if 활용: main 브랜치에서만 캐시를 저장하고, PR에서는 읽기만 하도록 설정하여 캐시가 오염되는 것을 방지합니다.
  • prefix-key: 캐시 크기가 너무 커지면(>5GB) 접두사를 변경하여 캐시를 초기화하세요.
  • 타겟별 분리: 교차 컴파일 타겟마다 별도의 캐시 키를 사용하세요.

3. cargo-make를 이용한 작업 자동화

cargo-make는 플랫폼에 독립적인 작업 실행 도구입니다.

# Makefile.toml 예시
[tasks.dev]
description = "CI와 동일한 로컬 검증 실행"
dependencies = ["check", "test", "clippy", "fmt-check"]

[tasks.coverage]
description = "HTML 커버리지 보고서 생성 및 열기"
install_crate = "cargo-llvm-cov"
command = "cargo"
args = ["llvm-cov", "--workspace", "--html", "--open"]

이제 cargo make dev 한 번으로 모든 로컬 검증을 마칠 수 있습니다.


4. 자동 릴리스: cargo-releasecargo-dist

  • cargo-release: 버전 번호 올리기, 태그 생성, Cargo.lock 업데이트를 자동화합니다.
  • cargo-dist: GitHub Releases에 업로드할 바이너리 아카이브와 설치 스크립트를 생성합니다. cargo dist init 명령어로 손쉽게 시작할 수 있습니다.

핵심 요약

  1. 병렬 실행 — 빠른 검증(check)을 통과한 후 무거운 작업들(test, cross, coverage)을 병렬로 돌려 시간을 단축하세요.
  2. 캐시 관리main 브랜치 위주의 캐시 저장 정책으로 PR 빌드 속도를 높이세요.
  3. 로컬 자동화 — CI와 동일한 환경을 로컬에서 cargo-make로 재현하여 커밋 전 실수를 방지하세요.
  4. 배포 자동화 — 버전 관리와 바이너리 배포를 도구화하여 실수를 줄이고 고품질의 아티팩트를 제공하세요.

12. 실전 팁과 요령 (Tricks from the Trenches) 🟡

학습 내용:

  • 한 장에 담기 힘든 실전에서 검증된 패턴들
  • CI 불안정성(flake)이나 바이너리 비대화와 같은 흔한 문제들에 대한 해결책
  • 오늘 바로 적용할 수 있는 빠른 성과를 내는 기술들

참조: 이 책의 모든 장 — 이 팁들은 모든 주제에 걸쳐 적용됩니다.

이 장에서는 실제 운영 환경의 Rust 코드베이스에서 반복적으로 나타나는 엔지니어링 패턴들을 모았습니다. 각 팁은 독립적이므로 순서와 상관없이 읽으셔도 좋습니다.


1. deny(warnings) 함정 피하기

문제: 소스 코드에 #![deny(warnings)]를 직접 넣으면 Clippy가 새로운 린트를 추가할 때 어제는 컴파일되던 코드가 갑자기 실패할 수 있습니다.

해결: 소스 코드 대신 CI 환경 변수(CARGO_ENCODED_RUSTFLAGS)를 사용하세요.

# CI: 소스 수정 없이 경고를 에러로 처리
env:
  CARGO_ENCODED_RUSTFLAGS: "-Dwarnings"

2. 빌드는 한 번, 테스트는 어디서나

문제: cargo test--lib, --doc, --test 사이를 전환할 때마다 프로필이 다르기 때문에 재컴파일이 발생합니다.

해결: 유닛/통합 테스트에는 cargo nextest를 사용하고 문서 테스트는 따로 실행하세요.

cargo nextest run --workspace        # 빠름: 병렬, 캐싱됨
cargo test --workspace --doc         # 문서 테스트 (nextest는 지원 안 함)

3. 기능 플래그(Feature Flag) 관리

문제: 라이브러리가 기본값으로 std를 사용하지만 아무도 --no-default-features를 테스트하지 않으면, 언젠가 임베디드 사용자가 컴파일 오류를 보고하게 됩니다.

해결: CI에 cargo-hack을 추가하여 모든 기능 조합을 검사하세요.

- name: Feature matrix
  run: |
    cargo hack check --each-feature --no-dev-deps

4. Lock 파일: 커밋할 것인가, 무시할 것인가?

권장 규칙:

  • 바이너리/애플리케이션: (재현 가능한 빌드를 위해 Cargo.lock 커밋)
  • 라이브러리: 아니요 (사용자가 버전을 선택할 수 있도록 .gitignore에 추가)
  • 둘 다 있는 워크스페이스: (바이너리 규칙이 우선함)

5. 최적화된 의존성을 이용한 디버그 빌드

문제: serde, regex 같은 무거운 의존성들이 최적화되지 않아 디버그 빌드의 실행 속도가 너무 느립니다.

해결: 개발용 프로필에서 의존성만 최적화하도록 설정하세요.

# Cargo.toml
[profile.dev.package."*"]
opt-level = 2  # 개발 모드에서 모든 의존성 최적화

6. RUSTFLAGS vs CARGO_ENCODED_RUSTFLAGS

문제: RUSTFLAGS="-Dwarnings"는 빌드 스크립트와 proc-macro까지 모두 에러로 처리하여 외부 라이브러리 경고 때문에 빌드가 깨질 수 있습니다.

해결: **CARGO_ENCODED_RUSTFLAGS**를 사용하면 최상위 크레이트에만 적용되어 더 안전합니다.


7. cargo tree를 이용한 중복 제거 루틴

문제: syn 버전이 5개, tokio-util 버전이 3개나 있어서 컴파일이 너무 오래 걸립니다.

해결: cargo tree --duplicates로 중복을 찾고 cargo update -p <패키지>로 버전을 통합하세요. 정기적으로 이 작업을 수행하면 컴파일 시간을 5~15% 단축할 수 있습니다.


핵심 요약

  1. 에러 처리의 분리 — 소스 코드보다는 CI 환경에서 경고 에러 여부를 관리하세요.
  2. 개발 생산성opt-level = 2 설정을 통해 의존성 실행 속도를 높이고 개발 주기를 단축하세요.
  3. 가벼운 의존성cargo treecargo update를 습관화하여 컴파일 시간과 바이너리 크기를 최적화하세요.
  4. 로컬 검증cargo make나 pre-push 훅을 이용해 실수 없는 커밋 문화를 만드세요.

빠른 참조 카드 (Quick Reference Card)

명령어 요약: 한눈에 보는 가이드

# ─── 빌드 스크립트 (Build Scripts) ───
cargo build                          # build.rs를 먼저 컴파일한 후 크레이트 빌드
cargo build -vv                      # 상세 출력 — build.rs의 모든 출력을 표시

# ─── 교차 컴파일 (Cross-Compilation) ───
rustup target add x86_64-unknown-linux-musl
cargo build --release --target x86_64-unknown-linux-musl
cargo zigbuild --release --target x86_64-unknown-linux-gnu.2.17
cross build --release --target aarch64-unknown-linux-gnu

# ─── 벤치마킹 (Benchmarking) ───
cargo bench                          # 모든 벤치마크 실행
cargo bench -- parse                 # "parse"가 포함된 벤치마크만 실행
cargo flamegraph -- --args           # 바이너리에서 플레임그래프 생성
perf record -g ./target/release/bin  # perf 데이터 기록
perf report                          # 대화형으로 perf 결과 확인

# ─── 커버리지 (Coverage) ───
cargo llvm-cov --html                # HTML 보고서 생성
cargo llvm-cov --lcov --output-path lcov.info
cargo llvm-cov --workspace --fail-under-lines 80
cargo tarpaulin --out Html           # 대안 도구 (Linux용)

# ─── 안전성 검증 (Safety Verification) ───
cargo +nightly miri test             # Miri에서 테스트 실행 (UB 감지)
valgrind --leak-check=full ./target/debug/binary  # 메모리 누수 확인
RUSTFLAGS="-Zsanitizer=address" cargo +nightly test -Zbuild-std --target x86_64-unknown-linux-gnu

# ─── 감사 및 공급망 (Audit & Supply Chain) ───
cargo audit                          # 알려진 취약점 스캔
cargo audit --deny warnings          # 취약점 발견 시 빌드 실패
cargo deny check                     # 라이선스 + 보안 + 소스 검증
cargo vet                            # 공급망 신뢰성 검증
cargo outdated --workspace           # 오래된 의존성 찾기
cargo semver-checks                  # 하위 호환성 깨짐 감지
cargo geiger                         # 의존성 트리의 unsafe 코드 개수 확인

# ─── 바이너리 최적화 (Binary Optimization) ───
cargo bloat --release --crates       # 크레이트별 바이너리 크기 기여도 확인
cargo bloat --release -n 20          # 가장 큰 함수 20개 확인
cargo +nightly udeps --workspace     # 사용되지 않는 의존성 찾기
cargo machete                        # 빠른 사용 중단 의존성 감지
cargo expand --lib module::name      # 매크로 확장 결과 확인
cargo msrv find                      # 최소 지원 Rust 버전(MSRV) 확인
cargo clippy --fix --workspace --allow-dirty  # 린트 경고 자동 수정

# ─── 컴파일 타임 최적화 (Compile-Time) ───
export RUSTC_WRAPPER=sccache         # 공용 컴파일 캐시 설정
sccache --show-stats                 # 캐시 적중 통계 표시
cargo nextest run                    # 더 빠른 병렬 테스트 실행 도구

# ─── 플랫폼 엔지니어링 (Platform Engineering) ───
cargo check --target thumbv7em-none-eabihf   # no_std 빌드 확인
cargo build --target x86_64-pc-windows-gnu   # Linux에서 Windows용 빌드
cargo xwin build --target x86_64-pc-windows-msvc  # MSVC ABI 기반 교차 빌드

# ─── 릴리스 (Release) ───
cargo release patch --dry-run        # 릴리스 예행 연습
cargo release patch --execute        # 버전 업, 커밋, 태그, 게시 실행
cargo dist plan                      # 배포용 아티팩트(바이너리) 생성 계획 확인

의사결정 테이블: 언제 어떤 도구를 사용할 것인가?

목표도구사용 시점
git 해시 / 빌드 정보 포함build.rs바이너리에 추적성이 필요할 때
Rust와 C 코드 함께 컴파일cc 크레이트소규모 C 라이브러리와 연결할 때
스키마에서 코드 생성prost-buildProtobuf, gRPC 스텁 생성이 필요할 때
시스템 라이브러리 링크pkg-configOpenSSL, libpci, systemd와 연결할 때
정적 Linux 바이너리musl 타겟컨테이너나 클라우드 배포 시
고령화된 glibc 타겟cargo-zigbuildCentOS 7, RHEL 7 호환성이 필요할 때
ARM 서버용 빌드cross / zigGraviton, Ampere 서버용 배포 시
통계적 벤치마킹Criterion.rs정밀한 성능 회귀 감지가 필요할 때
빠른 성능 확인Divan개발 중 프로파일링 전 가볍게 확인 시
핫스팟 식별cargo flamegraph어느 함수가 느린지 시각화할 때
라인/브랜치 커버리지cargo-llvm-covCI 게이트 설정 및 사각지대 분석 시
Rust UB 감지Miri순수 Rust unsafe 코드를 검증할 때
C FFI 메모리 안전성ValgrindRust와 C가 혼합된 코드베이스 검증 시
데이터 레이스 감지TSan / Miri동시성 기반 코드의 안전성을 확인할 때
로컬 자동화cargo make다단계 로컬 검증 과정을 하나로 묶을 때
자동 릴리스cargo-dist멀티 플랫폼 바이너리 배포를 자동화할 때
의존성 취약점 점검cargo-audit보안 감사가 필수인 프로젝트일 때
라이선스 준수cargo-deny상용 프로젝트의 라이선스 정책을 관리할 때
바이너리 크기 분석cargo-bloat임베디드나 용량 제한 환경용 빌드 시
하위 호환성 체크cargo-semver라이브러리 퍼블리싱 전 브레이킹 체인지 확인 시