C++ Static Analysis | "정적 분석" 가이드

C++ Static Analysis | "정적 분석" 가이드

이 글의 핵심

C++ Static Analysis에 대한 실전 가이드입니다.

들어가며

정적 분석(Static Analysis)은 코드를 실행하지 않고 분석하여 버그, 코드 스멜, 보안 취약점을 찾는 강력한 도구입니다. Clang-Tidy, Cppcheck 등을 활용하면 코드 품질을 크게 향상시킬 수 있습니다.


1. Clang-Tidy

설치 및 기본 사용

# Ubuntu/Debian
sudo apt-get install clang-tidy

# macOS
brew install llvm

# 기본 실행
clang-tidy program.cpp -- -std=c++17

# 자동 수정
clang-tidy -fix program.cpp -- -std=c++17

# 특정 검사만
clang-tidy -checks='modernize-*' program.cpp -- -std=c++17

예제 코드 분석

정적 분석 도구가 어떤 문제를 찾아내는지 실제 예제로 확인해봅시다:

// bad_code.cpp
#include <iostream>
#include <vector>

void processData() {
    // 문제 1: nullptr 역참조 (Null Pointer Dereference)
    int* ptr = nullptr;
    *ptr = 42;  // 크래시 발생! nullptr을 역참조하면 세그멘테이션 폴트
    
    // 문제 2: 배열 범위 초과 (Buffer Overflow)
    int arr[10];  // 인덱스 0~9만 유효
    for (int i = 0; i <= 10; i++) {  // i=10일 때 범위 초과!
        arr[i] = i;  // 미정의 동작 (Undefined Behavior)
        // 메모리 오염, 크래시, 보안 취약점 발생 가능
    }
    
    // 문제 3: 메모리 누수 (Memory Leak)
    int* data = new int(100);
    // delete data;를 하지 않음
    // 함수가 끝나도 메모리가 해제되지 않아 누수 발생
    
    // 문제 4: 사용하지 않는 변수 (Dead Code)
    int unused = 42;
    // 선언만 하고 사용하지 않음 → 코드 낭비
    
    // 문제 5: 비효율적 복사 (Unnecessary Copy)
    std::vector<int> vec = {1, 2, 3};
    for (int x : vec) {  // 매 반복마다 요소를 복사
        std::cout << x << std::endl;
    }
    // 올바른 방법: for (const int& x : vec) 또는 for (int x : vec) (작은 타입)
}

Clang-Tidy 실행 결과:

$ clang-tidy bad_code.cpp -- -std=c++17

# 경고 1: nullptr 역참조 감지
bad_code.cpp:7:5: warning: Dereference of null pointer [clang-analyzer-core.NullDereference]
    *ptr = 42;
    ^
# 설명: ptr이 nullptr인데 *ptr로 접근하면 크래시 발생

