코드 품질 — 정적 분석, 코드 커버리지, 기술 부채 측정
"코드 리뷰에서 매번 같은 실수가 반복되는데, 이걸 자동으로 잡을 수 없을까?"
코드 품질을 유지하려면 사람의 눈에만 의존하면 안 됩니다. 정적 분석 도구가 반복적인 실수를 잡아주고, 커버리지가 테스트 사각지대를 알려주고, 기술 부채 측정이 리팩토링 우선순위를 정해줍니다.
이게 뭔가요?
코드 품질 은 코드가 얼마나 읽기 쉽고, 유지보수하기 좋고, 버그가 적은지를 나타내는 지표입니다. 크게 세 가지 도구로 관리합니다:
- 정적 분석(Static Analysis): 코드를 실행하지 않고 소스 코드 자체를 분석해서 잠재적 버그, 코드 스멜, 보안 취약점을 찾아내는 도구
- 코드 커버리지(Code Coverage): 테스트가 전체 코드 중 얼마나 많은 부분을 실행하는지 측정하는 지표
- 기술 부채(Technical Debt): "지금 빠르게 만들기 위해 나중에 갚아야 할 비용"을 정량화하는 개념
왜 필요한가요?
사람의 리뷰만으로는 한계가 있습니다
// 이런 실수를 매번 눈으로 잡을 수 있을까?
public String getUserName(User user) {
return user.getName().trim(); // user가 null이면? getName()이 null이면?
}
정적 분석 도구는 이런 NullPointerException 가능성을 코드 실행 전에 자동으로 찾아줍니다.
테스트가 있다고 안심할 수 없습니다
테스트가 100개 있어도, 핵심 비즈니스 로직을 하나도 커버하지 않으면 의미가 없습니다. 커버리지는 "어디를 테스트하지 않았는가"를 보여줍니다.
부채를 모르면 갚을 수 없습니다
기술 부채가 어디에 얼마나 쌓여있는지 모르면, 리팩토링 우선순위를 정할 수 없습니다.
어떻게 동작하나요?
1. 정적 분석
정적 분석 도구는 코드를 파싱해서 규칙 기반으로 문제를 찾습니다.
주요 도구들:
| 도구 | 특징 | 분석 범위 |
|---|---|---|
| SonarQube | 종합 품질 플랫폼, 다국어 지원 | 버그, 코드 스멜, 보안, 중복 |
| ESLint/Checkstyle | 언어별 린터 | 코딩 컨벤션, 잠재 버그 |
| SpotBugs | Java 바이트코드 분석 | 버그 패턴 |
| GitHub Code Scanning | PR 기반 자동 분석 | 보안 취약점, 코드 품질 |
SonarQube가 잡아내는 것들:
// Bug: equals() 비교 시 null 체크 누락
if (name.equals("admin")) { ... }
// 권장: "admin".equals(name) 또는 Objects.equals()
// Code Smell: 메서드가 너무 긺 (인지 복잡도 초과)
public void processOrder(Order order) {
// 200줄짜리 메서드...
// → 메서드를 쪼개라는 제안
}
// Security Hotspot: 하드코딩된 비밀번호
String password = "admin123";
// → 환경 변수나 시크릿 매니저 사용 권장
CI 파이프라인에 통합:
# GitHub Actions 예시
- name: SonarQube Scan
uses: sonarsource/sonarqube-scan-action@v3
env:
SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }}
# Quality Gate 실패 시 PR 머지 차단
- name: Check Quality Gate
uses: sonarsource/sonarqube-quality-gate-action@v1
2. 코드 커버리지
커버리지는 테스트 실행 시 어떤 코드 라인이 실행되었는지를 추적합니다.
커버리지 유형:
public String classify(int score) {
if (score >= 90) { // Branch 1
return "A";
} else if (score >= 80) { // Branch 2
return "B";
} else { // Branch 3
return "C";
}
}
// 테스트: classify(95)만 작성했다면
// Line Coverage: 2/5 = 40% (if와 return "A"만 실행)
// Branch Coverage: 1/3 = 33% (Branch 1만 통과)
- 라인 커버리지: 전체 라인 중 실행된 라인의 비율
- 브랜치 커버리지: 전체 분기(if/else) 중 실행된 분기의 비율
- 조건 커버리지: 각 조건식의 true/false 경우를 모두 테스트했는지
Java에서 JaCoCo 설정:
// build.gradle
plugins {
id 'jacoco'
}
jacocoTestReport {
reports {
xml.required = true // SonarQube 연동용
html.required = true // 로컬 확인용
}
}
// 커버리지 최소 기준 설정
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.70 // 70% 미만이면 빌드 실패
}
}
}
}
3. 기술 부채 측정
기술 부채를 정량화하는 대표적인 방법:
SonarQube의 기술 부채 지표:
- SQALE Rating: A(최상) ~ E(최하) 등급
- Technical Debt Ratio: 수정에 필요한 시간 / 전체 개발 시간
- Remediation Cost: 수정에 필요한 예상 시간 (예: 3일 2시간)
기술 부채 사분면 (Martin Fowler):
의도적 비의도적
┌─────────────────┬─────────────────┐
│ "지금은 빠르게 │ "DDD를 몰랐을 │
신│ 만들고 나중에 │ 때 설계한 코드" │
중│ 리팩토링하자" │ │
├─────────────────┼─────────────────┤
│ "테스트 안 짜도 │ "이게 왜 안 │
경│ 괜찮아" │ 되지?" │
솔│ │ │
└─────────────────┴─────────────────┘
- 의도적 + 신중: 전략적 결정 (관리 가능)
- 비의도적 + 신중: 지식 부족으로 인한 부채 (학습으로 해결)
- 의도적 + 경솔: 무시한 부채 (위험)
- 비의도적 + 경솔: 모르고 만든 부채 (가장 위험)
자주 헷갈리는 포인트
커버리지 100%가 목표가 되면 안 됩니다
// 커버리지 100%를 만족하지만 의미 없는 테스트
@Test
void testGetter() {
User user = new User("홍길동");
assertEquals("홍길동", user.getName()); // getter만 테스트
}
Martin Fowler는 이렇게 말합니다: "커버리지가 너무 낮으면 문제지만, 높은 커버리지 자체가 테스트 품질을 보장하지는 않습니다." 핵심 비즈니스 로직의 분기와 엣지 케이스를 우선 커버하는 게 중요합니다.
정적 분석 경고를 모두 0으로 만들어야 하나요?
아닙니다. 중요한 건 새로운 코드에서 경고가 늘어나지 않게 하는 것입니다. SonarQube의 "Clean as You Code" 접근:
- 기존 코드: 점진적으로 개선
- 새 코드: 엄격한 기준 적용 (Quality Gate)
기술 부채를 숫자로 보고해도 될까요?
SonarQube의 "수정에 3일 필요"는 절대적 수치가 아니라 상대적 우선순위를 잡는 데 의미가 있습니다. 팀 내에서 "기술 부채 비율이 5%를 넘으면 스프린트에 리팩토링 태스크를 포함하자"처럼 기준을 잡는 게 효과적입니다.
정적 분석 vs 동적 분석
- 정적 분석: 코드를 실행하지 않고 소스 코드 자체를 분석 → 빠르지만 런타임 문제를 못 잡음
- 동적 분석: 코드를 실행하면서 분석 (프로파일링, 메모리 누수 탐지 등) → 실제 동작 기반이지만 느림
- 둘 다 필요합니다. 보통 CI에서 정적 분석, 스테이징에서 동적 분석을 돌립니다
정리
- 정적 분석은 코드를 실행하지 않고 잠재적 버그, 보안 취약점, 코드 스멜을 자동으로 찾아주는 도구
- 코드 커버리지는 테스트의 사각지대를 시각화해주지만, 높은 수치 자체가 목표가 되면 안 됨
- 기술 부채는 SonarQube 등으로 정량화할 수 있으며, 리팩토링 우선순위를 정하는 데 활용
- CI 파이프라인에 정적 분석과 커버리지 체크를 통합하면, 코드 리뷰 부담을 줄이고 일관된 품질을 유지 가능
- "Clean as You Code" 원칙: 새 코드에는 엄격하게, 기존 코드는 점진적으로 개선