본문으로 건너뛰기
고급약 45분

정적·동적 분석 심화 — 메모리 누수·자원 릭·와일드 포인터를 도구로 잡아낸다

테스트가 초록색인데도 남아 있는 결함을 다룬다. 정적 분석이 잡는 것과 원리상 못 잡는 것을 가르고, 오탐을 억제 규칙과 신규 코드 게이트로 운영하는 방식을 익히고, ASan·LSan·UBSan·TSan과 Valgrind 계열이 각각 무엇을 검출하며 성능 대가가 얼마인지 실제 출력으로 확인한다. 관리 언어의 누수 형태와 CI 배치 전략까지 다룬다.

  • #정적분석
  • #동적분석
  • #새니타이저
  • #메모리누수
  • #데이터레이스

개념

두 분석은 서로의 사각지대다

레슨 1~3은 "테스트가 코드를 실행했는가, 실행한 코드를 검증했는가"를 다뤘다. 그런데 커버리지 95%·뮤테이션 점수 80%인 프로젝트에서도 이런 일이 남는다. 테스트는 통과하는데 메모리가 하루에 400MB씩 증가한다. 통합 테스트가 200회 중 3회 크래시하고 재현이 안 된다. 부하를 걸면 30분 뒤 Too many open files 로 죽는다.

이유는 단순하다. 테스트는 "출력이 기대와 같은가"만 본다. 누수·릭·해제된 메모리 접근·데이터 레이스는 출력을 즉시 바꾸지 않는다.

정적 분석동적 분석
대상실행하지 않은 코드실행 중인 프로세스
커버리지모든 경로를 원리상 훑는다테스트가 지나간 경로만
대표 오류오탐(false positive)이 많다미탐(false negative)이 많다
확실성"여기 문제일 수 있다""여기서 실제로 일어났다"
비용빌드 시간의 일부실행 시간 2~50배

정적 분석은 넓지만 흐릿하고, 동적 분석은 좁지만 선명하다. 대체재가 아니라 짝이다 — 정적으로 의심 지점을 좁히고, 동적으로 실제 결함인지 확정한다.

정적 분석 도구의 네 갈래, 그리고 한계

"정적 분석을 붙였다"는 말은 너무 넓어 정보가 없다. 최소한 넷을 구분해야 대화가 된다.

갈래보는 것대표 도구오탐 성향
컴파일러 경고언어 규칙 위반, 명백한 실수clang/gcc -Wall -Wextra -Werror매우 낮다. 가장 먼저 켠다
린터버그 관용구·패턴·스타일ESLint, Ruff, clang-tidy, SpotBugs룰마다 다르다
타입 체커타입 불일치·널 가능성tsc, mypy, pyright낮다. 도입 비용이 있다
SAST보안 취약점. 테인트(taint) 흐름 추적Semgrep, CodeQL, Coverity높다. 흐름 분석의 근사 때문

기능안전 영역에는 추상 해석 기반 검증기와 코딩 표준(MISRA C:2012·CERT C) 준수 검사가 더 붙는다(모듈 35·40). 이 레슨의 관심은 보안이 아니라 코드 품질 결함 이다 — 보안 관점의 SAST 운용은 모듈 50, 임베디드 계측 장비는 모듈 46이다.

잘 잡는 것 — 널 역참조 가능 경로, 초기화 전 사용(레슨 2의 ur 이상), 자원 획득 후 미해제, dd·dk 데이터 흐름 이상, API 오용(반환값 무시·예외 삼키기·equals/hashCode 규약 위반), 포맷 문자열 불일치, 도달 불가 코드.

원리상 못 잡는 것 — 요구사항 오해(문법적으로 완벽하게 틀린 일을 한다), 상태·타이밍 의존 결함 대부분(특히 데이터 레이스), 설정·인프라 문제, 성능·사용성.

