본문으로 건너뛰기
Previous
Next
C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전

C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전

C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전

이 글의 핵심

C++ 디버깅 : GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전. 실무에서 겪는 디버깅 상황·GDB/LLDB 고급 기법.

들어가며: “프로덕션에서 크래시가 나는데 재현이 안 돼요”

실무에서 겪는 디버깅 문제들

실제 C++ 개발 현장에서는 이런 문제를 겪습니다:

  • 프로덕션 크래시 — 개발 환경에서는 멀쩡한데 배포하면 랜덤 크래시
  • 메모리 누수 — 3일째 메모리 사용량이 계속 증가, 어디서 새는지 모름
  • 데이터 레이스 — 멀티스레드 환경에서 가끔 이상한 값이 나옴
  • 성능 저하 — 특정 함수에서 병목이 있는데 프로파일러 없이 찾기 어려움
  • 반복자 무효화 — 컨테이너 순회 중 크래시, 어디서 수정됐는지 추적 불가 이 글에서는 실전 시나리오를 기반으로 GDB 고급 기법, Sanitizer 활용, 멀티스레드 디버깅, 프로덕션 환경 디버깅까지 다룹니다. 목표:
  • GDB/LLDB 고급 기법 (watchpoint, conditional breakpoint, 코어 덤프 분석)
  • Sanitizer 완전 활용 (ASan, TSan, UBSan, MSan)
  • 메모리 누수 추적 (Valgrind, Heaptrack, 실전 패턴)
  • 멀티스레드 디버깅 (데이터 레이스, 데드락 탐지)
  • 프로덕션 디버깅 (로깅, 코어 덤프, 원격 디버깅)
  • 자주 하는 실수와 해결법
  • 프로덕션 패턴 요구 환경: C++17 이상, GDB 8.0+, Clang/GCC with Sanitizers

1. 문제 시나리오: 실무에서 겪는 디버깅 상황

시나리오 1: “프로덕션에서만 크래시가 나요”

상황: 개발 환경에서는 정상 동작하는데, 프로덕션 배포 후 랜덤 크래시
증상: Segmentation fault, 코어 덤프 생성
원인: 릴리스 빌드 최적화로 숨겨진 버그 (초기화 안 된 변수, UB)
→ 코어 덤프 분석, UBSan으로 UB 탐지 필요

시나리오 2: “메모리가 계속 증가해요”

상황: 서버가 72시간 운영 후 메모리 8GB → 14GB로 증가
증상: 느린 메모리 누수, 재시작 전까지 회복 불가
원인: shared_ptr 순환 참조, 컨테이너에서 제거 안 된 객체
→ Valgrind, Heaptrack, ASan으로 누수 지점 추적

시나리오 3: “멀티스레드에서 가끔 이상한 값이 나와요”

상황: 10번 실행하면 1~2번 잘못된 결과 출력
증상: 데이터 레이스, 비결정적 동작
원인: 공유 변수 동기화 누락, 락 없는 접근
→ TSan으로 레이스 탐지, GDB로 스레드별 상태 확인

관련 글: 멀티스레드 기초에서 스레드 프로그래밍 기본 개념을 학습하세요.

시나리오 4: “컨테이너 순회 중 크래시해요”

상황: vector 순회 중 erase 호출 후 크래시
증상: iterator 무효화, Segmentation fault
원인: erase 후 반복자 갱신 안 함
→ GDB watchpoint로 컨테이너 수정 지점 추적

시나리오 5: “데드락이 발생해요”

상황: 멀티스레드 서버가 가끔 멈춤, CPU 사용률 0%
증상: 모든 스레드가 락 대기 상태
원인: 순환 락 대기 (A→B, B→A)
→ GDB로 스레드별 스택 확인, TSan으로 락 순서 분석

시나리오 6: “릴리스 빌드에서만 크래시해요”

상황: Debug 빌드는 정상, Release 빌드는 크래시
증상: 최적화로 인한 숨겨진 버그 노출
원인: 초기화 안 된 변수, 정의되지 않은 동작 (UB)
→ UBSan으로 UB 탐지, -O1로 중간 최적화 테스트

시나리오 7: “특정 입력에서만 크래시해요”

상황: 일반 입력은 정상, 특정 입력 (빈 문자열, 큰 숫자 등)에서만 크래시
증상: 경계 조건 (edge case) 처리 누락
원인: 입력 검증 부족, 배열 범위 체크 누락
→ Fuzzing (AFL, libFuzzer)으로 자동 테스트 케이스 생성
flowchart TB
    subgraph Problems[실무 디버깅 문제]
        P1[프로덕션 크래시]
        P2[메모리 누수]
        P3[데이터 레이스]
        P4[반복자 무효화]
        P5[데드락]
    end
    subgraph Tools[디버깅 도구]
        T1[GDB/LLDB + 코어 덤프]
        T2[Valgrind/ASan]
        T3[TSan]
        T4[Watchpoint]
        T5[스레드 분석]
    end
    P1 --> T1
    P2 --> T2
    P3 --> T3
    P4 --> T4
    P5 --> T5

2. GDB/LLDB 고급 기법

기본 사용법 복습

#include <iostream>
#include <vector>
int buggyFunction(int x) {
    int* ptr = nullptr;
    if (x > 10) {
        ptr = new int(x);
    }
    return *ptr;  // x <= 10이면 크래시
}
int main() {
    std::cout << buggyFunction(5) << std::endl;
    return 0;
}
# 컴파일 (디버그 심볼 포함)
g++ -g -O0 buggy.cpp -o buggy

# -g 플래그 내부 동작:
# - DWARF 디버그 정보 생성 (.debug_info, .debug_line 섹션)
# - 소스 파일 경로, 라인 번호, 변수 타입, 함수 주소 매핑
# - readelf -w a.out 으로 DWARF 정보 확인 가능

# -O0 플래그 중요성:
# - 최적화 비활성화 (변수 최적화 제거, 인라인 방지)
# - 소스 코드와 어셈블리 1:1 대응
# - -O2 이상에서는 변수가 최적화로 사라질 수 있음

# GDB 시작
gdb ./buggy

# GDB 내부 동작:
# 1. 실행 파일 로드 (ELF 파싱)
# 2. DWARF 디버그 정보 파싱
# 3. ptrace() 시스템 콜로 프로세스 제어 준비

# 기본 명령어
(gdb) break buggyFunction    # 함수에 브레이크포인트
# 내부 동작:
# - DWARF에서 buggyFunction 주소 찾기
# - 해당 주소에 int3 명령어 (0xCC) 삽입
# - 원래 명령어는 백업

(gdb) run                    # 실행
# 내부 동작:
# - ptrace(PTRACE_TRACEME)로 프로세스 시작
# - GDB가 부모 프로세스로서 자식 프로세스 제어
# - SIGTRAP 시그널 대기

(gdb) next                   # 다음 줄 (함수 넘어감)
# 내부 동작:
# - 현재 라인의 모든 어셈블리 명령어 실행
# - 함수 호출은 건너뜀 (CALL 명령어 다음에 임시 breakpoint)

(gdb) step                   # 다음 줄 (함수 안으로)
# 내부 동작:
# - 한 개 어셈블리 명령어 실행
# - CALL 명령어 만나면 함수 내부로 진입

