CRAP 점수 분석
이 스킬을 사용해야 할 때
다음 상황에서 이 스킬을 사용하세요:
- 변경 전 코드 품질과 테스트 커버리지 평가
- 리팩토링 또는 테스트가 필요한 고위험 코드 식별
- .NET 프로젝트의 커버리지 수집 설정
- 위험 기반 테스트 우선순위 지정
- CI/CD 파이프라인에 커버리지 임계값 설정
CRAP이란?
CRAP 점수 = 복잡도 x (1 - 커버리지)^2
CRAP(Change Risk Anti-Patterns) 점수는 순환 복잡도와 테스트 커버리지를 결합하여 위험한 코드를 식별합니다.
| CRAP 점수 | 위험 수준 | 필요한 조치 |
|---|---|---|
| < 5 | 낮음 | 잘 테스트되고 유지보수 가능한 코드 |
| 5-30 | 보통 | 수용 가능하지만 복잡도 주시 필요 |
| > 30 | 높음 | 테스트 또는 리팩토링 필요 |
CRAP이 중요한 이유
- 높은 복잡도 + 낮은 커버리지 = 위험: 이해하기 어렵고 테스트되지 않은 코드는 수정 시 위험
- 복잡도만으로는 부족: 100% 커버리지의 복잡한 메서드는 0% 커버리지의 간단한 메서드보다 안전
- 노력 집중: 단순한 getter/setter가 아닌 복잡한 코드에 테스트 우선순위 지정
CRAP 점수 예시
| 메서드 | 복잡도 | 커버리지 | 계산식 | CRAP |
|---|---|---|---|---|
GetUserId() | 1 | 0% | 1 x (1 - 0)^2 | 1 |
ParseToken() | 54 | 52% | 54 x (1 - 0.52)^2 | 12.4 |
ValidateForm() | 20 | 0% | 20 x (1 - 0)^2 | 20 |
ProcessOrder() | 45 | 20% | 45 x (1 - 0.20)^2 | 28.8 |
ImportData() | 80 | 10% | 80 x (1 - 0.10)^2 | 64.8 |
커버리지 수집 설정
coverage.runsettings
리포지토리 루트에 coverage.runsettings 파일을 생성하세요. CRAP 점수 계산에는 OpenCover 형식이 필수입니다. OpenCover는 순환 복잡도 메트릭을 포함하기 때문입니다.
<?xml version="1.0" encoding="utf-8" ?>
<RunSettings>
<DataCollectionRunSettings>
<DataCollectors>
<DataCollector friendlyName="XPlat code coverage">
<Configuration>
<!-- OpenCover 형식은 CRAP 점수를 위한 순환 복잡도를 포함 -->
<Format>cobertura,opencover</Format>
<!-- 테스트 및 벤치마크 어셈블리 제외 -->
<Exclude>[*.Tests]*,[*.Benchmark]*,[*.Migrations]*</Exclude>
<!-- 생성된 코드, 사용되지 않는 멤버, 명시적 제외 대상 제외 -->
<ExcludeByAttribute>Obsolete,GeneratedCodeAttribute,CompilerGeneratedAttribute,ExcludeFromCodeCoverageAttribute</ExcludeByAttribute>
<!-- 소스 생성 파일, Blazor 생성 코드, 마이그레이션 제외 -->
<ExcludeByFile>**/obj/**/*,**/*.g.cs,**/*.designer.cs,**/*.razor.g.cs,**/*.razor.css.g.cs,**/Migrations/**/*</ExcludeByFile>
<!-- 테스트 프로젝트 제외 -->
<IncludeTestAssembly>false</IncludeTestAssembly>
<!-- 최적화 플래그 -->
<SingleHit>false</SingleHit>
<UseSourceLink>true</UseSourceLink>
<SkipAutoProps>true</SkipAutoProps>
</Configuration>
</DataCollector>
</DataCollectors>
</DataCollectionRunSettings>
</RunSettings>
주요 구성 옵션
| 옵션 | 목적 |
|---|---|
Format | 복잡도 메트릭을 위해 opencover 포함 필수 |
Exclude | 패턴으로 테스트/벤치마크 어셈블리 제외 |
ExcludeByAttribute | 생성된 코드, 사용되지 않는 코드, 명시적 제외 대상 건너뛰기 (ExcludeFromCodeCoverageAttribute 포함) |
ExcludeByFile | 소스 생성 파일, Blazor 컴포넌트, 마이그레이션 제외 |
SkipAutoProps | 자동 속성을 분기로 계산하지 않음 |
ReportGenerator 설치
Risk Hotspots가 포함된 HTML 보고서를 생성하려면 ReportGenerator를 로컬 도구로 설치하세요.
.config/dotnet-tools.json에 추가
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-reportgenerator-globaltool": {
"version": "5.4.5",
"commands": ["reportgenerator"],
"rollForward": false
}
}
}
그런 다음 복원:
dotnet tool restore
또는 전역 설치
dotnet tool install --global dotnet-reportgenerator-globaltool
커버리지 수집
테스트 실행 및 커버리지 수집
# 이전 결과 정리
rm -rf coverage/ TestResults/
# 단위 테스트 실행 (커버리지 포함)
dotnet test tests/MyApp.Tests.Unit \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
# 통합 테스트 실행 (선택 사항, 커버리지에 추가)
dotnet test tests/MyApp.Tests.Integration \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
HTML 보고서 생성
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary;MarkdownSummaryGithub"
보고서 유형
| 유형 | 설명 | 출력 |
|---|---|---|
Html | 전체 대화형 보고서 | coverage/index.html |
TextSummary | 일반 텍스트 요약 | coverage/Summary.txt |
MarkdownSummaryGithub | GitHub 호환 마크다운 | coverage/SummaryGithub.md |
Badges | README용 SVG 뱃지 | coverage/badge_*.svg |
Cobertura | 병합된 Cobertura XML | coverage/Cobertura.xml |
보고서 읽기
Risk Hotspots 섹션
HTML 보고서에는 복잡도 순으로 정렬된 메서드를 보여주는 Risk Hotspots 섹션이 포함됩니다:
- 순환 복잡도(Cyclomatic Complexity): 코드를 통한 독립적인 경로 수 (if/else, switch case, 루프)
- NPath 복잡도: 비순환 실행 경로 수 (중첩에 따라 기하급수적으로 증가)
- CRAP 점수: 복잡도와 커버리지로 계산
결과 해석
Risk Hotspots
─────────────
메서드 복잡도 커버리지 CRAP 점수
───────────────────────────────────────────────────────────────
DataImporter.ParseRecord() 54 52% 12.4
AuthService.ValidateToken() 32 0% 32.0 ← 높은 위험
OrderProcessor.Calculate() 28 85% 1.3
UserService.CreateUser() 15 100% 0.0
조치 항목:
ValidateToken()CRAP > 30에 0% 커버리지 - 즉시 테스트 또는 리팩토링ParseRecord()는 복잡하지만 적절한 커버리지 있음 - 수용 가능CreateUser()와Calculate()는 잘 테스트됨 - 수정 안전
커버리지 임계값
권장 기준
| 커버리지 유형 | 목표 | 조치 |
|---|---|---|
| 라인 커버리지 | > 80% | 대부분의 프로젝트에 적합 |
| 분기 커버리지 | > 60% | 조건부 로직 검출 |
| CRAP 점수 | < 30 | 새 코드의 최대값 |
임계값 구성
리포지토리에 coverage.props를 생성하세요:
<Project>
<PropertyGroup>
<!-- CI 강제 적용을 위한 커버리지 임계값 -->
<CoverageThresholdLine>80</CoverageThresholdLine>
<CoverageThresholdBranch>60</CoverageThresholdBranch>
</PropertyGroup>
</Project>
CI/CD 통합
GitHub Actions
name: Coverage
on:
pull_request:
branches: [main, dev]
jobs:
coverage:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore tools
run: dotnet tool restore
- name: Run tests with coverage
run: |
dotnet test \
--settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults
- name: Generate report
run: |
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;MarkdownSummaryGithub;Cobertura"
- name: Upload coverage report
uses: actions/upload-artifact@v4
with:
name: coverage-report
path: coverage/
- name: Add coverage to PR
uses: marocchino/sticky-pull-request-comment@v2
with:
path: coverage/SummaryGithub.md
Azure Pipelines
- task: DotNetCoreCLI@2
displayName: '커버리지로 테스트 실행'
inputs:
command: 'test'
arguments: '--settings coverage.runsettings --collect:"XPlat Code Coverage" --results-directory $(Build.SourcesDirectory)/TestResults'
- task: DotNetCoreCLI@2
displayName: '커버리지 보고서 생성'
inputs:
command: 'custom'
custom: 'reportgenerator'
arguments: '-reports:"$(Build.SourcesDirectory)/TestResults/**/coverage.opencover.xml" -targetdir:"$(Build.SourcesDirectory)/coverage" -reporttypes:"HtmlInline_AzurePipelines;Cobertura"'
- task: PublishCodeCoverageResults@2
displayName: '커버리지 게시'
inputs:
codeCoverageTool: 'Cobertura'
summaryFileLocation: '$(Build.SourcesDirectory)/coverage/Cobertura.xml'
빠른 참조
한 줄 명령어
# 전체 분석 워크플로
rm -rf coverage/ TestResults/ && \
dotnet test --settings coverage.runsettings \
--collect:"XPlat Code Coverage" \
--results-directory ./TestResults && \
dotnet reportgenerator \
-reports:"TestResults/**/coverage.opencover.xml" \
-targetdir:"coverage" \
-reporttypes:"Html;TextSummary"
# 요약 보기
cat coverage/Summary.txt
# HTML 보고서 열기 (Linux)
xdg-open coverage/index.html
# HTML 보고서 열기 (macOS)
open coverage/index.html
# HTML 보고서 열기 (Windows)
start coverage/index.html
프로젝트 기준
| 메트릭 | 새 코드 | 레거시 코드 |
|---|---|---|
| 라인 커버리지 | 80%+ | 60%+ (점진적 개선) |
| 분기 커버리지 | 60%+ | 40%+ (점진적 개선) |
| 최대 CRAP | 30 | 예외 사항 문서화 |
| 고위험 메서드 | 테스트 필수 | 수정 전 테스트 추가 |
제외 대상
권장 coverage.runsettings에서 제외되는 항목:
| 패턴 | 이유 |
|---|---|
[*.Tests]* | 테스트 어셈블리는 프로덕션 코드가 아님 |
[*.Benchmark]* | 벤치마크 프로젝트 |
[*.Migrations]* | 데이터베이스 마이그레이션 (생성됨) |
GeneratedCodeAttribute | 소스 생성기 |
CompilerGeneratedAttribute | 컴파일러 생성 코드 |
ExcludeFromCodeCoverageAttribute | 개발자의 명시적 제외 |
*.g.cs, *.designer.cs | 생성된 파일 |
*.razor.g.cs | Blazor 컴포넌트 생성 코드 |
*.razor.css.g.cs | Blazor CSS 격리 생성 코드 |
**/Migrations/**/* | EF Core 마이그레이션 (자동 생성) |
SkipAutoProps | 자동 속성 (간단한 분기) |
임계값 업데이트 시기
일시적으로 임계값 낮추기:
- 현대화 중인 레거시 코드베이스 (README에 문서화)
- 수정할 수 없는 생성된 코드
- 서드파티 래퍼 코드
임계값을 절대 낮추지 말아야 할 때:
- "테스트하기 너무 어렵다" - 대신 리팩토링
- "나중에 테스트를 추가하겠다" - 지금 추가
- 새로운 기능 - 처음부터 기준을 충족해야 함
추가 자료
- Coverlet 문서: https://github.com/coverlet-coverage/coverlet
- ReportGenerator: https://github.com/danielpalme/ReportGenerator
- CRAP 점수 원본 논문: http://www.artima.com/weblogs/viewpost.jsp?thread=215899