한계에는 이론적 뿌리가 있다. 프로그램의 비자명한 의미적 성질은 일반적으로 결정 불가능하다(Rice의 정리). 도구는 반드시 근사하고, 오탐 쪽으로 기울일지 미탐 쪽으로 기울일지 를 고른다. 기능안전용 검증기는 미탐을 줄이려 하므로 오탐이 쏟아지고, 일반 린터는 개발자가 쓸 수 있게 미탐을 감수한다. "오탐이 많다"는 도구의 결함이 아니라 설계 선택의 결과 다.

오탐을 다루는 운영 방식

정적 분석이 폐기되는 경로는 늘 같다. 경고가 1,400건 나오고, 아무도 다 볼 수 없어 방치되고, 전부 노이즈로 취급되고, CI에서 빠진다. 막는 규칙은 넷이다.

① 트리아지는 세 갈래다. 진탐은 수정하고, 오탐은 억제하고, 진탐이지만 지금 고치지 않는 것은 "수용" 으로 따로 기록한다(담당자·만료일 포함). 세 번째를 만들지 않으면 팀은 수용해야 할 것을 오탐이라 부르기 시작한다. 그 순간 억제 목록이 거짓말을 담고, 다시 신뢰를 잃는다.

② 억제는 최소 범위로, 사유와 함께.

// clang-tidy: 드라이버 ABI가 요구하는 캐스트. ABI v3 전환 시 제거(ISSUE-4712)
// NOLINTNEXTLINE(cppcoreguidelines-pro-type-cstyle-cast)
uint32_t raw = (uint32_t)desc->payload;
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- 외부 SDK 타입 미제공(ISSUE-4821)
function decode(payload: any) { /* ... */ }

억제의 3원칙이 여기 다 있다. 범위는 한 줄, 규칙 id는 명시, 사유는 텍스트로. 파일 전체 억제(/* eslint-disable */)와 규칙 id 없는 억제는 리뷰에서 막거나, PR diff를 grep 하는 CI 검사로 차단한다.

③ 억제는 자산이 아니라 부채다. 개수의 추세를 지표로 두고 사유 없는 억제 와 만료일이 지난 수용 은 0으로 유지한다. 어떤 룰의 억제가 40건 쌓였다면 "코드가 나쁘다"보다 "그 룰이 이 코드베이스에 맞지 않는다" 는 증거다. 오탐률은 룰 단위로 잰다.

룰 id                              트리아지  진탐  오탐  오탐률
python.lang.correctness.nan-eq        23      21     2     9%
generic.secrets.hardcoded-token       87       4    83    95%   ← 룰을 끈다
java.lang.resource-leak               41      33     8    20%

95% 룰을 켜 둔 채 "억제하면 됩니다"라고 하면 팀 시간을 태운다. 끌 룰을 고르는 것도 운영이다.

④ 게이트는 전체가 아니라 신규 코드에 건다. 레슨 1에서 커버리지 게이트를 변경분 기준으로 바꾼 것과 같은 논리다. 기존 위반 1,400건은 베이스라인으로 얼려 부채 백로그로 옮기고, "이 PR이 새로 만든 위반 0" 만 게이트로 남긴다.

semgrep scan --config=p/default \
  --baseline-commit=$(git merge-base HEAD origin/main) --error
 
git diff -U0 origin/main | clang-tidy-diff.py -p1 -path build -quiet

리뷰 부담이 PR 크기에 비례하게 되고, 베이스라인은 시간이 지나며 줄어든다.

동적 분석 — 새니타이저 계열

새니타이저는 컴파일 시 계측 코드를 삽입해 실행 중 위반을 즉시 잡는다.