(gdb) print ptr              # 변수 출력
# 내부 동작:
# - DWARF에서 변수 위치 찾기 (스택 오프셋 또는 레지스터)
# - ptrace(PTRACE_PEEKDATA)로 메모리 읽기
# - 변수 타입에 맞게 포맷팅

(gdb) backtrace              # 스택 트레이스
# 내부 동작:
# - 스택 프레임 포인터 (RBP) 순회
# - 각 프레임의 반환 주소를 DWARF로 함수명/라인 번호 변환
# - 스택 언와인딩 (stack unwinding)

(gdb) continue               # 계속 실행
# 내부 동작:
# - ptrace(PTRACE_CONT)로 프로세스 재개
# - 다음 breakpoint나 시그널까지 실행

GDB의 프로세스 제어 메커니즘:

ptrace() 시스템 콜을 통한 제어:

1. Breakpoint 설정:
   원래 코드:     mov rax, [rbp-8]

   GDB가 수정:    int3  (0xCC, 소프트웨어 인터럽트)
   원래 코드 백업: mov rax, [rbp-8]
   
2. Breakpoint 도달:
   CPU가 int3 실행

   SIGTRAP 시그널 발생

   커널이 GDB에 알림 (wait() 반환)

   GDB가 제어권 획득

3. 명령 실행 (next, step):
   GDB가 원래 명령어 복원

   ptrace(PTRACE_SINGLESTEP) 호출

   CPU가 한 명령어 실행

   다시 SIGTRAP 발생

   GDB가 다시 제어권 획득

   필요시 int3 재삽입

4. Continue:
   ptrace(PTRACE_CONT)

   프로세스 정상 실행

   다음 breakpoint나 시그널까지 계속

Watchpoint: 변수 변경 추적 (Hardware Breakpoint)

문제: “이 변수가 언제 어디서 바뀌는지 모르겠어요”

#include <iostream>
#include <thread>
#include <vector>

int global_counter = 0;

void increment() {
    for (int i = 0; i < 1000; ++i) {
        ++global_counter;  // 어디서 바뀌는지 추적하고 싶음
    }
}

int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back(increment);
    }
    for (auto& t : threads) {
        t.join();
    }
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}

Watchpoint 내부 동작 메커니즘:

Hardware Watchpoint (권장):

1. CPU 디버그 레지스터 사용:
   x86_64 아키텍처:
   - DR0, DR1, DR2, DR3: 4개의 디버그 주소 레지스터
   - DR7: 디버그 제어 레지스터
   
   watch global_counter 실행 시:

   GDB가 ptrace(PTRACE_POKEUSER)로 DR0에 주소 설정
   DR0 = &global_counter (예: 0x555555558020)

   DR7에 조건 설정 (read/write/execute)
   DR7 = 0x....01 (write 감지)
   
2. 메모리 접근 감지:
   CPU가 global_counter 주소에 쓰기 시도

   MMU가 DR0-DR3과 비교

   일치하면 디버그 예외 발생 (INT 1)

   커널이 SIGTRAP 시그널 발생

   GDB가 제어권 획득

Hardware Watchpoint 제약:
- 최대 4개까지만 설정 가능 (DR0-DR3)
- 크기 제한: 1, 2, 4, 8바이트 (CPU 의존)
- 정렬 요구: 주소가 크기에 맞게 정렬되어야 함

Software Watchpoint (폴백):

하드웨어 레지스터 부족 시:

GDB가 모든 명령어를 single-step으로 실행
(ptrace(PTRACE_SINGLESTEP) 반복)

매 명령어마다 메모리 값 확인

변경 감지 시 중단

단점:
- 매우 느림 (10-100배)
- 대규모 구조체에는 비실용적
# GDB Watchpoint 사용
(gdb) break main
(gdb) run
(gdb) watch global_counter              # 변수 변경 시 중단

# 내부 동작:
# 1. global_counter 주소 확인 (예: 0x555555558020)
# 2. 하드웨어 디버그 레지스터 설정:
#    DR0 = 0x555555558020
#    DR7 = write 감지 모드
# 3. 다음 변경까지 빠르게 실행

(gdb) info watchpoints                  # 설정된 watchpoint 확인
# Num     Type           Disp Enb Address            What
# 2       hw watchpoint  keep y                      global_counter

(gdb) continue
# Hardware watchpoint 2: global_counter
# Old value = 0
# New value = 1
# increment() at test.cpp:7

(gdb) backtrace                         # 어느 함수에서 변경했는지 확인

# 조건부 watchpoint (변경 조건 지정)
(gdb) watch global_counter if global_counter > 500
# global_counter가 500 초과 시에만 중단

# 읽기 감지
(gdb) rwatch global_counter             # 읽기 시 중단

# 읽기/쓰기 모두 감지
(gdb) awatch global_counter             # 접근 시 중단

주의사항:

  • 하드웨어 워치포인트는 최대 4개까지만 설정 가능 (CPU 제약)
  • 거대한 구조체 전체 감시는 소프트웨어 워치포인트로 폴백 (매우 느림)
  • 멀티스레드에서는 모든 스레드가 같은 디버그 레지스터 공유

Conditional Breakpoint: 조건부 중단

문제: “반복문 1000번 중 특정 조건에서만 멈추고 싶어요”

#include <iostream>
#include <vector>
int main() {
    std::vector<int> data(1000);
    for (int i = 0; i < 1000; ++i) {
        data[i] = i * 2;
        // i가 500일 때만 확인하고 싶음
    }
    return 0;
}
# 조건부 브레이크포인트
(gdb) break 7 if i == 500               # i가 500일 때만 중단
(gdb) run
# 복잡한 조건
(gdb) break myFunction if ptr == nullptr && x > 100
# 조건 변경
(gdb) condition 1 i == 750              # 브레이크포인트 1번의 조건 변경

주의사항: 일부 최적화로 지역 변수가 레지스터만 쓰이면 조건식이 기대대로 동작하지 않을 수 있어 -O0를 권장합니다.

코어 덤프 분석

프로덕션에서 크래시 발생 시:

# 코어 덤프 활성화
ulimit -c unlimited
# 프로그램 실행 (크래시 시 core 파일 생성)
./myapp
# Segmentation fault (core dumped)
# 코어 덤프로 디버깅
gdb ./myapp core
# 크래시 지점 확인
(gdb) backtrace
(gdb) frame 0
(gdb) info locals              # 지역 변수 확인
(gdb) print this               # 객체 상태 확인

멀티스레드 디버깅

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
std::mutex mtx;
int shared_data = 0;
void worker(int id) {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++shared_data;
        std::cout << "Thread " << id << ": " << shared_data << std::endl;
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker, i);
    }
    for (auto& t : threads) {
        t.join();
    }
    return 0;
}
# 멀티스레드 디버깅
(gdb) info threads                      # 모든 스레드 목록
(gdb) thread 2                          # 스레드 2로 전환
(gdb) backtrace                         # 해당 스레드의 스택
(gdb) thread apply all backtrace        # 모든 스레드의 스택 출력
# 스레드별 브레이크포인트
(gdb) break worker thread 3             # 스레드 3에서만 중단

주의사항: 논블로킹·파이버 환경에서는 스레드 번호가 흔들릴 수 있어 재현 스크립트와 함께 쓰는 것이 좋습니다.

