C++ 정적 코드 분석하기 - clang-tidy, cppcheck, 컴파일 옵션

정적 코드 분석

이전 Software Engineering at Google 리뷰 글에서, 구글에서는 소프트웨어의 취약점을 최대한 빠르게 발견하기 위해서 다층 방어 전략을 구사한다고 했었고, 그중 하나가 정적 코드 분석이라고 했었다.

그렇다면 이 정적 코드 분석이란 무엇일까? 소프트웨어의 취약점, 버그는 테스트와 실행 과정에서 발견될 수도 있지만, 사실 대부분의 버그들은 코드 단계에서 발견될 수도 있다. nullptr 관련 문제들, 잘못된 코딩 습관으로 인한 버그들, 메모리 누수 관련 문제들은 대부분 사실 실행 단계가 아닌, 코드를 유심히 점검하는 과정에서 해결할 수 있다.

그리고 이러한 코드 분석을 정적 코드 분석(Static Code Analysis)라 한다.

그러나 사람은 기본적으로 자신이 만들고 있는 실수를 스스로 찾아내기 힘들다. 그렇기 때문에 코드를 아무리 잘 작성한다 하더라도 그 안에 취약점은 있기 마련이고, 이 취약점을 사람이 수작업으로 발견하는 것 또한 어렵다.

그렇기 떄문에 우리는 정적 코드 분석 과정을 도구의 힘을 빌려 수행해야 한다.

사실 현대적인 언어들은 모두 기본적인 정적 코드 분석 기능을 탑재하고 있다. dart/flutter의 flutter-lint, Go 언어의 gofmt 같은 기본적인 정적 분석 툴들이 그 예시이다.

이번 글에서는 C++의 정적 분석 툴에 대해서 알아볼 건데, 언어와 도구만 달라질 뿐 다른 언어들 역시 이 정적 분석을 적용하는 과정은 크게 다르지 않을 것이다.

C++은 애석하게도 자체적인 정적 분석 툴은 가지고 있지 않다. 하지만 gcc, msvc, clang 등의 컴파일러들이 각각 컴파일 옵션을 통해서 정적 분석 기능을 수행하기도 하고, 사실상 표준화된 외부 정적 분석 툴을 사용할 수 있기도 하다.

특히 C++은 잘못된 프로그래밍을 하게 되면 메모리 누수, 혹은 nullptr 이슈, 오브젝트의 라이프 사이클 관리 문제에 직면하게 되는 언어이기 때문에, 이러한 정적 분석에 더 세심하게 신경을 써야 한다. 그래서 보통은 하나의 방법만 사용하는 게 아니라 여러 방법을 섞어서 사용하는 경우가 많다. 하나씩 알아보도록 하자.

정적 분석을 하는 데 있어서 주의사항

정적 분석 툴을 사용하는데 있어 가장 중요한 원칙 하나가 있다. 바로 모든 경고는 에러로 받아들일 것이라는 원칙이다. 즉 정적 분석 단계에서 발견된 모든 경고는, 단 하나의 경고도 빠짐없이 수정하고 가야 한다. 이 원칙이 무시된다면, 점차 프로그래머들의 게으름으로 인해 하나둘씩 경고가 쌓이게 될 것이고, 결국 이러한 경고들은 아무 의미가 없어지고 말 것이다.

즉 정적 분석을 진행하는 데 있어 가장 중요한 원칙은 바로, 단 하나의 경고도 빠짐없이 수정하고 가야 한다는 것이다.

당연하겠지만 특별한 이유 없이 이 경고를 disable 처리하는 것 또한 금지되어야 한다.

C++에서 정적 분석을 해보자

Compiler Warning Flag Option 사용하기

C++에서 사실 가장 쉬운 정적 분석 방법은, 기본 제공되는 컴파일러의 경고 관련 flag 옵션을 켜주는 것이다. 의외로 많은 버그들, 특히 nullptr 이슈나 bad-access(잘못된 메모리 주소에 대한 접근), 메모리 누수 등의 문제들은 컴파일러의 옵션을 켜주는 것만으로도 대부분 해결할 수 있다.