도구플래그검출 대상대가(대략)
ASan-fsanitize=address힙·스택·전역 버퍼 오버플로, use-after-free, use-after-return/scope, 이중 해제·잘못된 해제실행 2배, 메모리 2~3배
LSan-fsanitize=leak 또는 ASan 내장메모리 누수(종료 시점 도달 불가 블록)단독 사용 시 거의 없음
UBSan-fsanitize=undefined부호 있는 정수 오버플로, 0 나눗셈, 널·비정렬 포인터 역참조, 범위 밖 시프트, 잘못된 enum·bool 값낮다
MSan-fsanitize=memory초기화되지 않은 메모리 읽기3배. 의존 라이브러리까지 계측 빌드 필요
TSan-fsanitize=thread데이터 레이스, 락 순서 역전(잠재적 교착)실행 515배, 메모리 510배

조합 규칙이 중요하다. ASan과 TSan은 함께 켤 수 없다. MSan도 ASan과 못 쓴다. 그래서 CI는 최소 두 빌드를 만든다 — ASan+UBSan 하나, TSan 하나. 정확한 제약은 컴파일러 버전마다 달라지므로 Clang/GCC 문서에서 원문 확인한다.

clang -g -O1 -fno-omit-frame-pointer \
      -fsanitize=address,undefined -fno-sanitize-recover=all -o app src/*.c
 
ASAN_OPTIONS=detect_leaks=1:detect_stack_use_after_return=1:abort_on_error=1 \
UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1 ./app

-g 와 프레임 포인터가 없으면 스택 트레이스가 읽히지 않는다. 그리고 -fno-sanitize-recover=all 과 halt_on_error=1 이 없으면 UBSan은 위반을 출력만 하고 계속 실행한다 — 그러면 CI가 초록색이 된다. 새니타이저를 붙였는데 아무것도 실패하지 않는 팀의 첫째 원인이 이것이다. macOS에서는 누수 검출 기본값이 다를 수 있어 detect_leaks=1 을 명시한다.

Valgrind 계열 — 도구를 골라 쓴다

Valgrind는 재컴파일 없이 바이너리를 가상 CPU에서 돌려 계측한다. 서드파티 바이너리에도 쓸 수 있는 것이 강점, 느린 것이 약점이다.

도구검출 대상
Memcheck(기본)잘못된 읽기·쓰기, 초기화되지 않은 값 사용, 잘못된 free, free/delete 불일치, memcpy 영역 겹침, 누수. 실행 20~50배
HelgrindPOSIX 스레드 오류 — 데이터 레이스, 락 순서 역전, pthread API 오용
DRD데이터 레이스. Helgrind의 대안으로 메모리 사용이 적은 편
Massif힙 사용량 프로파일 — 시간에 따른 증가 곡선
DHAT할당 블록의 수명·접근 패턴. "쓰지 않는데 들고 있는" 메모리 찾기
Cachegrind / Callgrind캐시·분기 예측 / 호출 그래프 프로파일. 결함 검출용이 아니다

스택·전역 배열 오버런용 실험적 도구도 있었으나 버전에 따라 제공 여부가 다르다 — 원문 확인한다. 새니타이저 대신 쓰는 경우 는 셋이다. 재빌드가 불가능할 때, 초기화되지 않은 값 사용을 MSan 없이 볼 때, 누수 분류가 더 세밀해야 할 때.

==8142== LEAK SUMMARY:
==8142==    definitely lost: 4,096 bytes in 4 blocks     ← 실제 누수. 여기부터 고친다
==8142==    indirectly lost: 1,280 bytes in 20 blocks    ← 부모를 고치면 함께 사라진다
==8142==      possibly lost: 0 bytes in 0 blocks         ← 내부 포인터만 남음. 오탐 잦다
==8142==    still reachable: 72,704 bytes in 3 blocks    ← 전역 캐시·싱글턴이면 정상
==8142==         suppressed: 0 bytes in 0 blocks
valgrind --leak-check=full --show-leak-kinds=definite,indirect \
         --track-origins=yes --error-exitcode=1 \
         --suppressions=ci/valgrind.supp --gen-suppressions=all ./app

--gen-suppressions=all 출력을 억제 파일에 붙인다. 여기에도 같은 규칙이 적용된다 — 사유 주석 없는 억제는 리뷰에서 막는다.

네 가지 결함을 증상과 출력으로 구분한다

실무에서 가장 자주 섞이는 넷이다. 증상만 보고 어느 도구를 켤지 판단 할 수 있어야 한다.

결함전형적 증상켜는 도구
메모리 누수RSS·힙이 시간에 비례해 단조 증가. 결국 OOMLSan / Memcheck / Massif
자원 릭메모리는 정상인데 일정 요청 수 후 실패(EMFILE, 커넥션 타임아웃)OS 카운터·풀 지표
와일드 포인터·use-after-free간헐적 크래시, 값이 이유 없이 손상, 재현율 1%ASan / Memcheck
데이터 레이스부하가 걸릴 때만 값이 틀어진다. 로그를 넣으면 사라진다TSan / Helgrind·DRD
==4271==ERROR: LeakSanitizer: detected memory leaks
Direct leak of 262144 byte(s) in 64 object(s) allocated from:
    #1 0x4f8c10 in session_create src/session.c:88
    #2 0x4f9204 in handle_login src/api.c:214
Indirect leak of 8192 byte(s) in 64 object(s) allocated from:
    #1 0x4f8c44 in session_token src/session.c:95
SUMMARY: AddressSanitizer: 270336 byte(s) leaked in 128 allocation(s).

읽는 순서는 아래에서 위로 다. handle_login 이 session_create 를 부르고 거기서 할당한 블록이 해제되지 않았다. Indirect 는 그 세션이 들고 있던 토큰이다. 부모 한 곳을 고치면 128건이 사라진다.

==5310==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300000eff4
READ of size 4 at 0x60300000eff4 thread T0
    #0 0x4f1a20 in cache_get src/cache.c:132
freed by thread T0 here:
    #1 0x4f1b88 in cache_evict src/cache.c:167
previously allocated by thread T0 here:
    #1 0x4f1988 in cache_put src/cache.c:104

이 출력의 가치는 세 개의 스택 트레이스 다. 어디서 접근했고(cache_get), 어디서 해제했고(cache_evict), 어디서 할당했는지(cache_put)를 한 번에 준다. ASan은 해제된 블록을 격리(quarantine)해 바로 재사용하지 않으므로 1% 확률로만 터지던 결함이 거의 항상 잡힌다.

WARNING: ThreadSanitizer: data race (pid=6120)
  Write of size 4 at 0x55e0f1a2c0 by thread T3:
    #0 counter_inc src/metrics.c:41
  Previous read of size 4 at 0x55e0f1a2c0 by thread T1:
    #0 counter_report src/metrics.c:57
  Location is global 'request_count' of size 4
 
src/fee.c:23:18: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'

TSan은 "언젠가 터질 수 있다"가 아니라 "이번 실행에서 동기화 없이 같은 주소에 접근한 두 스레드가 실제로 있었다" 를 보고한다. 값이 우연히 맞게 나와도 보고하므로, 부하 테스트로 재현되지 않는 레이스를 평범한 단위 테스트에서 잡아낸다. 마지막 한 줄은 UBSan이다 — 정의되지 않은 동작은 최적화 수준에 따라 결과가 바뀌므로 디버그 빌드에서 잘 돌던 코드가 릴리스에서 깨지는 대표 원인이다.

자원 릭은 새니타이저가 못 잡는다. 파일 핸들·소켓·DB 커넥션·스레드는 OS나 런타임이 관리하므로 계측 대상 밖이다. 카운터를 본다.

ls /proc/$PID/fd | wc -l                        # 부하 중 10초 간격으로 추세를 본다
ulimit -n
ss -tanp | awk '{print $1}' | sort | uniq -c     # CLOSE_WAIT 누적 확인

CLOSE_WAIT 가 계속 쌓이면 상대가 닫았는데 우리 쪽이 닫지 않았다 는 뜻이고, 대개 예외 경로에서 응답 스트림을 닫지 않는 코드다. TIME_WAIT 는 정상 상태이므로 혼동하지 않는다. 커넥션 풀은 활성·유휴 수와 획득 타임아웃 예외를 본다. 이 영역에서는 정적 분석이 의외로 강하다 — try-with-resources 없는 코드, finally 누락, with 문 없는 open() 은 린터의 자원 누수 룰이 잘 잡는다. 정적 분석으로 예방하고 부하 중 카운터로 검증하는 것 이 실무 조합이다.

관리 언어에서는 누수의 형태가 다르다

GC가 있으면 누수가 없다는 것은 오해다. GC는 도달 가능한 객체를 절대 수거하지 않는다. 그래서 누수는 "해제를 잊음"이 아니라 "참조를 놓지 않음" 으로 나타난다.

형태예증상
참조 유지static 컬렉션에 계속 담기, 스레드 풀에서 ThreadLocal 미정리힙이 단조 증가, Full GC 후에도 안 줄어든다
리스너·타이머 미해제이벤트 리스너·구독·setInterval 해제 누락화면을 반복 진입할수록 증가
캐시 무한 증가상한 없는 HashMap 캐시, lru_cache(maxsize=None)트래픽에 비례해 증가, 재시작하면 초기화
# JVM — 클래스별 인스턴스 수와 힙 덤프. 덤프는 dominator tree 로 참조 경로를 본다
jcmd $PID GC.class_histogram | head -20
jcmd $PID GC.heap_dump /tmp/heap.hprof
# 사고 대비: -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/var/log/
 
# Node.js — 스냅샷 3회 비교가 정석이다. ①기준 → 동작 N회 → ② → 동작 N회 → ③
node --inspect --heapsnapshot-signal=SIGUSR2 server.js   # kill -USR2 로 스냅샷
#  ①→② 와 ②→③ 양쪽에서 함께 남은 객체가 범인이다
# Python — tracemalloc 스냅샷 차분
import tracemalloc
tracemalloc.start(25)
base = tracemalloc.take_snapshot()
run_workload()
for stat in tracemalloc.take_snapshot().compare_to(base, 'lineno')[:10]:
    print(stat)      # app/cache.py:64: size=+18.2 MiB, count=+48120

세 도구의 공통 사용법이 하나 있다. 절대값이 아니라 차분을 본다. "힙이 800MB다"는 정보가 없고, "같은 동작 1,000회에 세션 객체가 1,000개 늘고 GC 후에도 안 줄었다"가 결함 보고다. 브라우저에서는 DOM에서 제거됐는데 JS가 참조를 들고 있는 분리된(detached) 노드 가 프런트엔드 누수의 큰 몫을 차지한다.

CI에 붙이는 현실 — 느린 검사를 어디에 두는가

기준은 하나다. PR 파이프라인의 총 시간 예산을 먼저 정하고, 그 안에 들어가는 것만 PR에 둔다.

검사대가배치실패 처리
컴파일러 경고 -Werror없음매 PR차단
린터·타입 체커(변경분)수십 초매 PR차단
SAST 증분(--baseline-commit)수 분매 PR신규 위반만 차단
ASan + UBSan 테스트2~3배매 PR(가능하면)차단
TSan 동시성 테스트5~15배야간 또는 동시성 모듈 한정이슈 자동 생성
Valgrind Memcheck20~50배야간·릴리스 후보릴리스 차단
MSan(의존성 재빌드)3배 + 빌드 비용주간 전용 잡이슈 생성
장시간 누수 감시(soak)수 시간야간·주말추세 경보

주의점 넷. 새니타이저 빌드는 별도 빌드 트리로 분리한다(일반 캐시와 섞이면 계측 여부가 뒤섞인다). 테스트 타임아웃을 함께 늘린다(ASan 2배·TSan 10배면 기존 타임아웃이 먼저 터져 원인을 오해한다). 성능 테스트와 같은 잡에 두지 않는다(계측된 바이너리의 응답 시간은 의미가 없다). 야간 실패는 담당자에게 도달하게 만든다 — 아무도 안 보는 야간 잡은 검사가 아니라 전기 요금이다.

도입 순서는 뮤테이션 테스트와 같다. 싼 것부터 — -Werror, 린터 변경분, UBSan, ASan, 그다음 TSan·Valgrind. 처음부터 전부 켜면 첫 주에 꺼진다.

실무에서 왜 필요한가

간헐적 결함은 QA의 신뢰를 가장 빨리 깎는 종류다. "200회 중 3회 실패했습니다"라고 보고하면 개발자는 대개 "환경 문제 아닐까요"라고 답하고, 재현 절차가 없으니 논쟁이 끝나지 않는다. ASan 리포트 한 장이 이 대화를 끝낸다 — 해제 위치와 접근 위치의 파일·줄 번호가 함께 있어 반박할 여지가 없다.

누수도 같다. "메모리가 새는 것 같습니다"로는 아무 일도 일어나지 않지만, "동일 시나리오 1,000회 반복 후 힙 덤프에서 SessionContext 인스턴스가 1,000개 유지되고 GC 후에도 감소하지 않습니다. 참조 경로는 static SESSION_REGISTRY 입니다"는 그날 수정된다. 도구는 재현 가능한 증거 를 만드는 장치다. 조직 차원에서는 게이트 설계가 남는다 — 경고 1,400건을 그대로 두고 "품질 게이트를 도입했다"는 조직과, 베이스라인·신규 위반 0으로 리뷰 부담을 상수로 만든 조직의 차이는 1년 뒤 부채 곡선에서 드러난다.

실습 과제

과제 1 — 네 가지 결함을 만들고 잡기 (30분)

아래를 bugs.c 로 저장한다. 결함이 넷 심겨 있다.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>
 
static int hits = 0;                          /* (D) 동기화 없는 공유 변수 */
 