GDB 스크립트 자동화

# debug.gdb 파일 생성
break main
run
print argc
print argv[0]
continue
# 스크립트 실행
gdb -x debug.gdb ./myapp
# 또는 GDB 내에서
(gdb) source debug.gdb

주의사항: 상대 경로는 GDB의 현재 작업 디렉터리 기준이라, 스크립트는 빌드 디렉터리에 두거나 절대 경로를 쓰세요.

Pretty Printing (STL 컨테이너)

#include <vector>
#include <map>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    std::map<std::string, int> m = {{"a", 1}, {"b", 2}};
    return 0;
}
# GDB에서 STL 예쁘게 출력
(gdb) print vec
# $1 = std::vector of length 5, capacity 5 = {1, 2, 3, 4, 5}
(gdb) print m
# $2 = std::map with 2 elements = {[a] = 1, [b] = 2}
# Pretty printer 활성화 (없으면)
# ~/.gdbinit에 추가:
# python
# import sys
# sys.path.insert(0, '/usr/share/gcc/python')
# from libstdcxx.v6.printers import register_libstdcxx_printers
# register_libstdcxx_printers(None)
# end

LLDB (macOS 기본 디버거)

# LLDB 기본 사용법 (GDB와 유사)
lldb ./myapp
# 주요 명령어 비교
(lldb) breakpoint set --name main       # GDB: break main
(lldb) run                              # GDB: run
(lldb) next                             # GDB: next
(lldb) step                             # GDB: step
(lldb) print variable                   # GDB: print variable
(lldb) bt                               # GDB: backtrace
(lldb) continue                         # GDB: continue
# Watchpoint
(lldb) watchpoint set variable global_counter
(lldb) watchpoint list

3. Sanitizer 완전 활용

AddressSanitizer (ASan): 메모리 오류 탐지

탐지 가능한 버그:

  • Use-after-free
  • Heap buffer overflow
  • Stack buffer overflow
  • Use-after-return
  • Memory leaks
#include <iostream>
int main() {
    int* arr = new int[10];
    delete[] arr;
    
    // Use-after-free
    std::cout << arr[0] << std::endl;  // 💥 ASan이 탐지
    
    return 0;
}
# ASan 활성화 컴파일
g++ -fsanitize=address -g -O1 use_after_free.cpp -o test
# 또는 Clang
clang++ -fsanitize=address -g -O1 use_after_free.cpp -o test
# 실행
./test
# 출력 예시:
# =================================================================
# ==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
# READ of size 4 at 0x....thread T0
#     #0 0x....in main use_after_free.cpp:7
# ...
# freed by thread T0 here:
#     #0 0x....in operator delete
#     #1 0x....in main use_after_free.cpp:5

Heap Buffer Overflow 탐지:

#include <iostream>
int main() {
    int* arr = new int[10];
    
    // Buffer overflow
    arr[10] = 42;  // 💥 ASan이 탐지 (인덱스 범위 초과)
    
    delete[] arr;
    return 0;
}

Stack Buffer Overflow 탐지:

#include <cstring>
int main() {
    char buffer[10];
    strcpy(buffer, "This is too long!");  // 💥 ASan이 탐지
    return 0;
}

ThreadSanitizer (TSan): 데이터 레이스 탐지

