사전 정적 분석
개념

정적 분석

gabury1

정적 분석은 프로그램을 돌려 보지 않고 코드를 읽어서 잘못된 곳을 찾아냅니다. 실행해 봐야 드러나는 문제를 코드만 보고 미리 알려 줍니다. 아직 아무도 밟아 보지 않은 실행 경로에 숨은 오류가 이렇게 걸립니다. 대신 넘겨짚는 만큼 틀린 지적이 섞입니다.

쉽고 빠른 이해

정적 분석은 코드를 돌리지 않고 읽기만 해서 문제를 찾아 주는 검사입니다. 값이 비어 있을 수 있는데 그냥 쓰는 곳, 만들어 놓고 아무도 안 쓰는 변수 같은 것을 코드에서 바로 짚어 냅니다.

이게 없으면 그런 문제는 테스트가 밟은 길에서만 드러납니다. 조건이 드물게 맞아떨어질 때만 지나가는 길의 오류는 운영에 나가서야 처음 터집니다.

어떻게 도나:

  1. 코드를 문법 구조로 쪼개 기계가 다루기 쉬운 모양으로 바꿉니다
  2. 값이 어디서 와서 어디로 흘러가는지, 실행이 어느 길로 갈리는지 따라갑니다
  3. 미리 정해 둔 규칙에 어긋나는 곳을 모아 보고합니다

대가는 틀린 지적이 섞인다는 것입니다. 돌려 본 것이 아니라 넘겨짚은 결과라서 문제가 안 되는 곳까지 경고로 나옵니다. 그런 경고가 쌓이면 사람이 목록 전체를 무시하게 됩니다.

상세

맞춤법 검사기는 글을 소리 내어 읽어 보지 않고도 문장만 보고 틀린 곳을 잡아냅니다. 띄어쓰기가 어긋난 곳이나 주어와 서술어가 안 맞는 문장을 글쓴이가 다시 읽기도 전에 밑줄로 알려 줍니다. 정적 분석이 코드에 하는 일이 이것입니다.

정적 분석은 프로그램을 실행하지 않고 코드 자체를 살펴 그 프로그램의 성질을 알아내는 일입니다. 예를 들어 「이 값이 비어 있을 수 있나」 같은 물음에 코드만 보고 답합니다. 비어 있을 수 있는 값을 그냥 쓰는 곳이 그렇게 드러납니다. 쫓는 범위는 한 줄에 그치지 않습니다. 한 값이 함수와 파일을 건너 어디까지 흘러가는지를 따라갑니다.

정적이라는 말은 멈춰 있다는 뜻입니다. 돌고 있는 프로그램이 아니라 멈춰 있는 코드를 본다는 것을 가리킵니다. 반대편에는 프로그램을 돌려 놓고 그 움직임을 재는 동적 분석이 있습니다.

읽는 대상은 사람이 쓴 소스 코드입니다. 번역을 거쳐 나온 바이트코드나 실행 파일일 수도 있습니다. 어느 쪽이든 프로그램을 돌리지 않는다는 점은 같습니다.

돌려 보지 않고 알아내는 원리

비어 있을 수 있는 값을 쓰는 자바 두 줄을 놓고, 테스트가 못 잡는 오류를 정적 분석이 어떻게 잡는지 보겠습니다.

Java
String name = req.get(KEY);     // null 가능
if (debug) log(name.length());  // 터진다

기록을 켜는 debug 가 꺼져 있으면 둘째 줄은 아예 실행되지 않습니다. 그 상태로만 테스트를 돌리면 아무 문제가 없어 보입니다.

프로그램이 무엇을 하는지는 코드에 이미 다 적혀 있습니다. 한 번 실행한다는 것은 갈림길에서 한 가닥 길을 골라 밟아 보는 일입니다. 정적 분석은 밟는 대신 갈림길을 전부 펴 놓고 봅니다.

flowchart TD
    A["name 을 받는다 · 비어 있을 수 있다"] --> B{"debug 가 켜졌나"}
    subgraph s1["테스트가 밟은 길"]
        C["둘째 줄을 건너뛴다 · 아무 일도 없다"]
    end
    subgraph s2["테스트가 안 밟은 길 · 정적 분석은 이쪽도 본다"]
        D["name 의 길이를 묻는다 · 터진다"]
    end
    B -->|"꺼짐"| C
    B -->|"켜짐"| D

테스트는 밟아 본 길에서 프로그램이 제대로 돌았다는 것을 확인해 줍니다. 정적 분석은 모든 길을 함께 놓고 보는 대신, 값을 하나하나 계산하지 않고 넘겨짚습니다. name 에 무엇이 들어오는지는 모르는 채로 「비어 있을 수 있다」까지만 알고 다음 줄로 갑니다.

코드를 읽어 내려가는 차례

도구마다 어디까지 파고드는지는 다르지만, 코드를 받아 읽어 내려가는 차례는 대체로 같습니다.