컴파일러 옵션은 다음과 같이 켜줄 수 있다. 예를 들어서 다음 main.cc 파일을 컴파일하려고 한다 해보자.

// main.cc
#include <iostream>

int main() {
    int variable = 10;
    int ptr[3] = { 1, 2, 3 };

    std::cout << "Hello World!" << std::endl;
    std::cout << ptr[4] << std::endl;

    return 0;
}

위 코드는 문법상의 문제는 없다. 그러나 variable이라는 변수가 사용되고 있지 않음을 알 수 있다. 더 나아가서, ptr 배열의 존재하지도 않는 4번째 원소에 접근하는, bad access를 일으키고 있음 역시 알 수 있다.

한 번 이 프로그램을 컴파일러를 통해서 컴파일해 보자.

$ g++ main.cc
$ ls
a.out main.cc

코드 자체에 문제가 있음에도 불구하고, 아무 문제 없이 정상적으로 컴파일이 완료되었음을 알 수 있다. 이는 코드 자체에는 사실 아무 문법상의 문제가 없기 때문이다.

물론 만약에 ./a.out을 통해서 바이너리 파일을 실행한다면, 이 코드는 segmentation fault를 일으키며 종료되거나, 더 최악은 undefined behavior로 잘못된 가비지 값을 사용하게 될 것이다.

우리가 원하는 것은 컴파일 단계에서 문제점을 발견하는 것이다. 컴파일 옵션을 통해서, 컴파일러가 스스로 코드의 취약점을 진단하고 우리에게 알려주도록 해보자.

$ g++ main.cc -Wall -Wextra -Werror

-Wall -Wextra -Werror 이라는 옵션을 주었더니, 컴파일이 되지 않고 아래와 같은 메시지가 발생한다.

main.cc:4:6: error: unused variable 'variable' [-Werror,-Wunused-variable]
        int variable = 10;
            ^
main.cc:8:15: error: array index 4 is past the end of the array (which contains 3 elements) [-Werror,-Warray-bounds]
        std::cout << ptr[4] << std::endl;
                     ^   ~
main.cc:5:2: note: array 'ptr' declared here
        int ptr[3] = { 1, 2, 3 };
        ^
2 errors generated.

위 코드가 갖고 있는 문제점, 즉 unused variable 문제와 bad access 문제를 컴파일 단계에서 발견되었음을 알 수 있다. 프로그래머들은 이 경고를 통해서 코드를 수정할 수 있게 된다.

// main.cc
#include <iostream>

int main() {
    int ptr[3] = { 1, 2, 3 };

    std::cout << "Hello World!" << std::endl;
    std::cout << ptr[2] << std::endl;

    return 0;
}
$ g++ main.cc -Wall -Wextra -Werror
$ ls
a.out main.cc
$ ./a.out
Hello World!
3

위와 같이 문제를 수정하고 나면, 컴파일이 정상적으로 완료되고 있음을 알 수 있다.

컴파일러 옵션을 간단히 살펴보자.

  • Wall : 모든 경고를 활성화한다.
  • Wextra : -Wall에 추가적인 경고를 활성화한다.
  • Werror : 경고를 에러로 취급한다. 즉 경고가 발생하면 컴파일이 실패한다.

주목해야 할 점은 -Werror이다. 정적 코드 분석의 목적은 프로그래머가 이를 컴파일 단계에서 바로 수정하도록 하는 것이다. 즉 프로그래머가 이 경고를 무시하고 넘어가면 안 된다. 경고를 에러로 받아들여야 한다. -Werror 옵션은 경고를 에러로 취급해서, 컴파일러가 취약점을 발견하면 아예 컴파일조차 안 되게 한다. 무조건 문제를 해결해야 컴파일을 할 수 있다.

이처럼 정적 분석의 핵심은 엄격함이라는 점을 기억하자.

사실, 위 세 개의 컴파일 옵션만 가지고서는 잡아내지 못하는 취약점들이 많다. 사실 더 많은 옵션들이 존재한다. 심지어, gcc, msvc, clang 같은 각기 다른 컴파일러가 지원하는 별도의 옵션도 존재한다!