#include <iostream>
#include <thread>
int global_counter = 0;  // 보호되지 않은 공유 변수
void increment() {
    for (int i = 0; i < 100000; ++i) {
        ++global_counter;  // 💥 TSan이 데이터 레이스 탐지
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}
# TSan 활성화 컴파일
g++ -fsanitize=thread -g -O1 data_race.cpp -o test -pthread
# 실행
./test
# 출력 예시:
# ==================
# WARNING: ThreadSanitizer: data race (pid=12345)
#   Write of size 4 at 0x....by thread T2:
#     #0 increment() data_race.cpp:7
#   Previous write of size 4 at 0x....by thread T1:
#     #0 increment() data_race.cpp:7

수정 버전 (뮤텍스 사용):

#include <iostream>
#include <thread>
#include <mutex>
int global_counter = 0;
std::mutex mtx;
void increment() {
    for (int i = 0; i < 100000; ++i) {
        std::lock_guard<std::mutex> lock(mtx);
        ++global_counter;  // ✅ 이제 안전
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    
    t1.join();
    t2.join();
    
    std::cout << "Counter: " << global_counter << std::endl;
    return 0;
}

UndefinedBehaviorSanitizer (UBSan): 정의되지 않은 동작 탐지

#include <iostream>
#include <limits>
int main() {
    // 정수 오버플로우
    int max = std::numeric_limits<int>::max();
    int overflow = max + 1;  // 💥 UBSan이 탐지
    
    // 0으로 나누기
    int x = 10;
    int y = 0;
    int result = x / y;  // 💥 UBSan이 탐지
    
    // 널 포인터 역참조
    int* ptr = nullptr;
    int value = *ptr;  // 💥 UBSan이 탐지
    
    // 잘못된 캐스팅
    class Base { virtual ~Base() {} };
    class Derived : public Base {};
    Base* b = new Base();
    Derived* d = static_cast<Derived*>(b);  // 💥 UBSan이 탐지
    
    return 0;
}
# UBSan 활성화 컴파일
g++ -fsanitize=undefined -g -O1 ub.cpp -o test
# 실행
./test
# 출력 예시:
# ub.cpp:7:20: runtime error: signed integer overflow: 2147483647 + 1 cannot be represented in type 'int'
# ub.cpp:11:19: runtime error: division by zero

MemorySanitizer (MSan): 초기화 안 된 메모리 탐지

#include <iostream>
int main() {
    int x;  // 초기화 안 됨
    
    if (x > 10) {  // 💥 MSan이 탐지
        std::cout << "x is large" << std::endl;
    }
    
    int* arr = new int[10];  // 초기화 안 됨
    std::cout << arr[0] << std::endl;  // 💥 MSan이 탐지
    
    delete[] arr;
    return 0;
}
# MSan 활성화 (Clang만 지원)
clang++ -fsanitize=memory -g -O1 uninit.cpp -o test
# 실행
./test

Sanitizer 조합 사용

# ASan + UBSan 조합 (권장)
g++ -fsanitize=address,undefined -g -O1 program.cpp -o test
# 프로덕션 빌드에는 사용하지 말 것 (성능 오버헤드 큼)
# 개발/테스트 환경에서만 사용

Sanitizer 옵션 설정

# ASan 옵션
export ASAN_OPTIONS=detect_leaks=1:check_initialization_order=1
# TSan 옵션
export TSAN_OPTIONS=second_deadlock_stack=1
# 실행
./test

4. 메모리 누수 추적

Valgrind: 메모리 프로파일링

#include <iostream>
void leakyFunction() {
    int* leak = new int[100];
    // delete[] leak;  // 누락! 💥
}
int main() {
    for (int i = 0; i < 10; ++i) {
        leakyFunction();
    }
    return 0;
}
# Valgrind로 메모리 누수 탐지
g++ -g leak.cpp -o leak
valgrind --leak-check=full --show-leak-kinds=all ./leak
# 출력 예시:
# ==12345== HEAP SUMMARY:
# ==12345==     in use at exit: 4,000 bytes in 10 blocks
# ==12345==   total heap usage: 10 allocs, 0 frees, 4,000 bytes allocated
# ==12345==
# ==12345== 4,000 bytes in 10 blocks are definitely lost in loss record 1 of 1
# ==12345==    at 0x...: operator new
# ==12345==    by 0x...: leakyFunction() (leak.cpp:4)
# ==12345==    by 0x...: main (leak.cpp:9)

shared_ptr 순환 참조 탐지

#include <iostream>
#include <memory>
class Node {
public:
    std::shared_ptr<Node> next;
    ~Node() {
        std::cout << "Node destroyed" << std::endl;
    }
};
int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    // 순환 참조 생성 💥
    node1->next = node2;
    node2->next = node1;
    
    // main 종료 시 소멸자가 호출되지 않음 (메모리 누수)
    return 0;
}

해결: weak_ptr 사용:

#include <iostream>
#include <memory>
class Node {
public:
    std::weak_ptr<Node> next;  // ✅ weak_ptr로 변경
    ~Node() {
        std::cout << "Node destroyed" << std::endl;
    }
};
int main() {
    auto node1 = std::make_shared<Node>();
    auto node2 = std::make_shared<Node>();
    
    node1->next = node2;
    node2->next = node1;
    
    // ✅ 이제 정상적으로 소멸됨
    return 0;
}

ASan으로 메모리 누수 탐지

# ASan은 기본적으로 메모리 누수도 탐지
g++ -fsanitize=address -g leak.cpp -o leak
./leak
# 출력:
# =================================================================
# ==12345==ERROR: LeakSanitizer: detected memory leaks
#
# Direct leak of 400 byte(s) in 1 object(s) allocated from:
#     #0 0x....in operator new
#     #1 0x....in leakyFunction() leak.cpp:4
#     #2 0x....in main leak.cpp:9

Heaptrack: 힙 메모리 프로파일링

# Heaptrack 설치 (Linux)
sudo apt install heaptrack
# 프로그램 실행
heaptrack ./myapp
# 결과 분석
heaptrack_gui heaptrack.myapp.12345.gz

일상 비유로 이해하기: 메모리를 아파트 건물로 생각해보세요. 스택은 엘리베이터 같아서 빠르지만 공간이 제한적입니다. 힙은 창고처럼 넓지만 물건을 찾는 데 시간이 걸립니다. 포인터는 “3층 302호”처럼 주소를 가리키는 메모지라고 보면 됩니다.

5. 멀티스레드

일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. 디버깅

데이터 레이스 실전 시나리오

#include <iostream>
#include <thread>
#include <vector>
class BankAccount {
private:
    int balance = 1000;
    
public:
    void withdraw(int amount) {
        // 💥 데이터 레이스: balance 읽기/쓰기가 원자적이지 않음
        if (balance >= amount) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            balance -= amount;
        }
    }
    
    int getBalance() const { return balance; }
};
int main() {
    BankAccount account;
    std::vector<std::thread> threads;
    
    // 10개 스레드가 동시에 100원씩 인출
    for (int i = 0; i < 10; ++i) {
        threads.emplace_back([&account]() {
            for (int j = 0; j < 10; ++j) {
                account.withdraw(100);
            }
        });
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    // 예상: 1000 - (10 * 10 * 100) = -9000 또는 음수
    // 실제: 매번 다른 값 (데이터 레이스)
    std::cout << "Final balance: " << account.getBalance() << std::endl;
    
    return 0;
}

TSan으로 탐지:

g++ -fsanitize=thread -g bank.cpp -o bank -pthread
./bank
# WARNING: ThreadSanitizer: data race

해결: 뮤텍스 사용:

#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
class BankAccount {
private:
    int balance = 1000;
    std::mutex mtx;  // ✅ 뮤텍스 추가
    
public:
    void withdraw(int amount) {
        std::lock_guard<std::mutex> lock(mtx);  // ✅ 락 획득
        if (balance >= amount) {
            std::this_thread::sleep_for(std::chrono::milliseconds(1));
            balance -= amount;
        }
    }
    
    int getBalance() {
        std::lock_guard<std::mutex> lock(mtx);
        return balance;
    }
};

데드락 탐지

#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread1() {
    std::lock_guard<std::mutex> lock1(mutex1);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock2(mutex2);  // 💥 데드락
    std::cout << "Thread 1" << std::endl;
}
void thread2() {
    std::lock_guard<std::mutex> lock2(mutex2);
    std::this_thread::sleep_for(std::chrono::milliseconds(10));
    std::lock_guard<std::mutex> lock1(mutex1);  // 💥 데드락
    std::cout << "Thread 2" << std::endl;
}
int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    
    t1.join();
    t2.join();
    
    return 0;
}

GDB로 데드락 분석:

# 프로그램이 멈추면 Ctrl+C로 중단
g++ -g -pthread deadlock.cpp -o deadlock
./deadlock
# (멈춤)
# 다른 터미널에서:
gdb -p $(pidof deadlock)
(gdb) info threads
(gdb) thread apply all backtrace
# 각 스레드가 어떤 락을 기다리는지 확인

해결: std::scoped_lock (C++17):

#include <iostream>
#include <thread>
#include <mutex>
std::mutex mutex1, mutex2;
void thread1() {
    std::scoped_lock lock(mutex1, mutex2);  // ✅ 데드락 방지
    std::cout << "Thread 1" << std::endl;
}
void thread2() {
    std::scoped_lock lock(mutex1, mutex2);  // ✅ 항상 같은 순서로 락 획득
    std::cout << "Thread 2" << std::endl;
}
int main() {
    std::thread t1(thread1);
    std::thread t2(thread2);
    
    t1.join();
    t2.join();
    
    return 0;
}

반복자 무효화 디버깅

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // 💥 반복자 무효화
    for (auto it = vec.begin(); it != vec.end(); ++it) {
        if (*it % 2 == 0) {
            vec.erase(it);  // erase 후 it가 무효화됨
        }
    }
    
    return 0;
}

GDB Watchpoint로 추적:

(gdb) break main
(gdb) run
(gdb) watch vec._M_impl._M_start  # vector 내부 포인터 감시
(gdb) continue
# erase 호출 시 중단됨

해결: erase-remove idiom:

#include <iostream>
#include <vector>
#include <algorithm>
int main() {
    std::vector<int> vec = {1, 2, 3, 4, 5};
    
    // ✅ erase-remove idiom
    vec.erase(
        std::remove_if(vec.begin(), vec.end(),
             { return x % 2 == 0; }),
        vec.end()
    );
    
    // 또는 C++20 erase_if
    // std::erase_if(vec,  { return x % 2 == 0; });
    
    return 0;
}

6. 프로덕션 환경 디버깅

구조화된 로깅

#include <iostream>
#include <fstream>
#include <chrono>
#include <iomanip>
#include <sstream>
#include <mutex>
class Logger {
public:
    enum Level { DEBUG, INFO, WARNING, ERROR, FATAL };
    
private:
    static std::mutex mtx_;
    static std::ofstream file_;
    static Level min_level_;
    
public:
    static void init(const std::string& filename, Level min_level = INFO) {
        file_.open(filename, std::ios::app);
        min_level_ = min_level;
    }
    
