"코드 리뷰에서 매번 같은 실수가 반복되는데, 이걸 자동으로 잡을 수 없을까?"

코드 품질을 유지하려면 사람의 눈에만 의존하면 안 됩니다. 정적 분석 도구가 반복적인 실수를 잡아주고, 커버리지가 테스트 사각지대를 알려주고, 기술 부채 측정이 리팩토링 우선순위를 정해줍니다.

이게 뭔가요?

코드 품질 은 코드가 얼마나 읽기 쉽고, 유지보수하기 좋고, 버그가 적은지를 나타내는 지표입니다. 크게 세 가지 도구로 관리합니다:

  • 정적 분석(Static Analysis): 코드를 실행하지 않고 소스 코드 자체를 분석해서 잠재적 버그, 코드 스멜, 보안 취약점을 찾아내는 도구
  • 코드 커버리지(Code Coverage): 테스트가 전체 코드 중 얼마나 많은 부분을 실행하는지 측정하는 지표
  • 기술 부채(Technical Debt): "지금 빠르게 만들기 위해 나중에 갚아야 할 비용"을 정량화하는 개념

왜 필요한가요?

사람의 리뷰만으로는 한계가 있습니다

JAVA
// 이런 실수를 매번 눈으로 잡을 수 있을까?
public String getUserName(User user) {
    return user.getName().trim();  // user가 null이면? getName()이 null이면?
}

정적 분석 도구는 이런 NullPointerException 가능성을 코드 실행 전에 자동으로 찾아줍니다.

테스트가 있다고 안심할 수 없습니다

테스트가 100개 있어도, 핵심 비즈니스 로직을 하나도 커버하지 않으면 의미가 없습니다. 커버리지는 "어디를 테스트하지 않았는가"를 보여줍니다.

부채를 모르면 갚을 수 없습니다

기술 부채가 어디에 얼마나 쌓여있는지 모르면, 리팩토링 우선순위를 정할 수 없습니다.

어떻게 동작하나요?

1. 정적 분석

정적 분석 도구는 코드를 파싱해서 규칙 기반으로 문제를 찾습니다.

주요 도구들:

도구특징분석 범위
SonarQube종합 품질 플랫폼, 다국어 지원버그, 코드 스멜, 보안, 중복
ESLint/Checkstyle언어별 린터코딩 컨벤션, 잠재 버그
SpotBugsJava 바이트코드 분석버그 패턴
GitHub Code ScanningPR 기반 자동 분석보안 취약점, 코드 품질

SonarQube가 잡아내는 것들:

JAVA
// 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 파이프라인에 통합:

YAML
# 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. 코드 커버리지

커버리지는 테스트 실행 시 어떤 코드 라인이 실행되었는지를 추적합니다.

커버리지 유형:

JAVA
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 설정:

GROOVY
// 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):

PLAINTEXT
            의도적              비의도적
  ┌─────────────────┬─────────────────┐
  │ "지금은 빠르게   │ "DDD를 몰랐을   │
신│  만들고 나중에   │  때 설계한 코드" │
중│  리팩토링하자"   │                 │
  ├─────────────────┼─────────────────┤
  │ "테스트 안 짜도  │ "이게 왜 안      │
경│  괜찮아"         │  되지?"          │
솔│                  │                 │
  └─────────────────┴─────────────────┘
  • 의도적 + 신중: 전략적 결정 (관리 가능)
  • 비의도적 + 신중: 지식 부족으로 인한 부채 (학습으로 해결)
  • 의도적 + 경솔: 무시한 부채 (위험)
  • 비의도적 + 경솔: 모르고 만든 부채 (가장 위험)

자주 헷갈리는 포인트

커버리지 100%가 목표가 되면 안 됩니다

JAVA
// 커버리지 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" 원칙: 새 코드에는 엄격하게, 기존 코드는 점진적으로 개선

References

댓글 로딩 중...