static char *make_label(const char *n) {
    char *buf = malloc(16);                   /* (A) 해제되지 않는다 */
    snprintf(buf, 16, "L-%s", n);
    return buf;
}
 
static void dangling(void) {
    char *p = malloc(12);
    strcpy(p, "hello");
    free(p);
    printf("%c\n", p[1]);                     /* (B) use-after-free */
}
 
static void *worker(void *a) {
    for (int i = 0; i < 100000; i++) hits++;
    return NULL;
}
 
int main(void) {
    printf("%s\n", make_label("one"));
    dangling();
    int big = 2147483647;
    printf("%d\n", big * 2);                  /* (C) 부호 있는 정수 오버플로 */
    pthread_t t1, t2;
    pthread_create(&t1, NULL, worker, NULL);
    pthread_create(&t2, NULL, worker, NULL);
    pthread_join(t1, NULL); pthread_join(t2, NULL);
    printf("hits=%d\n", hits);
    return 0;
}
  1. 계측 없이 컴파일해 실행한다. 어떤 결함이 증상으로 드러나는가. hits 값을 다섯 번 재서 기록한다
  2. -fsanitize=address,undefined 빌드로 실행한다. (A)·(B)·(C) 중 몇 개가 잡히는가
  3. -fsanitize=thread 빌드로 실행한다. (D)가 잡히는가. 두 스택 트레이스가 각각 무엇을 가리키는가
  4. ASan과 TSan을 한 빌드에 함께 켜 보고 무슨 일이 일어나는지 적는다
  5. valgrind --leak-check=full 로 돌려 Memcheck의 누수 분류와 LSan 출력을 비교한다
  6. 네 결함을 고치고 세 빌드가 모두 통과할 때까지 반복한다