여기 gcc의 compiler warning flag를 살펴보면, 정말 많은 옵션들이 있고, 다시 말해 정말 많은 취약점들이 컴파일러 옵션을 켜주는 것만으로도 사전에 발견될 수 있었음을 알 수 있다.

여기서 이런 의문이 들 수 있다. "경고 flag 관련 컴파일 옵션이 이렇게나 많은데, 나는 그중 무엇을 사용하면 되지?"

프로젝트에 따라, 팀의 정책에 따라 달라지겠지만, 보통 이 답은 의외로 간단하다. 사용 가능한 모든 경고 flag 옵션들을 전부 켜주면 된다. 그냥 모든 옵션들을 켜주면, 고민할 필요 없이 정적 분석의 혜택을 누릴 수 있게 된다.

더불어 서로 다른 컴파일러가 서로 다른 컴파일 옵션들을 별도로 지원하는 경우도 있다고 했다. 이 때문에 clang에서는 통과된 코드가 gcc에서는 실패하는 경우도, 그 반대로 존재한다.

이 때문에, C++의 코드를 빌드할 때는 두 개 이상의 컴파일러를 통해서 빌드하는 것이 좋다. 이를 통해 서로 다른 컴파일러에서 발견되는 취약점들을 모두 잡아낼 수 있기 때문이다.

예컨대 CMake를 활용한다면, 다음과 같은 옵션을 주는 것이 좋은 방법이다. (내가 C++을 활용할 때 항상 아래 옵션들을 복붙하고 사용한다.)

add_compile_options(-Werror -Wall -Wextra -Wpedantic -Wconversion -Wsign-conversion -Wshadow -Wundef -Wunreachable-code -Wstrict-aliasing -Wnull-dereference -Wdouble-promotion -Wformat=2 -Wcast-qual -Wcast-align)

if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "GNU")
    # Additional flags for GCC
    add_compile_options(-Wuseless-cast)
elseif ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
    # Additional flags for Clang
    add_compile_options(-Weverything -Wno-c++98-compat -Wno-c++98-compat-pedantic -Wno-unused-macros -Wno-padded)
endif ()

CMake에서 빌드를 수행할 때, 위와 같이 컴파일러 옵션을 줄 수 있다. 이때 공용으로 사용되는 -Werror, -Wall, -Wextra, -Wpedantic, -Wconversion ... 등부터 Clang에서만 지원하는 옵션들(-Weverything, ... 등), GCC에서만 지원하는 옵션들(-Wuseless-cast, ... 등)까지 모두 주었다.

이렇게 하면, 컴파일러가 직접 발견 가능한 거의 모든 취약점들을 발견하게 할 수 있고, clang에서 놓친 경고를 gcc에서 발견하기도 한다.

이게 이렇게까지 해야 하나 싶을 수도 있겠지만, 실제로 본인 역시 GCC와 Clang을 통한 크로스 컴파일 과정을 통해서, 취약점까지는 아니었지만 불필요한 행동들을 잡아낸 적이 두 번 있다. 두 번 정도의 경험이면 그 필요성을 인식하기에 충분했다. 크로스 컴파일은 중요하다는 것을.

Clang-tidy

[Fig 1. LLVM Logo] Clang-tidy는 로고가 따로 없지만, 대신 간지나는 LLVM의 로고가 있다

사실 컴파일러 옵션을 켜주는 것만으로는, 우리가 원하는 만큼 충분한 성능을 발휘해 주지 못한다. 그렇기 때문에 컴파일 옵션과 더불어서 외부 정적 분석 툴을 적극적으로 사용한다.

외부 분석 툴은 다양하게 있는데, 그중 Clang-tidy와 cppcheck가 대표적이다. 먼저 Clang-tidy부터 알아보자.

Clang-tidy는 Clang 빌드 시스템의 일부이자 LLVM 프로젝트에 사용되는 정적 코드 분석 툴이다. 아주 강력한 툴인데, Clang-tidy를 사용하는 것만으로도 수많은 코드들을 더 안전하게, 그리고 더 현대적으로 관리할 수 있다.