flowchart TD
    A["소스 코드"] --> B["글자를 낱말 단위로 끊는다"]
    subgraph s1["컴파일러와 겹치는 앞단"]
        B --> C["문법에 맞춰 트리로 세운다"]
        C --> D["이름과 타입을 이어 붙인다"]
    end
    subgraph s2["정적 분석의 본체"]
        E["실행이 갈라지는 길을 그린다"]
        E --> F["값이 어디로 흐르는지 따라간다"]
    end
    D --> E
    F --> G["규칙에 어긋난 곳을 보고한다"]

앞의 세 단계는 컴파일러가 프로그램을 번역하려고 하는 일과 같습니다. 코드를 읽어 기계가 다룰 수 있는 모양으로 정리하는 일이라, 번역하는 쪽도 검사하는 쪽도 똑같이 필요로 합니다.

도해의 셋째 상자가 추상 구문 트리를 만드는 단계입니다. 코드를 문법 구조에 따라 나무 모양으로 세운 것입니다. 여기에 각 이름이 무엇을 가리키는지와 어떤 타입인지를 붙이면 코드의 뜻이 기계가 다룰 수 있는 모양으로 정리됩니다.

뒤의 두 단계가 정적 분석의 본체입니다. 다섯째 상자가 제어 흐름 그래프를 그리는 단계입니다. 실행이 조건에 따라 어느 길로 갈리는지를 그린 그림입니다.

여섯째 상자가 데이터 흐름 분석입니다. 그 길들을 따라 값이 어디서 만들어져 어디까지 가는지를 쫓는 일입니다. 비어 있을 수 있는 값이 그냥 쓰이는 곳은 이 단계에서 드러납니다.

찾아내는 문제의 갈래

무엇이 걸리는지는 도구가 코드를 얼마나 멀리까지 쫓아가느냐가 정합니다. 아래로 갈수록 값을 더 멀리 따라가야 나오는 지적입니다.

무엇을 보나 걸러 내는 것
글자와 문법 모양 들여쓰기와 이름 규칙에서 벗어난 코드
쓰이는지 여부 아무도 부르지 않는 함수 · 만들어 놓고 안 쓰는 변수
타입 서로 맞지 않는 타입을 주고받는 호출
값의 흐름 비어 있을 수 있는 값을 그냥 쓰는 곳 · 열어 놓고 안 닫은 자원
바깥에서 들어온 값 검사 없이 질의문이나 명령에 실리는 입력

마지막 줄이 보안 검사의 뼈대입니다. 바깥에서 들어온 값을 오염된 값으로 표시해 두고 그 값이 위험한 호출에 닿는지를 쫓는 방법이 오염 분석입니다.

분석 깊이의 세 갈래

같은 정적 분석이라도 코드를 어디까지 따라가느냐가 갈립니다. 얕은 쪽은 코드의 모양만 보고, 깊은 쪽은 값이 될 수 있는 범위까지 헤아립니다.

깊이 무엇을 보나 대가
모양 대조 문제를 일으키는 코드 모양과 맞아떨어지는지만 본다 모양이 조금만 달라져도 놓친다
흐름 추적 값과 실행 경로를 함수 안에서 따라간다 함수를 건너가면 끊긴다
값의 범위 추정 값이 가질 수 있는 범위를 넘겨짚어 어느 경로도 빠뜨리지 않는다 분석에 걸리는 시간이 크게 늘어난다

표의 셋째 줄에 해당하는 방법이 추상 해석입니다. 값을 하나하나 계산하는 대신 그 값이 가질 수 있는 범위만 넘겨짚어 다룹니다.

같은 셋째 줄에 심볼릭 실행도 들어갑니다. 갈림길마다 조건을 식으로 모아 어느 경로가 진짜 실행되는지를 푸는 방법입니다.

추상 해석과 심볼릭 실행은 둘 다 분석이 오래 걸립니다. 그래서 코드 전체에 늘 돌리기보다 중요한 부분에만 돌립니다.

검사가 넘겨짚을 수밖에 없는 까닭

「이 값이 여기서 비어 있을 수 있나」에 제대로 답하려면 어느 경로가 진짜 실행되는지를 알아야 합니다.

그런데 어떤 프로그램이 언젠가 멈추는지조차, 모든 코드에 대해 맞게 답해 주는 방법은 없습니다. 이것을 정지 문제라고 합니다. 코드가 무엇을 하는지 묻는 물음은 대개 이 물음과 같은 것으로 바뀌어 버립니다.

그래서 정적 분석 도구는 모든 코드를 맞게 판정하는 대신, 한쪽으로 넘겨짚는 길을 고릅니다. 넘겨짚는 방향은 둘뿐입니다.

의심스러우면 일단 경고하는 쪽으로 기울면 문제가 아닌 곳까지 경고가 나옵니다. 이것을 오탐이라고 합니다.