과제 2 — 오탐 운영 규칙 만들기 (25분)

업무 저장소에 정적 분석 도구 하나를 붙인다(Semgrep·ESLint·clang-tidy·SpotBugs 중 스택에 맞는 것).

  1. 전체 스캔 경고 총 건수를 기록한다
  2. 무작위 20건을 진탐·오탐·수용 세 갈래로 트리아지한다
  3. 룰별 오탐률 표를 만든다. 50%를 넘는 룰이 있으면 끄는 결정 을 기록한다
  4. 남은 룰로 베이스라인을 만들고 신규 위반 0 게이트를 CI에 붙인다
  5. 팀 억제 규칙을 한 장으로 쓴다 — 억제 범위·사유 형식·수용 만료일·감사 주기
스캔: 총 ____건 / 트리아지 20건 → 진탐 ___ 오탐 ___ 수용 ___
끄기로 결정한 룰: ____________ (오탐률 ___%, 사유: ____________)
베이스라인 커밋: ____________    게이트: 신규 위반 0

과제 3 — 관리 언어 누수 재현하기 (20분)

담당 서비스(JVM·Node.js·Python 중 하나)에서 누수 재현 절차를 만든다.

  1. 의심 동작 하나를 고른다(세션 생성, 화면 진입·이탈, 파일 업로드 등)
  2. 기준 스냅샷을 뜬다(jcmd GC.class_histogram / 힙 스냅샷 / tracemalloc)
  3. 동작을 500회 반복하고 GC를 유도한 뒤 다시 스냅샷을 뜬다
  4. 증가한 상위 5개 클래스·할당 지점을 표로 적고, 참조 경로를 한 줄로 쓴다 — 무엇이 이 객체를 놓지 않는가
  5. 같은 부하에서 파일 디스크립터 수와 커넥션 풀 활성 수도 기록해, 메모리 누수와 자원 릭 중 어느 쪽인지 판정한다