Clang-tidy 역시 기본적으로 CLI 인터페이스로 사용 가능한 도구이다. 그러나 보통은 CMake와 같은 프로젝트와 통합되어 사용되는 게 일반적이다.

CLion의 CMake 프로젝트에서 clang-tidy를 사용해 보자.
사실 CLion에서 clang-tidy를 사용하기 위해서 우리가 해야 할 것은 아무것도 없다. IDE에서 자체적으로 지원해 주기 때문이다.

정확히 말하면 CMake 프로젝트에서 빌드 시 Clang-tidy를 사용해서 Linting 하는 과정을 포함하기 위해서는, -DCMAKE_CXX_CLANG_TIDY flag를 설정해주어야 한다. 명령어를 통해서 설정을 해주거나, CMakeLists.txt 파일에 직접 넣는 방법이 있다.

$ cmake -DCMAKE_CXX_CLANG_TIDY="clang-tidy;-checks=-*,google-readability-casting;-fix;-fix-errors;" ..

이런 식으로 해놓거나

set(CMAKE_CXX_CLANG_TIDY "clang-tidy;-checks=-*,google-readability-casting;-fix;-fix-errors;")

이런 식으로 해놓으면 된다. 그런데 사실 CLion에서는 이렇게 안 해도, IDE 자체에서 CMake를 지원해 준다.

[Fig 2. CLion view]

아까 전 main.cc를 다시 CLion 프로젝트에서 복붙하면, 위와 같이 경고 옵션이 뜨는 것을 볼 수 있다. CLion의 우상단, 노란색 경고 박스 ⚠️ 버튼을 누르면, Clang-tidy에서 감지한 취약점들을 확인할 수 있다.

Clang-tidy auto fix

Clang-tidy가 강력한 점은, 이렇게 감지된 문제점들 대부분을 자동으로 수정할 수 있다는 데 있다. 좀 더 실용적인 예로, 다음 코드를 보자.

bool AuthManager::verifyPassword(std::string_view password, std::string_view encryptedPassword)
{
    return argon2id_verify(encryptedPassword.data(), password.data(), password.size()) == ARGON2_OK;
}

위 함수를 자세히 보면, 함수 내부에서 class의 상태에 어떠한 의존 관계도 갖지 않음을 알 수 있다. 즉 class의 private 변수나 내부 메소드를 사용하지 않는다.

이러한 함수들은 함수형 프로그래밍 언어(functional programming language)에서는 pure 함수라고 한다. pure 함수가 될 수 있는 함수는 최대한 pure로 선언해 주는 게 좋다.

C++에서는 pure 키워드는 없고, 대신 static 키워드로 이를 대체하는데, static 키워드는 함수의 정적 바인딩을 의미하는 동시에 실용적으로는 함수의 독립적인 의존 관계를 나타내기 때문이다.

[Fig 3. Example of Clang-tidy auto fix]

그래서 CLion에서 함수 위에 노란색으로 경고 마크가 되어 있고, 이를 확인해 보면 함수 verifyPassword는 static 함수가 되어 있다고 나와 있다.

그리고 밑에 파란색 글씨를 클릭하면, 자동으로 함수를 static으로 만들어준다.

이처럼 clang-tidy는 자동으로 코드를 수정해 주는 auto fix 기능 덕분에 매우 편리하다. 특히 놓치기 쉬운 취약점들을 고칠 때 꽤 편리한데, static_cast 문제, 그 반대인 필요 없는 형 변환(useless casting) 문제 등을 고칠 때 등 놓치기 쉬운 문제들을 처리하는데 편리하다.

코딩 스타일을 제안해주기도 한다. const 키워드를 붙인다던가, 필요한 곳에 [[nodiscard]] 속성을 붙이게끔 한다던가, auto 키워드 사용, iterator 대신 ranged-base for loop 사용 등 전반적으로 현대적이고 안전한 C++ 코딩 스타일을 제안해 준다.

Clang-tidy에서는 기본적으로 정말 많은 옵션들과 함께, 코딩 스타일 관련 옵션들도 지원해 준다. 예를 들어 Google 관련 옵션을 켜주면 Google 코딩 스타일에 맞게 코드를 수정해 준다던지, C++ Core Guidelines에 맞게 코드를 수정해준다던지 하는 식이다.