반대로 확실할 때만 경고하는 쪽으로 기울면 경고는 믿을 만해지지만 진짜 문제를 지나칩니다. 이것을 미탐이라고 합니다.

flowchart TD
    A{"검사기가 경고했나"} -->|"했다"| B{"진짜 문제였나"}
    A -->|"안 했다"| C{"진짜 문제였나"}
    B -->|"맞다"| D["제대로 잡은 경고"]
    B -->|"아니다"| E["오탐"]
    C -->|"맞다"| F["미탐"]
    C -->|"아니다"| G["조용히 넘어간 정상 코드"]

어느 쪽으로 기울일지는 무엇을 놓치면 더 아픈지가 정합니다. 사람이 다치거나 돈이 새는 코드를 보는 검사는 오탐을 감수하고 의심스러운 것을 다 올립니다. 개발하는 동안 늘 켜 두는 검사는 미탐을 감수하고 확실한 것만 올립니다.

경고 피로와 그 대처

오탐이 섞인 경고가 수천 개 쌓이면 사람들은 목록 전체를 건너뜁니다. 그러면 그 안에 있던 진짜 문제도 같이 묻힙니다. 경고를 보고도 아무 반응이 안 남는 이 상태를 경고 피로라고 합니다. 도구를 새로 들인 팀이 처음 부딪히는 문제입니다.

막는 방법은 대개 셋입니다. 첫째, 그 팀이 지키지 않을 규칙은 아예 꺼 둡니다. 둘째, 지금 있는 경고를 한 번 적어 두고(기준선) 그 뒤로 새로 생긴 경고만 봅니다.

셋째, 모든 경고를 빌드 실패로 만들지 않고 꼭 막아야 하는 몇 가지만 고릅니다. 그 몇 가지에 걸리면 빌드를 멈추는 관문이 품질 게이트입니다.

검사를 끼워 넣는 시점

정적 분석은 사람이 코드를 쓰는 동안에도, 커밋을 만들기 전에도, 팀에 코드를 올린 뒤에도 돌릴 수 있습니다. 같은 검사라도 언제 돌리느냐에 따라 고치는 데 드는 품이 달라집니다.

언제 누가 받나 고칠 때의 품
편집기에서 코드를 쓰는 동안 코드를 쓰는 사람 방금 쓴 줄이라 바로 고친다
커밋을 만들기 직전 코드를 쓰는 사람 아직 팀에 안 나갔다
풀 리퀘스트를 올린 뒤 검토하는 동료 고치려면 커밋을 다시 밀어야 한다
지속적 통합에서 빌드마다 팀 전체 여러 변경이 섞여 원인부터 가려야 한다

앞쪽일수록 품이 적게 듭니다. 그래서 같은 규칙을 여러 단계에 겹쳐 둡니다. 앞에서 놓친 것을 뒤에서 한 번 더 거릅니다.

테스트와 나누는 몫

정적 분석은 코드가 규칙에 어긋나지 않는다는 것만 말해 줍니다. 그 코드가 원하는 값을 내놓는지는 말해 주지 않습니다. 계산식을 잘못 세운 코드는 문법도 타입도 값의 흐름도 멀쩡해서 정적 분석에 안 걸립니다.

그래서 단위 테스트나 동적 분석과 겹쳐 씁니다. 돌려 봐야 알 수 있는 것과 안 돌려 보고도 알 수 있는 것은 애초에 다른 묶음입니다.

관련 항목

정적 분석이 코드에서 짚어 내는 결함

널 포인터 역참조 · 메모리 누수 · 자원 누수 · 경쟁 상태 · SQL 인젝션 · 버퍼 오버플로 · 죽은 코드 · 코드 스멜

코드를 읽어 만드는 중간 표현

추상 구문 트리 · 제어 흐름 그래프 · 중간 표현 · 심볼 테이블 · 바이트코드 · 파서

코드를 훑을 때 쓰는 분석 기법

데이터 흐름 분석 · 오염 분석 · 추상 해석 · 심볼릭 실행 · 포인터 분석 · 형식 검증

오류를 미리 거르는 타입 장치

타입 시스템 · 정적 타입 · 타입 검사 · 타입 추론 · 널 안전성 · 어노테이션

같은 결함을 실행으로 찾는 검증 수단

동적 분석 · 퍼징 · 단위 테스트 · 테스트 커버리지 · 코드 리뷰 · 침투 테스트

정적 분석을 수행하는 도구

린터 · 정적 분석기 · 포매터 · 컴파일러 · 소프트웨어 구성 분석 · 의존성 스캔

정적 분석이 끼어드는 개발 단계

컴파일 타임 · 프리커밋 훅 · 풀 리퀘스트 · 지속적 통합 · 빌드 파이프라인 · 품질 게이트

분석 결과와 코드 상태를 재는 잣대

오탐 · 미탐 · 사이클로매틱 복잡도 · 기술 부채 · CWE · 코드 품질

다른 이름: static analysis · 정적분석 · 정적 코드 분석