자가 체크리스트

  • 정적 분석과 동적 분석의 커버리지·확실성·비용 차이를 설명할 수 있다
  • 컴파일러 경고·린터·타입 체커·SAST를 구분하고 각각의 오탐 성향을 말할 수 있다
  • 정적 분석이 원리상 못 잡는 결함을 셋 이상 들고 그 이유를 설명할 수 있다
  • 경고를 진탐·오탐·수용 세 갈래로 트리아지하고 억제 3원칙을 적용할 수 있다
  • 룰별 오탐률을 근거로 룰을 끄는 결정을 할 수 있다
  • 베이스라인 + 신규 위반 0 게이트를 실제 명령으로 구성할 수 있다
  • ASan·LSan·UBSan·MSan·TSan이 각각 무엇을 잡고 대가가 얼마인지 구분할 수 있다
  • Memcheck·Helgrind·DRD·Massif·DHAT의 용도와 새니타이저와의 선택 기준을 말할 수 있다
  • 메모리 누수·자원 릭·use-after-free·데이터 레이스를 증상만 보고 구분할 수 있다
  • 관리 언어의 누수 세 형태를 알고 스냅샷 차분으로 재현 절차를 만들 수 있다

흔한 실수

  • UBSan을 붙였는데 CI가 초록색이다. -fno-sanitize-recover=all 과 halt_on_error=1 이 없으면 출력만 하고 넘어간다. 검사가 있는 척만 하는 상태다.
  • ASan과 TSan을 한 빌드에 켠다. 함께 쓸 수 없다. 두 빌드·두 잡으로 나눈다.
  • Valgrind를 매 PR에 붙인다. 20~50배다. CI 시간이 무너지고 곧 꺼진다. 야간·릴리스 후보가 자리다.
  • still reachable 을 누수로 보고한다. 전역 캐시·싱글턴이면 정상일 수 있다. definitely lost 부터 고친다.
  • 자원 릭을 새니타이저로 찾으려 한다. 파일 핸들·소켓·커넥션은 대상이 아니다. OS 카운터와 풀 지표를 본다.
  • GC 언어에 누수가 없다고 생각한다. GC는 도달 가능한 객체를 수거하지 않는다. 참조 유지·리스너 미해제·무한 캐시가 그 자리를 채운다.
  • 힙 절대값으로 누수를 판정한다. 차분이 근거다. 동일 동작 N회 전후 증가와 GC 후 감소 여부를 함께 본다.
  • 억제에 사유를 남기지 않는다. 6개월 뒤 아무도 지울 수 없다. 규칙 id·사유·(수용이면) 만료일이 한 세트다.