내 개인적인 설정은, 일단 경고 관련 옵션은 전부 켜준다. 그리고 modernized 관련 옵션들 역시 필요한 것들을 적절히 켜준다 (나의 코딩 스타일에 맞지 않는 것 제외하고는 다 켜주는 편이다). 여기까지가 기본적으로 하는 세팅이고, 선택적으로는, 예를 들어서 Google의 코딩 컨벤션을 지킬 때는 google-readability 관련 옵션들을 켜주는 식이다.

CLion에서 Clang-tidy를 활용하는 자세한 방법은 Jetbrains의 공식 문서인 Clang-Tidy integration 이 글을 확인해 보자.

Cppcheck

[Fig 4.1. C++ Check Logo]

 

[Fig 4.2. C++ Check Logo]

cppcheck는 구글에서 만든 정적 분석 도구이다. clang-tidy와 기본적인 사용법은 비슷하고, 구글의 코딩 스타일에 입각해서 코드가 짜였는지 분석하는 도구이다.

CLion에서는 cppcheck를 기본적으로 지원해주지는 않는데, 대신 확장 기능을 설치해서 사용할 수 있다.

[Fig 5. cppcheck plugin]

위 확장 기능을 설치하면, CLion에서도 cppcheck를 사용할 수 있다.

다만 개인적으로는 정말 필요한 기능들이나 편리한 기능들 외에는 IDE에 확장 기능을 이것저것 설치하는 것을 선호하지는 않아서, 나는 주로 cppcheck를 터미널에서 사용한다.

사실 CLI의 간지 때문이기도 하고, CLion에서는 이상하게 저 플러그인이 일부 충돌이 나는 경우가 있더라.

$ cppcheck src/ --enable=all --std=c++20 --language=c++ --suppress=missingIncludeSystem

위와 같이 입력하면 src/ 디렉터리에 있는 모든 cpp 파일을 검사한다는 의미이다.

각 flag가 의미하는 것을 설명하자면

  • --enable : 검색 범위 옵션이다. 위에서는 all로 지정해 두었다. 이 외에 error, warning, portability 등의 옵션이 있다. netmarble의 기술 블로그에서는 가급적 warning으로 설정할 것을 권하고 있다 (--enable=warning)
  • --suppress : 탐지하지 말아야 할 기능을 지정하는 옵션, 즉 제외 기능이다. 여기서는 cppcheck에게 include path를 지정해주지 않았으므로, missingIncludeSystem을 지정해 주었다.
  • --std : C++ 표준을 지정하는 옵션이다.
  • --language : 언어를 지정해 준다. 기본적으로 --std를 지정했다면, 암시적으로 C++로 지정되기에 생략 가능하다.

위 커맨드를 실행하면 아래와 같이 분석이 진행된다.

[Fig 6. Cppcheck run]

그림에서는 raw loop를 사용하는 것보다 std::any_of 알고리즘을 사용하는 것을 권장한다고 나와 있다. 무슨 상황인지 보자.

for (const auto &near_table_id : near_tables)
{
    if (near_table_id.has_value())
    {
        const auto near_table = getTable(near_table_id.value());
        // find reservation list in given time
        const auto reservation_list = near_table.getReservationList(time);
        for (const auto &reservation : reservation_list)
        {
            if (reservation.getUserInfo().getGender() != gender)
            {
                return true;
            }
        }
    }
}
return false;

이중 for-loop이 들어가 있어서 그렇다. 코드의 가독성을 위해선 indentation을 최대한 줄여야 하는데, 이 경우는 그냥 쌩으로 for loop를 돌리고 있다. 이때는 std::any_of 알고리즘을 사용하는 게 좋다고 알려준다.

std::any_of 알고리즘을 사용해서 코드를 리팩토링해보자.

return std::any_of(near_tables.begin(), near_tables.end(), [this, &gender, &time](const auto &near_table_id) {
    if (near_table_id.has_value())
    {
        const auto near_table = getTable(near_table_id.value());
        const auto reservation_list = near_table.getReservationList(time);
        return std::any_of(reservation_list.begin(), reservation_list.end(), [&gender](const auto &reservation) {
            return reservation.getUserInfo().getGender() != gender;
        });
    }
    return false;
});