# 경고 2: 배열 범위 초과 감지
bad_code.cpp:11:23: warning: Value stored to 'i' is never read [clang-analyzer-deadcode.DeadStores]
    for (int i = 0; i <= 10; i++) {
                      ^
# 설명: i가 10일 때 arr[10]은 범위를 벗어남 (arr[0]~arr[9]만 유효)

# 경고 3: 메모리 누수 감지
bad_code.cpp:16:16: warning: Potential memory leak [clang-analyzer-cplusplus.NewDeleteLeaks]
    int* data = new int(100);
               ^
# 설명: new로 할당한 메모리를 delete하지 않음

# 경고 4: 사용하지 않는 변수 감지
bad_code.cpp:19:9: warning: unused variable 'unused' [clang-diagnostic-unused-variable]
    int unused = 42;
        ^
# 설명: 변수를 선언했지만 사용하지 않음

# 경고 5: 비효율적 복사 감지
bad_code.cpp:23:14: warning: loop variable is copied but only used as const reference [performance-for-range-copy]
    for (int x : vec) {
             ^
# 설명: 매 반복마다 요소를 복사하는 대신 const 참조를 사용하면 성능 향상
# 제안: for (const int& x : vec) 또는 for (int x : vec) (int는 작아서 복사가 빠름)

정적 분석의 가치:

  • 런타임에 발생할 버그를 컴파일 타임에 미리 발견
  • 코드 리뷰 전에 기본적인 문제 해결
  • 보안 취약점 조기 발견

2. .clang-tidy 설정

기본 설정 파일

# .clang-tidy
Checks: >
  -*,
  bugprone-*,
  modernize-*,
  performance-*,
  readability-*,
  -modernize-use-trailing-return-type

CheckOptions:
  - key: readability-identifier-naming.ClassCase
    value: CamelCase
  - key: readability-identifier-naming.FunctionCase
    value: camelBack
  - key: readability-identifier-naming.VariableCase
    value: camelBack
  - key: readability-identifier-naming.ConstantCase
    value: UPPER_CASE

검사 카테고리

# 버그 탐지
bugprone-*

# 모던 C++ 변환
modernize-*
  - modernize-use-nullptr
  - modernize-use-auto
  - modernize-use-override
  - modernize-loop-convert

# 성능 개선
performance-*
  - performance-move-const-arg
  - performance-unnecessary-copy-initialization
  - performance-for-range-copy

# 가독성
readability-*
  - readability-identifier-naming
  - readability-magic-numbers
  - readability-braces-around-statements

3. Cppcheck

설치 및 기본 사용

# Ubuntu/Debian
sudo apt-get install cppcheck

# macOS
brew install cppcheck

# 기본 실행
cppcheck program.cpp

# 모든 검사 활성화
cppcheck --enable=all program.cpp

# 특정 검사
cppcheck --enable=warning,performance program.cpp

# XML 리포트
cppcheck --xml program.cpp 2> report.xml

# 디렉토리 전체 검사
cppcheck --enable=all src/

예제

// test.cpp
#include <iostream>
#include <vector>

void processArray() {
    int arr[10];
    
    // 문제: 초기화되지 않은 배열
    for (int i = 0; i < 10; i++) {
        std::cout << arr[i] << std::endl;
    }
}

void processVector() {
    std::vector<int> vec;
    
    // 문제: 범위 초과
    std::cout << vec[0] << std::endl;
}

Cppcheck 실행:

$ cppcheck --enable=all test.cpp

[test.cpp:9]: (error) Uninitialized variable: arr
[test.cpp:17]: (error) Out of bounds access in expression 'vec[0]'

4. 실전 예제: CI/CD 통합

GitHub Actions

# .github/workflows/static-analysis.yml
name: Static Analysis

on: [push, pull_request]

jobs:
  clang-tidy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Clang-Tidy
        run: |
          sudo apt-get update
          sudo apt-get install -y clang-tidy
      
      - name: Run Clang-Tidy
        run: |
          find src -name "*.cpp" -exec clang-tidy {} -- -std=c++17 \;
  
  cppcheck:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      
      - name: Install Cppcheck
        run: sudo apt-get install -y cppcheck
      
      - name: Run Cppcheck
        run: |
          cppcheck --enable=all --error-exitcode=1 src/

CMake 통합

# CMakeLists.txt
cmake_minimum_required(VERSION 3.15)
project(MyProject)

set(CMAKE_CXX_STANDARD 17)

# Clang-Tidy 통합
set(CMAKE_CXX_CLANG_TIDY 
    clang-tidy;
    -checks=bugprone-*,modernize-*,performance-*;
    -header-filter=.*
)

add_executable(myapp main.cpp)

5. 자주 발생하는 문제

문제 1: 거짓 양성 (False Positive)

// 정적 분석 경고 (거짓 양성)
int* getStaticPointer() {
    static int value = 10;
    return &value;  // 경고: 지역 변수 주소 반환
}
// 실제로는 안전 (static 변수)

// 억제 방법
// NOLINTNEXTLINE(clang-analyzer-core.StackAddressEscape)
int* getStaticPointer() {
    static int value = 10;
    return &value;
}

문제 2: 너무 많은 경고

# ❌ 모든 검사 (압도적)
clang-tidy -checks=* program.cpp

# ✅ 점진적 적용
# 1단계: 버그만
clang-tidy -checks=bugprone-* program.cpp

# 2단계: 모던화
clang-tidy -checks=bugprone-*,modernize-* program.cpp

# 3단계: 성능
clang-tidy -checks=bugprone-*,modernize-*,performance-* program.cpp

문제 3: 레거시 코드

// 레거시 코드에 정적 분석 적용 전략:
// 1. 새 코드부터 적용
// 2. 중요한 모듈부터
// 3. 우선순위 설정 (버그 > 성능 > 스타일)
// 4. 점진적 개선

문제 4: 성능

# ❌ 느린 분석
clang-tidy -checks=* large_project/**/*.cpp

# ✅ 병렬 실행
find src -name "*.cpp" | xargs -P 8 -I {} clang-tidy {} -- -std=c++17

# ✅ 변경된 파일만
git diff --name-only --diff-filter=AM | grep '\.cpp$' | xargs clang-tidy

6. 정적 분석 도구 비교

도구장점단점용도
Clang-Tidy강력, 자동 수정, 확장 가능느림, Clang 의존주 도구
Cppcheck빠름, 독립적, 간단검출률 낮음보조 도구
PVS-Studio정확, 상세 리포트상용 (비쌈)상용 프로젝트
SonarQube웹 기반, 팀 협업, 다양한 언어설정 복잡대규모 팀
Coverity정확, 엔터프라이즈상용, 느림엔터프라이즈

정리

핵심 요약

  1. 정적 분석: 코드 실행 없이 버그 탐지
  2. Clang-Tidy: 가장 강력한 도구
  3. Cppcheck: 빠르고 간단한 보조 도구
  4. CI/CD 통합: 자동화로 품질 보장
  5. 점진적 적용: 레거시 코드는 우선순위 설정
  6. 거짓 양성: 억제 주석으로 관리

정적 분석 워크플로우

코드 작성

로컬에서 Clang-Tidy 실행

문제 수정 또는 억제

커밋

CI/CD에서 자동 검사

리포트 확인

코드 리뷰

실전 팁

도구 선택:

  • 주 도구: Clang-Tidy (강력, 자동 수정)
  • 보조 도구: Cppcheck (빠름, 간단)
  • 상용: PVS-Studio, Coverity (정확)

적용 전략:

  • 새 프로젝트: 처음부터 적용
  • 레거시: 점진적 적용 (새 코드부터)
  • 우선순위: 버그 > 성능 > 스타일

성능 최적화:

  • 병렬 실행 (xargs -P)
  • 변경된 파일만 검사
  • 필요한 검사만 활성화

다음 단계

  • C++ Code Coverage
  • C++ Profiling
  • C++ Clang-Tidy & Cppcheck

관련 글

  • C++ 정적 분석 도구 통합: Clang-Tidy와 Cppcheck로 코드 퀄리티 강제하기 [#41-1]
  • C++ 정적 분석 도구 | Clang-Tidy·Cppcheck·SonarQube [#53-5]
  • C++ 코드 커버리지 완벽 가이드 | gcov, lcov, Codecov 실전 활용
  • C++ CLion 완벽 설정 가이드 | CMake·디버거·코드 분석·vcpkg·원격 개발 [#53-1]
  • 배열과 리스트 | 코딩 테스트 필수 자료구조 완벽 정리