참고 자료

  • Clang — AddressSanitizer — 검출 대상, 플래그, ASAN_OPTIONS, 억제 파일 형식
  • Clang — LeakSanitizer — 단독 사용과 ASan 내장 모드, 플랫폼별 기본값(원문 확인)
  • Clang — UndefinedBehaviorSanitizer — 검사 묶음과 -fno-sanitize-recover
  • Clang — ThreadSanitizer — 데이터 레이스 검출 모델과 제약
  • Clang — MemorySanitizer — 의존 라이브러리 계측 요구 사항
  • Valgrind 사용자 매뉴얼 — Memcheck·Helgrind·DRD·Massif·DHAT 도구별 장. 제공 여부는 버전별 원문 확인
  • Memcheck — 누수 분류 — definitely/indirectly/possibly lost, still reachable 정의
  • Semgrep 공식 문서 — --baseline-commit 기반 증분 스캔
  • MISRA C:2012, CERT C Coding Standard — 코딩 표준 준수 검사의 규칙집. 판·개정 번호는 원문 확인
  • 레슨 s3-m53-l02 — 데이터 흐름 이상(dd·ur·dk)과 정적 분석 경고의 연결
  • 레슨 s3-m09-l04 — 운영체제 기초(프로세스 메모리·파일 디스크립터)
  • 레슨 s3-m10-l04 — 비결정적 테스트 다루기(간헐적 실패의 다른 원인들)
  • 레슨 s4-m14-l04 — 품질 게이트 설계(파이프라인 배치의 일반 원칙)