    static void log(Level level, const std::string& message,
                    const char* file = __builtin_FILE(),
                    int line = __builtin_LINE(),
                    const char* func = __builtin_FUNCTION()) {
        if (level < min_level_) return;
        
        std::lock_guard<std::mutex> lock(mtx_);
        
        auto now = std::chrono::system_clock::now();
        auto time = std::chrono::system_clock::to_time_t(now);
        auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
            now.time_since_epoch()) % 1000;
        
        std::ostringstream oss;
        oss << std::put_time(std::localtime(&time), "%Y-%m-%d %H:%M:%S")
            << "." << std::setfill('0') << std::setw(3) << ms.count()
            << " [" << levelToString(level) << "] "
            << "[" << file << ":" << line << "] "
            << "[" << func << "] "
            << message << std::endl;
        
        std::string log_line = oss.str();
        std::cout << log_line;
        if (file_.is_open()) {
            file_ << log_line;
            file_.flush();
        }
    }
    
private:
    static const char* levelToString(Level level) {
        switch (level) {
            case DEBUG:   return "DEBUG";
            case INFO:    return "INFO";
            case WARNING: return "WARN";
            case ERROR:   return "ERROR";
            case FATAL:   return "FATAL";
            default:      return "UNKNOWN";
        }
    }
};
std::mutex Logger::mtx_;
std::ofstream Logger::file_;
Logger::Level Logger::min_level_ = Logger::INFO;
// 매크로로 편리하게 사용
#define LOG_DEBUG(msg) Logger::log(Logger::DEBUG, msg, __FILE__, __LINE__, __func__)
#define LOG_INFO(msg) Logger::log(Logger::INFO, msg, __FILE__, __LINE__, __func__)
#define LOG_ERROR(msg) Logger::log(Logger::ERROR, msg, __FILE__, __LINE__, __func__)
int main() {
    Logger::init("app.log", Logger::DEBUG);
    
    LOG_INFO("프로그램 시작");
    LOG_DEBUG("디버그 정보");
    LOG_ERROR("에러 발생");
    
    return 0;
}

코어 덤프 자동 수집

#!/bin/bash
# core_dump_setup.sh
# 코어 덤프 활성화
ulimit -c unlimited
# 코어 덤프 파일 위치 설정
echo "/var/crash/core.%e.%p.%t" | sudo tee /proc/sys/kernel/core_pattern
# 프로그램 실행
./myapp
# 크래시 발생 시 /var/crash/에 코어 덤프 생성

원격 디버깅 (gdbserver)

# 서버 (프로덕션 환경)
gdbserver :1234 ./myapp
# 클라이언트 (개발 환경)
gdb ./myapp
(gdb) target remote server_ip:1234
(gdb) continue

프로덕션 크래시 리포트

#include <csignal>
#include <cstdlib>
#include <iostream>
#include <execinfo.h>
#include <unistd.h>
void signalHandler(int sig) {
    std::cerr << "Error: signal " << sig << std::endl;
    
    // 스택 트레이스 출력
    void* array[10];
    size_t size = backtrace(array, 10);
    
    std::cerr << "Stack trace:" << std::endl;
    backtrace_symbols_fd(array, size, STDERR_FILENO);
    
    exit(1);
}
int main() {
    // 시그널 핸들러 등록
    signal(SIGSEGV, signalHandler);
    signal(SIGABRT, signalHandler);
    
    // 프로그램 로직
    int* ptr = nullptr;
    *ptr = 42;  // 크래시 발생 시 스택 트레이스 출력
    
    return 0;
}

7. 완전한 디버깅 워크플로우

단계별 디버깅 프로세스

flowchart TB
    Start[버그 발견] --> Reproduce[재현 가능한가?]
    Reproduce -->|Yes| Minimal[최소 재현 코드 작성]
    Reproduce -->|No| Logging[로깅 추가]
    Logging --> Reproduce
    
    Minimal --> Hypothesis[가설 수립]
    Hypothesis --> Tool{도구 선택}
    
    Tool -->|메모리 오류| ASan[AddressSanitizer]
    Tool -->|데이터 레이스| TSan[ThreadSanitizer]
    Tool -->|정의되지 않은 동작| UBSan[UBSan]
    Tool -->|일반 디버깅| GDB[GDB/LLDB]
    
    ASan --> Verify[검증]
    TSan --> Verify
    UBSan --> Verify
    GDB --> Verify
    
    Verify -->|해결| Test[테스트 작성]
    Verify -->|미해결| Hypothesis
    
    Test --> Done[완료]

실전 예제: 복합 버그 디버깅

#include <iostream>
#include <thread>
#include <vector>
#include <memory>
class Resource {
public:
    int* data;
    
