cargo-crap은 변경 위험 반패턴(CRAP) 메트릭을 계산해 복잡하면서도 테스트가 부족한 Rust 함수를 찾아내는 도구입니다.
Rust는 많은 버그를 불가능하게 만듭니다.
메모리 안전성. 스레드 안전성. 소유권. 라이프타임. 완전한 매칭. 강한 타입.
하지만 Rust는 유지보수에서 꼭 필요한 한 가지 질문에는 답할 수 없습니다. 이 코드는 변경해도 안전한가?
함수는 완벽하게 컴파일되더라도 수정하기에는 위험할 수 있습니다.
분기가 너무 많을 수 있고, 특수한 경우가 너무 많을 수 있으며, 숨겨진 경로가 너무 많고, 확신을 줄 만큼 테스트가 충분하지 않을 수 있습니다.
이것이 제가 cargo-crap을 만든 이유입니다. 이것은 변경 위험 반패턴(CRAP) 메트릭을 계산하여 복잡하면서도 테스트가 부족한 함수를 찾아내는 Rust 도구입니다.
커버리지는 코드의 어떤 부분이 테스트에 의해 실행되었는지를 보여줍니다. 복잡성은 함수 안에 얼마나 많은 경로가 존재할 수 있는지를 보여줍니다. CRAP은 이 두 신호를 결합해 변경하기 위험한 코드를 드러냅니다.
저는 이것을 AI 지원 Rust 개발을 위한 작은 가드레일 중 하나로 만들었습니다. 에이전트는 빠르게 움직일 수 있지만, 그들이 도입하는 복잡성을 둘러싼 측정 가능한 점검 장치는 여전히 필요합니다.
코드 커버리지는 유용하지만, 오해를 부를 수 있습니다.
0% 커버리지를 가진 작은 헬퍼 함수는 바람직하지 않지만, 아마도 시스템에서 가장 큰 위험은 아닐 것입니다:
fn is_empty(value: &str) -> bool {
value.trim().is_empty()
}
이제 이것을 입력을 파싱하고, 비즈니스 규칙을 검증하고, 여러 엣지 케이스를 처리하고, 상태를 변경하며, 중첩된 분기가 여러 개 있는 큰 함수와 비교해 보세요.
그 함수가 0% 커버리지를 가지고 있다면, 위험은 완전히 다릅니다.
커버리지만으로는 그것을 알려주지 않습니다. 테스트 중 무엇이 실행되었는지만 알려줄 뿐입니다. 코드가 얼마나 이해하기 어려운지, 또는 그 안에 얼마나 많은 경로가 존재하는지는 알려주지 않습니다.
순환 복잡도는 함수 안의 독립적인 경로 수를 측정합니다. 모든 if, match, loop, 그리고 분기는 더 많은 가능한 경로를 추가합니다.
이것은 유용한 정보이지만, 복잡성만으로도 충분하지 않습니다.
어떤 코드는 본질적으로 복잡합니다. 파서, 상태 머신, 프로토콜 핸들러, 검증 로직, 호환성 계층은 도메인 자체가 분기하기 때문에 자주 분기합니다.
그 코드가 잘 테스트되어 있다면 위험은 더 낮습니다.
진짜 문제는 복잡성 그 자체가 아니라 테스트되지 않은 복잡성입니다.
CRAP 메트릭은 2007년에 Alberto Savoia와 Bob Evans가 crap4j와 함께 소개했으며, 이후 Savoia는 Google Testing Blog에서 이 아이디어를 더 자세히 설명했습니다. 이것은 순환 복잡도와 테스트 커버리지를 하나의 숫자로 결합합니다:
CRAP(m) = comp(m)² × (1 − cov(m)/100)³ + comp(m)
Where:
comp(m)는 함수의 순환 복잡도입니다cov(m)는 해당 함수의 테스트 커버리지 백분율입니다공식을 외울 필요는 없습니다. 직관은 단순합니다:
마지막 범주가 중요한 이유는 바로 그 지점에서 코드 변경이 위험해지기 때문입니다.
높은 CRAP 점수를 개선하는 직접적인 방법은 두 가지입니다. 함수의 복잡성을 낮추거나, 중요한 경로 주변에 의미 있는 테스트를 추가하는 것입니다. 이 숫자는 함수를 비난하기 위한 것이 아닙니다. 어떤 곳에서 리팩터링이나 테스트 작업이 가장 큰 위험 감소를 가져오는지를 보여줍니다.
AI 에이전트는 일상적인 소프트웨어 개발의 일부가 되어가고 있습니다. 이들은 코드를 생성하고, 함수를 리팩터링하고, 테스트를 추가하고, API를 업데이트하며, 코드베이스를 매우 빠르게 이동할 수 있습니다.
그 속도는 양날의 검입니다. 이전보다 코드를 더 복잡하게 만들거나 테스트를 더 약하게 만들 수도 있습니다.
자동으로 생성된 분기, 예외, 폴백이 하나씩 추가될 때마다 AI 에이전트는 함수를 점차 더 이해하기 어렵고 테스트하기 어렵게 만들 수 있습니다.
제가 자주 보는 한 가지 패턴은 누적으로 보존하는 방식입니다. 모델을 단순화하는 대신, 에이전트는 현재 동작을 유지하기 위해 또 하나의 폴백, 또 하나의 특수 케이스, 또 하나의 호환성 분기를 추가합니다. 테스트는 여전히 통과할 수 있지만, 함수는 추론하기 더 어려워집니다. 코드는 컴파일되지만, 시스템은 안전하게 변경하기 더 어려워집니다.
cargo-crap은 선택한 임곗값을 점수가 넘을 때 경고를 보내는 AI 지원 개발의 경계선 역할을 합니다.
코드는 바꿀 수 있지만,
cargo-crap은 조용히 증가하는 고위험의 테스트되지 않은 복잡성을 보이게 만듭니다.
그 경계선은 코드가 인간뿐 아니라 AI 에이전트에 의해서도 바뀔 때 더욱 중요합니다.
제가 의도한 워크플로는 단순합니다:
cargo-crap을 실행합니다.cargo-crap이 하는 일cargo-crap은 이 메트릭을 Cargo 스타일 도구로 Rust 생태계에 가져옵니다.
cargo-binstall을 사용한다면, 미리 빌드된 바이너리를 설치할 수 있습니다:
cargo binstall cargo-crap
또는 소스에서 설치할 수 있습니다:
cargo install cargo-crap
기본 워크플로는 두 개의 명령입니다:
cargo llvm-cov --lcov --output-path lcov.info
cargo crap --lcov lcov.info
LCOV 커버리지 리포트를 생성합니다.cargo-crap이 Rust 소스 코드를 분석하고, 함수별 복잡성을 계산하고, 이를 커버리지 데이터와 결합한 뒤, 순위가 매겨진 리포트를 출력합니다:┌───┬───────┬────┬───────────────────┬──────────┬───────────────┐
│ │ CRAP │ CC │ Coverage │ Function │ Location │
╞═══╪═══════╪════╪═══════════════════╪══════════╪═══════════════╡
│ ✗ │ 156.0 │ 12 │ ░░░░░░░░░░ 0.0% │ crappy │ src/lib.rs:24 │
│ ▲ │ 6.7 │ 4 │ ████░░░░░░ 44.4% │ moderate │ src/lib.rs:12 │
│ ✓ │ 1.0 │ 1 │ ██████████ 100.0% │ trivial │ src/lib.rs:8 │
└───┴───────┴────┴───────────────────┴──────────┴───────────────┘
✗ 1/3 function(s) exceed CRAP threshold 30.
큰 커버리지 리포트를 훑어보며 추측하는 대신, cargo-crap은 주의가 필요한 함수들의 집중된 순위 목록을 제공합니다.
기본 임곗값은 30이며, 이는 원래의 CRAP 가이드를 따릅니다. 함수가 그 선을 넘으면, 일반적인 배경 잡음이 아니라 고위험 변경 대상으로 취급할 가치가 있습니다.
세 개의 함수를 상상해 봅시다:
| Function | Complexity | Coverage | CRAP |
|---|---|---|---|
| trivial | 1 | 100% | 1.0 |
| moderate | 4 | 50% | 6.0 |
| risky | 12 | 0% | 156.0 |
첫 번째 함수는 단순하고 완전히 테스트되어 있습니다. 크게 걱정할 것이 없습니다.
두 번째는 어느 정도 분기가 있고 부분적인 커버리지를 가집니다. 개선할 가치가 있을 수는 있지만, 당장 주목을 요구할 정도는 아닙니다.
세 번째가 흥미로운 대상입니다. 복잡도 12는 코드 안에 많은 경로가 있음을 나타냅니다. 커버리지가 0%라면, 모든 변경은 작은 믿음의 도약이 됩니다.
아마도 프로덕션에서 문제를 일으키기 전에 먼저 살펴보고 싶은 함수가 바로 그런 함수입니다.
다음은 cargo-crap이 눈에 띄게 만들고자 하는 형태의 예입니다:
fn classify_event(kind: &str, retry_count: u8, source: Option<&str>) -> &'static str {
if kind == "payment_failed" {
if retry_count > 3 {
return "manual_review";
}
if source == Some("partner") {
return "partner_retry";
}
return "retry";
}
if kind == "payment_succeeded" {
return "complete";
}
if kind == "refund_requested" && source != Some("internal") {
return "review_refund";
}
"unknown"
}
이 함수는 아주 크지는 않고, Rust는 기꺼이 이것을 컴파일할 것입니다. 하지만 이미 여러 경로, 조기 반환, 그리고 조건문 안에 숨겨진 비즈니스 규칙을 가지고 있습니다. 커버리지가 그 경로 대부분을 놓친다면, 위험은 이론적인 것이 아닙니다. 다음에 이것을 변경하는 사람이나 에이전트는 어떤 분기가 중요한지 추측해야 합니다.
다음은 cargo-crap에 SARIF 2.1.0 출력을 추가한 PR #17에서 가져온 발췌입니다:

그 코멘트는 리뷰 대화를 바꿉니다. “이 PR이 괜찮아 보이나요?”라고 묻는 대신, 더 구체적인 질문을 할 수 있습니다:
이것이 실질적인 가치입니다. 리뷰를 대체하지는 않지만, 리뷰가 더 날카로운 출발점을 갖게 해줍니다.
커버리지는 올바른 동작을 검증하지 않고도 한 줄을 실행할 수 있습니다. 함수는 완전히 커버되어 있어도 테스트 품질이 나쁠 수 있습니다.
그래서 CRAP 점수는 절대적인 진실로 취급되어서는 안 됩니다. 이것은 신호이며, 유용한 신호입니다.
이 도구의 가장 좋은 사용법은 더 나은 질문을 던지는 것입니다:
좋은 도구는 사고를 대체하지 않습니다. 사고가 더 잘 집중되도록 도와줄 뿐입니다.
이것은 Rust 코드베이스가 커지고 있는 팀, 큰 리팩터링, 생성된 코드, 또는 AI 지원 풀 리퀘스트에 특히 유용합니다. 코드가 빠르게 바뀌고 리뷰 시간이 제한되어 있다면, 위험한 함수들의 순위 목록은 리뷰어에게 구체적인 출발점을 제공합니다.
cargo-crap은 로컬에서도 사용할 수 있지만, 저는 CI에서 더 유용해진다고 생각합니다.
새롭거나 작은 프로젝트에서는 절대 임곗값이 잘 작동할 수 있습니다. 함수가 임곗값을 넘으면 빌드를 실패하게 만들 수 있습니다:
cargo crap --lcov lcov.info --fail-above --threshold 30
기존 프로젝트에서는 엄격한 임곗값에 주의하는 것이 좋습니다. 대부분의 실제 코드베이스에는 이미 어느 정도의 레거시 복잡성이 있습니다. 즉시 강한 게이트를 켜면, 지금 당장 아무도 고칠 준비가 되어 있지 않은 오래된 문제들의 긴 목록이 나올 수 있습니다.
성숙한 코드베이스라면, 저는 기준선 모드로 시작하겠습니다:
cargo crap --lcov lcov.info --format json --output baseline.json
cargo crap --lcov lcov.info --baseline baseline.json --fail-regression
이것은 코드베이스가 오늘 완벽하다고 가장하지 않습니다. 단지 이렇게 말할 뿐입니다:
기존 문제가 있을 수는 있지만, 새로운 변경이 그것들을 더 나쁘게 만들어서는 안 된다.
이것은 건강한 엔지니어링 규칙입니다. 특히 코드베이스에 대한 통제를 잃지 않으면서 빠르게 반복하고 싶은 AI 지원 개발에 매우 유용합니다.
출력을 리뷰어 친화적으로 만들 수도 있습니다. --format github는 GitHub Actions 주석을 출력하고, --format pr-comment는 리뷰 중에 더 쉽게 훑어볼 수 있는 풀 리퀘스트 코멘트 형식을 생성합니다.
GitHub Actions에서 가장 단순한 임곗값 게이트는 다음과 같습니다:
name: change-risk
on:
pull_request:
push:
branches: [main]
jobs:
cargo-crap:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- uses: taiki-e/install-action@cargo-llvm-cov
- run: cargo install cargo-crap
- run: cargo llvm-cov --lcov --output-path lcov.info
- run: cargo crap --lcov lcov.info --fail-above --threshold 30
이것은 의도적으로 작게 유지한 예입니다. 성숙한 코드베이스라면 절대 게이트보다 기준선 모드로 시작한 다음, 가장 심각한 문제들이 파악된 뒤에 임곗값을 더 엄격하게 조이세요.
소프트웨어 품질은 코드가 컴파일되는지, 또는 테스트가 존재하는지만의 문제가 아닙니다. 내일 우리가 그 시스템을 안전하게 바꿀 수 있는가의 문제입니다.
이것은 AI 에이전트가 인간이 모든 세부 사항을 검토하는 속도보다 더 빠르게 코드를 바꿀 수 있을 때 더욱 중요합니다.
cargo-crap은 한 가지 문제를 보이게 만듭니다. 바로 테스트 커버리지가 부족한 복잡한 Rust 코드입니다.
목표는 모든 숫자를 완벽하게 만들거나 변경을 금지하는 것이 아닙니다. 요점은 위험한 변경이 조용히 일상이 되기 전에 그것을 보이게 만드는 것입니다.
Rust는 우리의 프로그램이 무엇을 할 수 없는지에 대해 강력한 보장을 제공합니다. AI는 속도를 제공합니다. cargo-crap은 이 두 현실을 단순한 규칙으로 연결하는 데 도움을 줍니다:
빠르게 움직이되, 추가하고 있는 위험을 측정하라.