낯선 말 풀이

이 레슨에 나온 약어입니다. 본문에서도 처음 나올 때 점선으로 표시해 두었습니다 (제목과 링크 글자는 그대로 둡니다).

SAST — 정적 보안 분석
프로그램을 돌리지 않고 코드 자체를 읽어 위험한 부분을 찾는 검사.
MISRA — 미스라
안전이 중요한 분야에서 "이런 식으로는 코드를 쓰지 말라"를 모아 둔 규칙집.
API — 응용 프로그램 인터페이스
프로그램끼리 주고받는 창구. 사람이 화면 버튼을 누르는 대신, 프로그램이 정해진 주소로 요청을 보내 답을 받는다.
CI — 지속적 통합
코드를 올릴 때마다 자동으로 합쳐서 빌드하고 시험까지 돌리는 방식.
PR — 풀 리퀘스트
"내가 고친 코드를 본체에 합쳐 달라"고 올리는 요청. 합치기 전에 사람들이 보고 의견을 단다.
CPU — 중앙처리장치
계산을 담당하는 부품. 사람으로 치면 머리다.
OOM — 메모리 부족
쓸 수 있는 메모리를 다 써서 프로그램이 죽는 상황.
OS — 운영체제
기기를 돌리는 바탕 프로그램 — 윈도우, 맥, 안드로이드 같은 것.
DB — 데이터베이스
데이터를 넣어 두고 꺼내 쓰는 저장소. 보통 표 여러 개가 서로 연결된 형태다.
GC — 가비지 컬렉션
더 이상 쓰지 않는 메모리를 프로그램이 알아서 치우는 일.
DOM — 문서 객체 모델
웹 화면을 프로그램이 다룰 수 있게 나무 구조로 바꿔 놓은 것. 화면의 현재 상태를 담고 있다.
JS — 자바스크립트
웹 화면을 움직이게 만드는 프로그래밍 언어. 버튼을 눌렀을 때 일어나는 일을 적는다.

이 레슨에 나온 나머지 2개를 포함해 용어집 전체 보기