    Resource() : data(new int[100]) {
        std::cout << "Resource created" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource destroyed" << std::endl;
    }
};
std::shared_ptr<Resource> global_resource;
void worker() {
    // 💥 여러 버그가 섞여 있음
    for (int i = 0; i < 1000; ++i) {
        if (!global_resource) {
            global_resource = std::make_shared<Resource>();
        }
        
        // 데이터 레이스
        global_resource->data[i % 100] = i;
        
        if (i == 500) {
            global_resource.reset();  // 다른 스레드가 사용 중일 수 있음
        }
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

디버깅 단계:

# 1단계: TSan으로 데이터 레이스 탐지
g++ -fsanitize=thread -g bug.cpp -o bug -pthread
./bug
# WARNING: ThreadSanitizer: data race
# 2단계: ASan으로 메모리 오류 탐지
g++ -fsanitize=address -g bug.cpp -o bug -pthread
./bug
# ERROR: AddressSanitizer: heap-use-after-free
# 3단계: GDB로 상세 분석
g++ -g bug.cpp -o bug -pthread
gdb ./bug
(gdb) break worker
(gdb) run
(gdb) info threads
(gdb) thread apply all backtrace

수정 버전:

#include <iostream>
#include <thread>
#include <vector>
#include <memory>
#include <mutex>
class Resource {
public:
    int* data;
    
    Resource() : data(new int[100]) {
        std::cout << "Resource created" << std::endl;
    }
    
    ~Resource() {
        delete[] data;
        std::cout << "Resource destroyed" << std::endl;
    }
};
std::shared_ptr<Resource> global_resource;
std::mutex resource_mutex;
void worker() {
    for (int i = 0; i < 1000; ++i) {
        std::shared_ptr<Resource> local_resource;
        
        {
            std::lock_guard<std::mutex> lock(resource_mutex);
            if (!global_resource) {
                global_resource = std::make_shared<Resource>();
            }
            local_resource = global_resource;  // ✅ 로컬 복사본 유지
        }
        
        // ✅ 이제 안전하게 접근 가능
        local_resource->data[i % 100] = i;
    }
}
int main() {
    std::vector<std::thread> threads;
    for (int i = 0; i < 4; ++i) {
        threads.emplace_back(worker);
    }
    
    for (auto& t : threads) {
        t.join();
    }
    
    return 0;
}

8. 자주 발생하는 실수와 해결법

실수 1: 디버그 심볼 없이 컴파일

# ❌ 잘못된 방법
g++ -O2 program.cpp -o program
# ✅ 올바른 방법
g++ -g -O0 program.cpp -o program  # 디버그 빌드
g++ -g -O2 program.cpp -o program  # 릴리스 빌드 (디버그 심볼 포함)

실수 2: 최적화로 인한 변수 최적화

int main() {
    int x = 10;
    int y = x + 5;  // 컴파일러가 y = 15로 최적화
    return y;
}
# GDB에서 x를 출력하려 하면 "optimized out" 메시지
(gdb) print x
# $1 = <optimized out>
# 해결: -O0으로 컴파일
g++ -g -O0 program.cpp -o program

실수 3: Sanitizer와 최적화 레벨

# ❌ -O0은 일부 버그를 숨길 수 있음
g++ -fsanitize=address -g -O0 program.cpp
# ✅ -O1 또는 -O2 권장 (Sanitizer 공식 권장사항)
g++ -fsanitize=address -g -O1 program.cpp

실수 4: 멀티스레드 프로그램에서 -pthread 누락

# ❌ 링크 에러 또는 런타임 오류
g++ -fsanitize=thread -g program.cpp
# ✅ -pthread 추가
g++ -fsanitize=thread -g program.cpp -pthread

실수 5: 코어 덤프 크기 제한

# 코어 덤프가 생성되지 않으면 확인
ulimit -c
# 0이면 비활성화 상태
# 무제한으로 설정
ulimit -c unlimited
# 영구 설정 (/etc/security/limits.conf에 추가)
# * soft core unlimited
# * hard core unlimited

실수 6: Valgrind와 ASan 동시 사용

# ❌ 충돌 발생
g++ -fsanitize=address program.cpp -o program
valgrind ./program
# ✅ 하나만 사용
# ASan 사용 시
g++ -fsanitize=address program.cpp -o program
./program
# Valgrind 사용 시
g++ -g program.cpp -o program
valgrind --leak-check=full ./program

9. 모범 사례·베스트 프랙티스

개발 환경 설정

# CMakeLists.txt에 디버그 옵션 추가
if(CMAKE_BUILD_TYPE MATCHES Debug)
    add_compile_options(-g -O0)
    add_compile_options(-fsanitize=address,undefined)
    add_link_options(-fsanitize=address,undefined)
endif()
if(CMAKE_BUILD_TYPE MATCHES RelWithDebInfo)
    add_compile_options(-g -O2)
endif()

CI/CD에 Sanitizer 통합

# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      
      - name: Build with ASan
        run: |
          mkdir build
          cd build
          cmake -DCMAKE_BUILD_TYPE=Debug ..
          make
      
      - name: Run tests
        run: |
          cd build
          export ASAN_OPTIONS=detect_leaks=1
          ctest --output-on-failure

어설션 전략

#include <cassert>
#include <iostream>
// 디버그 빌드에서만 활성화되는 어설션
#ifdef NDEBUG
    #define DEBUG_ASSERT(condition, message) ((void)0)
#else
    #define DEBUG_ASSERT(condition, message) \
        if (!(condition)) { \
            std::cerr << "Assertion failed: " << message << std::endl; \
            std::cerr << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
            std::abort(); \
        }
#endif
// 릴리스 빌드에서도 활성화되는 어설션
#define RELEASE_ASSERT(condition, message) \
    if (!(condition)) { \
        std::cerr << "Fatal error: " << message << std::endl; \
        std::cerr << "File: " << __FILE__ << ", Line: " << __LINE__ << std::endl; \
        std::abort(); \
    }
int main() {
    int x = 10;
    
    // 개발 중에만 체크
    DEBUG_ASSERT(x > 0, "x must be positive");
    
    // 항상 체크 (중요한 불변 조건)
    RELEASE_ASSERT(x < 100, "x must be less than 100");
    
    return 0;
}

로깅 레벨 전략

// 개발: DEBUG 레벨
Logger::init("app.log", Logger::DEBUG);
// 스테이징: INFO 레벨
Logger::init("app.log", Logger::INFO);
// 프로덕션: WARNING 레벨
Logger::init("app.log", Logger::WARNING);

테스트 주도 디버깅

#include <cassert>
#include <iostream>
// 버그 재현 테스트 작성
void test_divide_by_zero() {
    try {
        int result = divide(10, 0);
        assert(false && "Should throw exception");
    } catch (const std::invalid_argument& e) {
        std::cout << "Test passed: " << e.what() << std::endl;
    }
}
int main() {
    test_divide_by_zero();
    return 0;
}

10. 프로덕션 패턴

패턴 1: 헬스 체크 엔드포인트

#include <iostream>
#include <chrono>
#include <thread>
#include <atomic>
class HealthMonitor {
private:
    std::atomic<bool> is_healthy_{true};
    std::chrono::steady_clock::time_point last_heartbeat_;
    
public:
    void heartbeat() {
        last_heartbeat_ = std::chrono::steady_clock::now();
    }
    
    bool isHealthy() {
        auto now = std::chrono::steady_clock::now();
        auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
            now - last_heartbeat_).count();
        
        // 10초 이상 heartbeat 없으면 비정상
        return elapsed < 10;
    }
    
    void setUnhealthy() {
        is_healthy_ = false;
    }
};
// HTTP 서버에서 /health 엔드포인트 제공
// GET /health -> {"status": "ok", "uptime": 12345}

패턴 2: 메트릭 수집

#include <iostream>
#include <atomic>
#include <chrono>
class Metrics {
private:
    std::atomic<uint64_t> request_count_{0};
    std::atomic<uint64_t> error_count_{0};
    std::atomic<uint64_t> total_latency_ms_{0};
    
public:
    void recordRequest(uint64_t latency_ms, bool is_error = false) {
        ++request_count_;
        total_latency_ms_ += latency_ms;
        if (is_error) {
            ++error_count_;
        }
    }
    
    void report() {
        uint64_t requests = request_count_.load();
        uint64_t errors = error_count_.load();
        uint64_t latency = total_latency_ms_.load();
        
        std::cout << "Requests: " << requests << std::endl;
        std::cout << "Errors: " << errors << std::endl;
        if (requests > 0) {
            std::cout << "Avg latency: " << (latency / requests) << "ms" << std::endl;
            std::cout << "Error rate: " << (errors * 100.0 / requests) << "%" << std::endl;
        }
    }
};

패턴 3: 그레이스풀 셧다운

#include <csignal>
#include <atomic>
#include <iostream>
#include <thread>
std::atomic<bool> shutdown_requested{false};
void signalHandler(int signal) {
    if (signal == SIGINT || signal == SIGTERM) {
        std::cout << "Shutdown requested..." << std::endl;
        shutdown_requested = true;
    }
}
int main() {
    signal(SIGINT, signalHandler);
    signal(SIGTERM, signalHandler);
    
    std::cout << "Server started. Press Ctrl+C to stop." << std::endl;
    
    while (!shutdown_requested) {
        // 메인 루프
        std::this_thread::sleep_for(std::chrono::seconds(1));
    }
    
    std::cout << "Shutting down gracefully..." << std::endl;
    // 리소스 정리
    
    std::cout << "Shutdown complete." << std::endl;
    return 0;
}

패턴 4: 순환 버퍼 로깅

#include <array>
#include <string>
#include <mutex>
template<size_t N>
class CircularLogBuffer {
private:
    std::array<std::string, N> buffer_;
    size_t index_ = 0;
    std::mutex mtx_;
    
public:
    void log(const std::string& message) {
        std::lock_guard<std::mutex> lock(mtx_);
        buffer_[index_] = message;
        index_ = (index_ + 1) % N;
    }
    
    void dump() {
        std::lock_guard<std::mutex> lock(mtx_);
        std::cout << "=== Last " << N << " log entries ===" << std::endl;
        for (size_t i = 0; i < N; ++i) {
            size_t idx = (index_ + i) % N;
            if (!buffer_[idx].empty()) {
                std::cout << buffer_[idx] << std::endl;
            }
        }
    }
};
// 크래시 시 최근 로그만 덤프
CircularLogBuffer<100> crash_log;

11. 정리 및 체크리스트

디버깅 도구 선택 가이드

문제 유형추천 도구사용 시기
메모리 누수ASan, Valgrind개발/테스트
데이터 레이스TSan멀티스레드 개발
정의되지 않은 동작UBSan모든 빌드
일반 크래시GDB/LLDB개발 중
프로덕션 크래시코어 덤프 + GDB프로덕션
성능 병목perf, gprof최적화 단계
초기화 안 된 메모리MSanClang 환경

Sanitizer 성능 비교

도구속도 오버헤드메모리 오버헤드탐지 범위컴파일러 지원
ASan2x2~3x메모리 오류, 누수GCC, Clang, MSVC
TSan5~15x5~10x데이터 레이스GCC, Clang
UBSan1.2x최소정의되지 않은 동작GCC, Clang, MSVC
MSan3x2x초기화 안 된 메모리Clang만
Valgrind10~50x최소메모리 전반모든 바이너리
권장 조합: ASan + UBSan (일상 개발), TSan (멀티스레드), Valgrind (정밀 분석)

개발 환경 체크리스트

# ✅ 디버그 빌드 설정
- [ ] -g 플래그 추가
- [ ] -O0 또는 -O1 사용
- [ ] Sanitizer 활성화 (-fsanitize=address,undefined)
- [ ] 컴파일 경고 최대화 (-Wall -Wextra -Wpedantic)
# ✅ 테스트 환경
- [ ] 단위 테스트 작성
- [ ] CI/CD에 Sanitizer 통합
- [ ] 코드 커버리지 측정
- [ ] Fuzzing 테스트 (AFL, libFuzzer)
# ✅ 프로덕션 준비
- [ ] 로깅 시스템 구축 (레벨별, 파일 로테이션)
- [ ] 코어 덤프 활성화 (ulimit -c unlimited)
- [ ] 헬스 체크 엔드포인트
- [ ] 메트릭 수집 (요청 수, 에러율, 레이턴시)
- [ ] 그레이스풀 셧다운 (SIGTERM 핸들러)
- [ ] 모니터링 알림 (Prometheus, Grafana)

실전 팁: 디버깅 시간 단축

  1. 항상 Sanitizer와 함께 개발
   # .bashrc 또는 .zshrc에 추가
   alias g++debug='g++ -g -O1 -fsanitize=address,undefined -Wall -Wextra'
   
   # 사용
   g++debug myapp.cpp -o myapp
  1. GDB 설정 파일 (.gdbinit)
   # ~/.gdbinit
# 실행 예제
   set print pretty on
   set print array on
   set print array-indexes on
   set pagination off
   
   # 자주 쓰는 명령어 단축
   define pv
       print $arg0
   end
  1. 빠른 재현 스크립트
   #!/bin/bash
   # reproduce_bug.sh
   
   # 디버그 빌드
   g++ -g -O0 -fsanitize=address bug.cpp -o bug
   
   # 여러 번 실행 (재현 확인)
   for i in {1..10}; do
       echo "Run $i"
       ./bug || break
   done
  1. 로그 레벨 동적 변경
   // 환경 변수로 로그 레벨 제어
   const char* log_level = std::getenv("LOG_LEVEL");
   if (log_level && std::string(log_level) == "DEBUG") {
       Logger::setLevel(Logger::DEBUG);
   }
   
   // 실행 시
   // LOG_LEVEL=DEBUG ./myapp

디버깅 워크플로우 요약

  1. 재현: 버그를 재현 가능한 최소 코드로 만들기
  2. 가설: 원인 가설 수립
  3. 도구: 적절한 디버깅 도구 선택
  4. 분석: GDB, Sanitizer로 원인 분석
  5. 수정: 버그 수정
  6. 검증: 테스트 작성 및 실행
  7. 문서화: 버그 원인과 해결법 기록

빠른 참조: 디버깅 체크리스트

# 🔍 버그 발견 시 즉시 확인할 것
 재현 가능한가? (재현 불가 로깅 추가)
 최소 재현 코드 작성 완료?
 컴파일 경고 모두 확인? (gcc -Wall -Wextra)
 Sanitizer 활성화? (ASan + UBSan)
# 🛠️ 도구 선택
 메모리 오류 ASan 또는 Valgrind
 데이터 레이스 TSan
 크래시 GDB + 코어 덤프
 성능 병목 perf, gprof
# ✅ 해결 후
 테스트 케이스 작성
 코드 리뷰 요청
 문서화 (버그 원인과 해결법)

트러블슈팅: 빠른 문제 해결

증상원인해결법
Segfaultnullptr 역참조, 배열 범위 초과ASan 활성화, GDB로 스택 확인
메모리 누수delete 누락, 순환 참조Valgrind 또는 ASan leak 탐지
데이터 레이스동기화 누락TSan 활성화, 뮤텍스 추가
데드락순환 락 대기GDB로 스레드 스택 확인, scoped_lock 사용
랜덤 크래시UB, 초기화 안 된 변수UBSan, MSan 활성화
느린 빌드PCH 미사용, 단일 스레드ccache, Ninja, 병렬 빌드

다음 단계


FAQ

Q1: 디버깅을 어디서부터 시작해야 하나요?

A:

  1. 버그를 재현 가능한 최소 코드로 만들기
  2. ASan + UBSan으로 컴파일해서 실행 (대부분의 버그 자동 탐지)
  3. 여전히 문제가 있으면 GDB로 단계별 실행

Q2: GDB vs LLDB 어떤 걸 써야 하나요?

A:

  • Linux: GDB (표준)
  • macOS: LLDB (기본 제공, Xcode 통합)
  • Windows: Visual Studio Debugger 또는 GDB (MinGW)
  • 명령어는 거의 유사하므로 하나만 익히면 됩니다.

Q3: 프로덕션에서 Sanitizer를 사용해도 되나요?

A:

  • ❌ 권장하지 않음: 성능 오버헤드가 크고 (2~5배), 메모리 사용량 증가
  • 개발/테스트 환경에서만 사용
  • 프로덕션에서는 로깅 + 코어 덤프 + 모니터링 사용

Q4: Valgrind vs ASan 어떤 게 더 좋나요?

A:

  • ASan: 빠름 (2~3배 느림), 컴파일 타임에 통합, 더 많은 버그 탐지
  • Valgrind: 느림 (10~50배 느림), 별도 실행, 정밀한 메모리 추적
  • 권장: 일상적으로는 ASan, 정밀 분석이 필요하면 Valgrind

Q5: 멀티스레드 버그를 어떻게 재현하나요?

A:

  • TSan으로 컴파일 (대부분의 레이스 자동 탐지)
  • std::this_thread::sleep_for로 타이밍 조절
  • 스레드 수를 늘려서 경합 증가
  • stress 도구로 부하 생성

Q6: 디버깅 학습 리소스는?

A:

  • : “The Art of Debugging with GDB, DDD, and Eclipse”
  • 문서: GDB 공식 문서, Sanitizer 문서
  • 실습: 의도적으로 버그를 만들고 디버깅 연습
  • 오픈소스: 유명 프로젝트의 이슈 트래커에서 버그 수정 과정 학습

Q7: 프로덕션 환경에서 디버깅할 때 주의사항은?

A:

  • 성능 영향 최소화: 로깅 레벨을 적절히 설정 (WARNING 이상만)
  • 민감 정보 보호: 비밀번호, API 키 등을 로그에 남기지 않기
  • 코어 덤프 크기 제한: ulimit -c 설정으로 디스크 공간 관리
  • 원격 디버깅 보안: gdbserver 사용 시 방화벽 설정
  • 재현 환경 구축: 프로덕션과 동일한 환경에서 테스트

Q8: ASan을 켰는데 메모리 사용량이 너무 많아요

A: ASan은 메모리 사용량을 2~3배 증가시킵니다. 이는 정상입니다.

  • 개발/테스트 환경에서만 사용하세요
  • 메모리가 부족하면 작은 테스트 케이스로 분할
  • 또는 Valgrind 사용 (느리지만 메모리 사용량 적음)
  • CI/CD에서는 메모리 충분한 인스턴스 사용

Q9: GDB에서 “optimized out” 메시지가 나와요

A: 컴파일러 최적화로 변수가 제거되었습니다.

# 해결법: 최적화 비활성화
g++ -g -O0 program.cpp  # -O0 사용
# 또는 특정 함수만 최적화 비활성화
__attribute__((optimize("O0")))
void debugFunction() {
    // 함수는 최적화되지 않음
}

Q10: 데드락을 어떻게 예방하나요?

A:

  • 락 순서 일관성: 항상 같은 순서로 락 획득
  • std::scoped_lock 사용 (C++17, 데드락 방지)
  • 타임아웃 설정: try_lock_for() 사용
  • 락 홀딩 시간 최소화: 락 안에서 긴 작업 피하기
  • TSan으로 검증: 개발 중 항상 TSan 활성화

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

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


관련 글

심화 부록: 구현·운영 관점

이 부록은 앞선 본문에서 다룬 주제(「C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(I/O·네트워크·동시성) → 관측의 흐름으로 장애를 나누면 원인 추적이 빨라집니다.

내부 동작과 핵심 메커니즘

flowchart TD
  A[입력·요청·이벤트] --> B[파싱·검증·디코딩]
  B --> C[핵심 연산·상태 전이]
  C --> D[부작용: I/O·네트워크·동시성]
  D --> E[결과·관측·저장]
sequenceDiagram
  participant C as 클라이언트/호출자
  participant B as 경계(런타임·게이트웨이·프로세스)
  participant D as 의존성(API·DB·큐·파일)
  C->>B: 요청/이벤트
  B->>D: 조회·쓰기·RPC
  D-->>B: 지연·부분 실패·재시도 가능
  B-->>C: 응답 또는 오류(코드·상관 ID)
  • 불변 조건(Invariant): 버퍼 경계, 프로토콜 상태, 트랜잭션 격리, FD 상한 등 단계별로 문장으로 적어 두면 디버깅 비용이 줄어듭니다.
  • 결정성: 순수 층과 시간·네트워크·스케줄에 의존하는 층을 분리해야 테스트와 장애 분석이 쉬워집니다.
  • 경계 비용: 직렬화, 인코딩, syscall 횟수, 락 경합, 할당·GC, 캐시 미스를 의심 목록에 둡니다.
  • 백프레셔: 생산자가 소비자보다 빠를 때 버퍼·큐·스트림에서 속도를 줄이는 신호를 어디에 둘지 정의합니다.

프로덕션 운영 패턴

영역운영 관점 질문
관측성요청 단위 상관 ID, 에러율·지연 p95/p99, 의존성 타임아웃·재시도가 대시보드에 보이는가
안전성입력 검증·권한·비밀·감사 로그가 코드 경로마다 일관적인가
신뢰성재시도는 멱등 연산에만 적용되는가, 서킷 브레이커·백오프·DLQ가 있는가
성능캐시·배치 크기·커넥션 풀·인덱스·백프레셔가 데이터 규모에 맞는가
배포롤백 룬북, 카나리/블루그린, 마이그레이션·피처 플래그가 문서화되어 있는가
용량피크 트래픽·디스크·FD·스레드 풀 상한을 주기적으로 검증하는가

스테이징은 데이터 양·네트워크 RTT·동시성을 프로덕션에 가깝게 맞출수록 재현율이 올라갑니다.

확장 예시: 엔드투엔드 미니 시나리오

앞선 본문 주제(「C++ 디버깅 완벽 가이드 | GDB·Sanitizer·메모리 누수·멀티스레드 디버깅 실전」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  1. 입력 계약 고정: 스키마·버전·최대 페이로드·타임아웃·에러 코드를 경계에 둔다.
  2. 핵심 경로 계측: 요청 ID, 단계별 지연, 외부 호출 결과 코드를 로그·메트릭·트레이스에서 한 흐름으로 본다.
  3. 실패 주입: 의존성 타임아웃·5xx·부분 데이터·락 대기를 스테이징에서 재현한다.
  4. 호환·롤백: 설정/마이그레이션/클라이언트 버전을 되돌릴 수 있는지 확인한다.
  5. 부하 후 검증: 피크 대비 p95/p99, 에러율, 리소스 상한, 알림 임계값을 점검한다.
handle(request):
  ctx = newCorrelationId()
  validated = validateSchema(request)
  authorize(validated, ctx)
  result = domainCore(validated)
  persistOrEmit(result, idempotentKey)
  recordMetrics(ctx, latency, outcome)
  return result

문제 해결(Troubleshooting)

증상가능 원인조치
간헐적 실패레이스, 타임아웃, 외부 의존성, DNS최소 재현 스크립트, 분산 트레이스·로그 상관관계, 재시도·서킷 설정 점검
성능 저하N+1, 동기 I/O, 락 경합, 과도한 직렬화, 캐시 미스프로파일러·APM으로 핫스팟 확인 후 한 가지씩 제거
메모리 증가캐시 무제한, 구독/리스너 누수, 대용량 버퍼, 커넥션 미반납상한·TTL·힙/FD 스냅샷 비교
빌드·배포만 실패환경 변수, 권한, 플랫폼 차이, lockfileCI 로그와 로컬 diff, 런타임·이미지 버전 핀
설정 불일치프로필·시크릿·기본값, 리전스키마 검증된 설정 단일 소스와 배포 매트릭스 표준화
데이터 불일치비멱등 재시도, 부분 쓰기, 캐시 무효화 누락멱등 키·아웃박스·트랜잭션 경계 재검토

권장 순서: (1) 최소 재현 (2) 최근 변경 범위 축소 (3) 환경·의존성 차이 (4) 관측으로 가설 검증 (5) 수정 후 회귀·부하 테스트.

배포 전에는 git addgit commitgit pushnpm run deploy 순서를 권장합니다.


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

C++, debugging, gdb, lldb, sanitizer, valgrind, 메모리누수 등으로 검색하시면 이 글이 도움이 됩니다.