이렇게 리팩토링할 수 있다. 혹은 cppcheck가 제안한 std::any_of 대신 std::for_each를 사용해 보는 것도 고려할 수 있겠다. 다만 사실 cppcheck와는 관계없이, 이중 for-loop이 들어갔다는 것부터 뭔가 설계상의 문제가 있지 않나 고민해봐야 한다.

저 코드 상황 역시 기본적으로 설계가 잘못되어서 이중 for-loop이 등장하게 된 거고, 이후에 다시 설계해서 코드를 작성하였다.

한편 다시 cppcheck로 돌아와서, cppcheck를 git hook을 이용해서 commit 전 cppcheck를 실행하도록 자동화하는 것도 가능하다. 여기 내가 쓰는 git hook이 있다. 매번 commit을 발생시킬 때마다 src 디렉토리에 있는 코드들을 대상으로 cppcheck를 실행하도록 해놨다.

# Run cppcheck on src directory
echo "Running cppcheck on src directory: "
cppcheck src --enable=all --std=c++20 --language=c++ --suppress=missingIncludeSystem

# check if cppcheck is failed
cppcheck_exit_code=$?
if [ $cppcheck_exit_code -ne O ]; then
    echo "Cppcheck has failed"
    echo $cppcheck_exit_code
fi

echo "cppcheck done"

./.git/hooks/pre-commit 파일에 위와 같이 작성하면 된다. 그러면 commit 시 자동으로 git hook이 실행되어서 정적 분석을 진행해 준다.

기타 정적 분석 도구

그 외에도 정말 다양한 C++용 정적 코드 분석 도구들이 존재한다. 사용해보지는 않았는데, 찾아보니깐 아래와 같은 것들이 있었다.

더 많은 정보는 Top 9 C++ Static Code Analysis Tools 이 글을 참고하자.

그리고 이 글에서 나오지는 않았지만, 만약에 윈도우 환경에서 visual studio를 사용한다면 visual studio의 자체 정적 코드 분석 도구를 알게 모르게 이미 사용하고 있었을 것이다. 사실 VS 쓰고 있다면 이미 VS의 코드 분석이 워낙 강력하게 잘 되어 있어서 이런 정적 도구들의 필요성을 못 느끼고 있었을지도 모른다.

개인적인 생각으로는 clang-tidy와 cppcheck 등 두 개 정도의 외부 정적 분석 도구, 그리고 경고 flag 옵션을 전부 킨 cross compile 과정 정도면 충분한 것 같다. 이 정도 코드 품질 관리면, 실행과 테스트 등 동적 분석 과정 이전에 미리 잡아낼 수 있는 많은 문제들을 걸러낼 수 있다.

구글에서는 어떻게 정적 분석을 활용하는가

구글에서의 정적 분석

Software Engineering at Google 책에서는 효과적인 정적 분석을 위해서는 확장성유용성을 달성해야 한다고 한다. 그리고 확장성과 유용성을 높이기 위해 세 가지 핵심 교훈을 제시한다.

  1. 개발자의 행복에 집중하자
  2. 정적 분석을 개발자 워크플로에 반드시 끼워 넣자
  3. 사용자가 기여할 수 있도록 하자

개발자의 행복에 집중하자는 것은, 위양성(false positive)을 줄이는데 집중하는 것을 의미한다. 많은 정적 분석 도구들은 위음성(false negative)을 줄이는데 집중한다. 그렇게 해서 최대한 많은 취약성 있는 코드를 놓치지 않고 찾아내는데 집중하는데, 이 과정에서 실제론 문제가 없음에도 문제가 있다고 보고하는 거짓 양성이 발생하기도 한다. 이런 거짓 양성은 개발자들에게 정적 분석 도구를 신뢰하지 못하게 만든다. 개발자가 정적 분석 도구를 신뢰하게끔 하는 것은 매우 중요하다.

