본문으로 건너뛰기 C++ Static Analysis | '정적 분석' 가이드

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

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

이 글의 핵심

C++ Static Analysis: "정적 분석" 가이드. Clang-Tidy·.clang-tidy 설정.

들어가며

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


1. Clang-Tidy

Clang-Tidy는 LLVM/Clang 프로젝트가 제공하는 정적 분석 도구로, 단순히 버그를 찾는 것을 넘어 코드를 자동으로 고쳐주는 -fix 옵션까지 지원한다는 점이 가장 큰 특징입니다. Clang의 완전한 구문 트리(AST)를 그대로 활용하기 때문에 검사의 정확도가 높고, modernize-* 계열 검사를 통해 낡은 C++ 코드를 최신 문법으로 바꾸는 마이그레이션 작업에도 자주 쓰입니다.

설치 및 기본 사용

Clang-Tidy는 대부분의 패키지 매니저에서 바로 설치할 수 있으며, 기본 실행은 분석할 소스 파일 뒤에 --로 컴파일 옵션을 구분해서 넘겨주는 방식입니다. -fix 옵션을 붙이면 단순 경고에 그치지 않고 코드 자체를 즉시 수정해 주므로, 처음 도입할 때는 반드시 버전 관리 시스템에 커밋된 상태에서 실행해 변경 사항을 리뷰할 수 있도록 해야 합니다.

# 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) (작은 타입)
}

위 코드에는 nullptr 역참조, 배열 범위 초과, 메모리 누수, 미사용 변수, 불필요한 복사라는 다섯 가지 전형적인 문제가 섞여 있습니다. 사람이 코드 리뷰만으로 이 모두를 매번 놓치지 않고 잡아내기는 현실적으로 어렵지만, Clang-Tidy는 각 문제에 대해 정확한 위치와 원인을 짚어주는 경고를 아래처럼 출력합니다.

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

매번 명령줄에서 -checks 옵션을 길게 나열하는 대신, 프로젝트 루트에 .clang-tidy 설정 파일을 두면 팀 전체가 동일한 검사 규칙을 공유할 수 있습니다. 이 파일을 버전 관리에 포함시켜 두면 새로 합류한 팀원도 별도 설정 없이 같은 기준으로 코드를 검사받게 되어, 코드 스타일과 품질 기준이 자연스럽게 통일됩니다.

기본 설정 파일

Checks 필드는 활성화할 검사 카테고리를 지정하는데, 맨 앞의 -*는 모든 기본 검사를 먼저 끈 뒤 이어지는 항목들을 화이트리스트 방식으로 다시 켜는 관용적인 패턴입니다. CheckOptions에서는 클래스명은 파스칼 케이스, 함수와 변수명은 카멜 케이스, 상수는 대문자 스네이크 케이스로 강제하는 등 팀의 네이밍 컨벤션을 코드 리뷰가 아닌 도구 차원에서 자동으로 검증하도록 만들 수 있습니다.

# .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

검사 카테고리

Clang-Tidy의 검사는 크게 네 계열로 나뉘며, 각 계열은 서로 다른 종류의 문제를 대상으로 합니다. bugprone-*는 실제 버그로 이어질 가능성이 높은 패턴을, modernize-*는 C++11 이후에 도입된 더 안전하고 간결한 문법으로의 전환을, performance-*는 불필요한 복사나 이동 누락 같은 성능 저하 요인을, readability-*는 매직 넘버나 중괄호 생략처럼 가독성을 해치는 패턴을 각각 찾아냅니다. 프로젝트 성격에 따라 이 중 일부만 선택적으로 활성화하는 것이 일반적입니다.

# 버그 탐지
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

Cppcheck는 Clang-Tidy와 달리 Clang 컴파일러 인프라에 의존하지 않는 독립적인 정적 분석기로, 컴파일이 완전히 되지 않는 코드나 빌드 시스템이 복잡한 프로젝트에서도 비교적 가볍게 실행할 수 있다는 장점이 있습니다. 검출률은 Clang-Tidy보다 낮은 편이지만 실행 속도가 빠르고 오탐(false positive)이 적어, CI 파이프라인의 첫 번째 관문이나 Clang-Tidy의 보조 도구로 함께 쓰는 경우가 많습니다.

설치 및 기본 사용

--enable=all로 모든 검사를 활성화하면 가장 넓은 범위의 문제를 잡아낼 수 있지만, 그만큼 실행 시간도 길어지고 결과도 많아지므로 처음에는 warning, performance처럼 필요한 카테고리만 지정하는 것이 다루기 쉽습니다. --xml 옵션으로 결과를 리포트 파일로 저장해두면, 이후 CI 대시보드나 코드 품질 관리 도구에서 이 리포트를 파싱해 추이를 추적하는 데도 활용할 수 있습니다.

# 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 통합

정적 분석 도구를 로컬 개발자 PC에서만 실행하면, 개인마다 실행 여부와 검사 강도가 제각각이 되어 결국 일부 코드는 검증을 거치지 않고 병합될 위험이 생깁니다. CI/CD 파이프라인에 정적 분석을 통합해 두면 모든 풀 리퀘스트가 동일한 기준을 통과해야만 병합될 수 있으므로, 코드 품질을 팀 전체의 규율이 아니라 자동화된 게이트로 강제할 수 있습니다.