한편 책에서는 분석 도구가 실제로 문제가 있는 코드를 발견하고 문제가 있다고 보고하였는데, 개발자의 인식에 의거하여 이를 거짓 양성이라고 인식하는 것 역시 경계해야 한다고 한다. 구글에서는 이를 유효 거짓 양성(effective false positive)라고 부른다.

정적 분석을 개발자 워크플로우에 반드시 끼워 넣자라는 말은, 말 그대로 코드 리뷰 프로세스의 시간을 단축하기 위해서 정적 분석 과정을 매 커밋되는 과정에 녹여내는 것을 의미한다.

사용자가 기여할 수 있도록 하자는 교훈은, 정적 분석 도구 사용자가 스스로 자신이 가진 지식을 정적 분석 도구에 녹여낼 수 있도록 하는 것을 의미한다. 이는 사실 구글이어서 가능해 보이긴 하는데, 구글은 우수한 지식을 갖고 있는 소프트웨어 전문가가 많이 있다. 이들이 자신이 갖고 있는 노하우와 지식을 녹여내어 새로운 검사 로직을 추가하는 등의 기여를 하도록 유도한다.

Tricorder: 구글의 정적 분석 플랫폼

구글은 정적 분석 생태계를 소수의 기존 도구들에 통합하는 것보다, 이를 플러그인하기 쉽게 만드는데 집중하였다. 그래서 어느 구글 엔지니어도 간단한 API 매뉴얼을 보고 분석기를 만들거나 연동시킬 수 있게 하였다.

그리고 구글은 Tricorder라는 자체 분석 도구를 개발하여, 구글의 주 리뷰 도구인 Critique와 통합하여 운용한다고 한다. 여러 대의 분석 서버를 두고 변경된 코드와 메타데이터를 분석을 진행하면, 이 결과가 Critique에 표시된다고 설명한다. Tricorder의 검사 로직은 다음 네 가지 조건을 충족해야 하며, 아래 네 가지 조건을 충족한다면 누구든 새로운 검사 로직을 만들어 추가할 수도 있다.

  • 이해하기 쉬울 것
  • 실행 가능 및 수정 용이: 분석 결과는 컴파일러 검사보다 수정하는 데 더 많은 시간, 고민, 노력이 들 수 있다. 따라서 결과에는 문제를 실제로 해결하는 방법을 제시해야 한다.
  • 유효 거짓 양성 비율 10% 미만: 모든 개발자들이 적중률이 최소 90% 이상이라고 느낄 것
  • 코드 품질 개선에 크게 기여할 수 있는 잠재력: 정확성과 관계없이, 개발자가 심각하게 받아들이고 의식적으로 수정할 만한 문제를 짚어줄 것

재밌는 점은, Tricorder에 있는 100여 개의 분석기 중에 대부분이 Tricorder 팀이 작성한 게 아니라고 한다. 사용자가 스스로 로직을 추가해서 기여할 수 있게끔 하는, 구글의 주인 의식을 강조하는 문화가 느껴진다.

Tricorder: Building a Program Analysis Ecosystem 라는, Tricorder에 관한 논문이 책에서 소개되어 있다. 나는 읽어보지는 않았는데, 그래도 구글의 정적 분석 도구는 어떠한 지 궁금하다면 읽어보는 것도 좋을 것 같다.

글을 마치며

다시 말하지만, 정적 코드 분석에서 결국 중요한 것은 코드의 품질 관리를 유지하기 위한 프로그래머 자신의 노력이다. 정적 코드 분석 과정에서 아무리 경고가 발생해도, 이를 해결하지 않는다면 아무 의미가 없다. 그리고 이것들은 결국 도구일 뿐, 활용하는 것은 프로그래머의 몫이라는 것을 기억하자.

글에서는 내가 쓰는 C++의 정적 도구에 대해서 이야기했는데, 당연히 다른 언어들 역시 온갖 정적 분석 도구들이 존재한다. 오히려 현대 언어들은 대부분 자체적인 정적 분석 기능을 탑재해있기도 하다. 그만큼 중요하다는 뜻인데, 그러니 경고들을 무시하지 말고 활용하도록 해보자.

참고 자료