GitHub Actions

아래 워크플로는 Clang-Tidy와 Cppcheck를 각각 별도의 잡(job)으로 병렬 실행하도록 구성한 예시입니다. error-exitcode=1 옵션을 준 Cppcheck 잡은 문제가 하나라도 발견되면 빌드 자체를 실패 처리하므로, 검사를 통과하지 못한 커밋은 자동으로 병합이 차단됩니다.

# .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 통합

별도의 스크립트나 CI 설정 없이도, CMake는 CMAKE_CXX_CLANG_TIDY 변수를 설정해두는 것만으로 빌드할 때마다 자동으로 Clang-Tidy를 함께 실행하도록 지원합니다. 이렇게 하면 개발자가 로컬에서 평소처럼 빌드 명령을 실행하기만 해도 정적 분석 경고가 컴파일 경고와 나란히 출력되므로, 별도의 도구 실행을 잊어버려 검사를 건너뛰는 일이 줄어듭니다.

# 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)

정적 분석기는 코드를 실행하지 않고 정적으로 흐름을 추론하기 때문에, static 변수처럼 프로그램 전체 수명 동안 유효한 특수한 경우를 항상 정확히 인식하지는 못합니다. 아래 getStaticPointer()는 지역 변수처럼 보이는 value가 실제로는 static이라 함수가 반환된 뒤에도 계속 유효한데, 분석기는 이를 “지역 변수 주소를 반환하는 위험한 패턴”으로 오탐할 수 있습니다. 이런 경우 NOLINTNEXTLINE 주석으로 해당 줄의 특정 경고만 콕 집어 억제하면, 다른 검사는 그대로 유지하면서 거짓 양성만 조용히 만들 수 있습니다.

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

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

문제 2: 너무 많은 경고

기존 코드베이스에 모든 검사(-checks=*)를 한꺼번에 켜서 실행하면, 수백에서 수천 개의 경고가 쏟아져 나와 팀원들이 압도당한 나머지 정적 분석 자체를 외면하게 되는 역효과가 나기 쉽습니다. 버그 탐지 검사부터 시작해 모던화, 성능 검사 순으로 범위를 단계적으로 넓혀가면, 매 단계에서 발생하는 경고 수를 감당할 수 있는 수준으로 유지하면서 팀이 도구에 적응할 시간을 벌 수 있습니다.

# ❌ 모든 검사 (압도적)
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: 성능

대규모 프로젝트에서 모든 파일에 대해 모든 검사를 순차적으로 실행하면 분석 시간이 수 분에서 수십 분까지 늘어나 CI 파이프라인의 병목이 되기 쉽습니다. xargs -P로 여러 파일을 병렬로 분석하거나, git diff로 이번 커밋에서 실제로 변경된 파일만 골라 검사하면 전체 분석 시간을 크게 줄이면서도 검사의 실효성은 그대로 유지할 수 있습니다.

# ❌ 느린 분석
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와 Cppcheck 외에도 다양한 정적 분석 도구가 존재하며, 각각 강력함·속도·비용 사이에서 서로 다른 지점을 택하고 있습니다. 오픈소스 도구로 기본기를 다진 뒤, 규제가 엄격한 산업군이나 대규모 팀이라면 상용 도구 도입을 검토하는 것이 일반적인 흐름입니다.

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

정리

정적 분석은 “코드 리뷰어가 놓치기 쉬운 것을 도구가 대신 잡아준다”는 단순한 아이디어에서 출발하지만, 도구 선택과 도입 전략에 따라 실무에서 체감하는 효과는 크게 달라집니다. 아래 요약과 워크플로우로 지금까지의 내용을 정리합니다.

핵심 요약

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

정적 분석 워크플로우

정적 분석을 팀에 안정적으로 정착시키려면 로컬 개발 환경과 CI 파이프라인 두 곳 모두에 검사를 배치하는 것이 이상적입니다. 아래 흐름처럼 로컬에서 한 번 걸러낸 뒤 CI에서 다시 한번 검증하면, 개발자는 빠른 피드백을 받고 팀은 병합 전 최종 안전망을 확보하는 이중 구조를 만들 수 있습니다.

코드 작성

로컬에서 Clang-Tidy 실행

문제 수정 또는 억제

커밋

CI/CD에서 자동 검사

리포트 확인

코드 리뷰

실전 팁

도구 선택:

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

적용 전략:

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

성능 최적화:

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

다음 단계


관련 글


자주 묻는 질문 (FAQ)

Q. 이 내용을 실무에서 언제 쓰나요?

A. Everything about C++ Static Analysis : from basic concepts to practical applications.

Q. 선행으로 읽으면 좋은 글은?

A. 각 글 하단의 이전 글 또는 관련 글 링크를 따라가면 순서대로 배울 수 있습니다. C++ 시리즈 목차에서 전체 흐름을 확인할 수 있습니다.

Q. 더 깊이 공부하려면?

A. cppreference와 해당 라이브러리 공식 문서를 참고하세요. 글 말미의 참고 자료 링크도 활용하면 좋습니다.


같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.


이 글에서 다루는 키워드 (관련 검색어)

C++, static-analysis, clang-tidy, cppcheck, quality 등으로 검색하시면 이 글이 도움이 됩니다.