본문으로 건너뛰기
Previous
Next
C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]

C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]

C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]

이 글의 핵심

C++ 디버깅: GDB/LLDB 완전 예제, AddressSanitizer·ThreadSanitizer, 구조화 로깅, 문제 시나리오, 흔한 에러, 모범 사례, 프로덕션 패턴까지 실전 코드로 다룹니다.

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

"개발 PC에서는 잘 되는데, 고객 환경에서만 Segmentation fault가 발생해요”

C++는 성능과 제어력이 뛰어나지만, 메모리 관리, 동시성, 포인터 오류로 인해 디버깅이 까다롭습니다. “내 PC에서는 절대 안 나는데”라는 말은 C++ 개발자에게 익숙한 고민입니다. 비유하면 “집에서는 잘 작동하는 자동차가 고객 차고에서는 시동이 안 걸리는 것”과 같습니다. 환경 차이, 타이밍, 메모리 레이아웃이 미묘하게 달라서 버그가 드러나거나 숨을 수 있습니다. 문제의 핵심:

  • 크래시 덤프가 없거나, 있어도 심볼이 없어 분석이 어렵습니다
  • 데이터 레이스는 재현이 불규칙하고, “가끔”만 발생합니다
  • 메모리 오류(버퍼 오버플로우, use-after-free)는 즉시 크래시하지 않고 나중에 터질 수 있습니다
  • 로그가 부족하면 “어디서” 문제가 났는지 좁히기 어렵습니다 이 글에서 다루는 것:
  • 문제 시나리오: 실제 겪는 디버깅 상황
  • GDB/LLDB 완전 예제: 브레이크포인트, 백트레이스, 변수 검사, 코어 덤프 분석
  • AddressSanitizer(ASan): 메모리 오류 조기 발견
  • ThreadSanitizer(TSan): 데이터 레이스 탐지
  • 구조화 로깅: 프로덕션 추적 패턴
  • 자주 발생하는 에러와 해결법
  • 모범 사례프로덕션 패턴 요구 환경: C++17 이상, Linux/macOS (GDB/LLDB), GCC 또는 Clang 이 글을 읽으면:
  • GDB/LLDB로 크래시를 분석하고 원인을 찾을 수 있습니다
  • ASan/TSan으로 개발 단계에서 버그를 조기 발견할 수 있습니다
  • 프로덕션 수준의 로깅과 덤프 수집 패턴을 적용할 수 있습니다

실무 적용 경험: 이 글은 대규모 C++ 프로젝트에서 실제로 겪은 문제와 해결 과정을 바탕으로 작성되었습니다. 책이나 문서에서 다루지 않는 실전 함정과 디버깅 팁을 포함합니다.

1. 문제 시나리오

시나리오 1: 프로덕션에서 가끔 Segmentation fault

상황: 서버가 24시간 운영 중 가끔(하루 1~2회) 크래시합니다. 재현 조건을 알 수 없고, 크래시 시점의 로그만 있습니다. 해결: 코어 덤프를 활성화하고, GDB로 core 파일을 열어 bt full로 백트레이스를 확인합니다. 릴리즈 빌드에도 디버그 심볼을 분리해 보관하면 분석이 가능합니다.

시나리오 2: “가끔” 잘못된 값이 나와요 (데이터 레이스)

상황: 멀티스레드 앱에서 공유 변수가 가끔 이상한 값으로 바뀝니다. 단일 스레드로 실행하면 문제가 없습니다. 해결: ThreadSanitizer(TSan)로 빌드해 실행하면 데이터 레이스 발생 지점을 정확히 보고합니다. -fsanitize=thread로 컴파일합니다.

시나리오 3: 메모리 누수로 장시간 실행 후 OOM

상황: 서버가 3일째 되면 메모리 사용량이 계속 증가해 OOM으로 죽습니다. 어디서 누수가 나는지 모릅니다. 해결: LeakSanitizer(LSan, ASan에 포함)로 실행해 누수 지점을 찾습니다. 또는 Valgrind의 memcheck --leak-check=full을 사용합니다.

시나리오 4: 버퍼 오버플로우가 나중에 터짐

상황: strcpy나 배열 인덱스 오류로 인해 즉시 크래시하지 않고, 나중에 다른 함수에서 “이상한” 크래시가 납니다. 해결: AddressSanitizer로 빌드하면 오버플로우/언더플로우/use-after-free를 즉시 탐지합니다. 개발·CI에서 ASan 빌드를 돌립니다.

시나리오 5: 고객 PC에서만 크래시, 덤프 없음

상황: 고객 환경에서만 크래시하는데, 덤프 수집이 안 되어 원인 분석이 불가능합니다. 해결: 앱에 크래시 핸들러를 넣어 abort 시 자체 덤프를 파일로 저장하고, minidump 형식으로 전송받도록 합니다. 또는 Breakpad/Crashpad를 통합합니다.

디버깅 도구 선택 흐름

flowchart TB
    subgraph problem[문제 유형]
        P1[크래시·Segfault]
        P2[데이터 레이스]
        P3[메모리 오류]
        P4[메모리 누수]
        P5[로직 버그]
    end
    subgraph tool[권장 도구]
        T1[GDB/LLDB + 코어덤프]
        T2[ThreadSanitizer]
        T3[AddressSanitizer]
        T4[LeakSanitizer/Valgrind]
        T5[브레이크포인트·로깅]
    end
    P1 --> T1
    P2 --> T2
    P3 --> T3
    P4 --> T4
    P5 --> T5

2. GDB 완전 예제

환경: Linux, GCC/Clang

GDB(GNU Debugger)는 Linux에서 가장 널리 쓰이는 디버거입니다. 크래시 분석, 브레이크포인트, 변수 검사, 조건부 중단을 지원합니다.

디버그 빌드 준비

# 디버그 심볼 포함 빌드 (-g)
g++ -std=c++17 -g -O0 -o myapp main.cpp
# 릴리즈 + 분리된 디버그 심볼 (프로덕션 분석용)
g++ -std=c++17 -O2 -g -o myapp main.cpp
objcopy --only-keep-debug myapp myapp.debug
objcopy --strip-debug myapp
objcopy --add-gnu-debuglink=myapp.debug myapp

설명:

  • -g: 디버그 정보 포함
  • -O0: 최적화 비활성화 (변수·라인 매핑 정확)
  • objcopy: 릴리즈 바이너리는 심볼 제거, .debug 파일로 분리해 보관

기본 GDB 명령어

# GDB로 프로그램 실행
gdb ./myapp
# GDB 내부 명령
(gdb) run                    # 실행
(gdb) run arg1 arg2          # 인자와 함께 실행
(gdb) break main             # main에 브레이크포인트
(gdb) break file.cpp:42      # file.cpp 42번째 줄
(gdb) break func if x > 10   # 조건부 브레이크포인트
(gdb) continue               # 다음 브레이크포인트까지 계속
(gdb) next                   # 다음 줄 (함수 안으로 들어가지 않음)
(gdb) step                   # 다음 줄 (함수 안으로 들어감)
(gdb) finish                 # 현재 함수 반환까지 실행
(gdb) print x                # 변수 x 출력
(gdb) print *ptr             # 포인터 역참조
(gdb) backtrace              # 호출 스택 (bt)
(gdb) backtrace full         # 스택 + 지역 변수
(gdb) frame 2                # 2번째 프레임으로 이동
(gdb) list                   # 소스 코드 표시
(gdb) quit                   # 종료

크래시 재현 및 백트레이스

// crash_example.cpp
#include <iostream>
#include <vector>
void process(const std::vector<int>& v) {
    std::cout << v[100];  // 버그: 인덱스 오버플로우
}
int main() {
    std::vector<int> data = {1, 2, 3};
    process(data);
    return 0;
}
# 컴파일 및 실행
g++ -std=c++17 -g -O0 -o crash_example crash_example.cpp
./crash_example
# 출력: Segmentation fault (core dumped)
# 코어 덤프가 생성되었다면
gdb ./crash_example core
# 또는
gdb ./crash_example -c core
# GDB 출력 예시
(gdb) bt full
#0  0x00007f....in std::vector<int>::operator[] (this=0x7ffc..., __n=100)
#1  0x000055....in process (v=...) at crash_example.cpp:6
#2  0x000055....in main () at crash_example.cpp:11

해석: process 함수 6번째 줄에서 v[100] 접근 시 오버플로우. vsize()는 3이므로 인덱스 100은 유효하지 않음.

코어 덤프 활성화 (Linux)

# 현재 세션에서 무제한 크기로 코어 덤프 허용
ulimit -c unlimited
# 시스템 전역 설정 (Ubuntu/Debian)
# /etc/security/limits.conf 에 추가:
# * soft core unlimited
# systemd 서비스의 코어 덤프 저장 위치 확인
coredumpctl list
coredumpctl info
coredumpctl dump -o /tmp/core ./myapp

GDB 스크립트로 자동 분석

# analyze_crash.gdb
set pagination off
run
bt full
info registers
info threads
quit
gdb -batch -x analyze_crash.gdb ./myapp

3. LLDB 완전 예제 (macOS)

환경: macOS, Clang

macOS에서는 GDB 대신 LLDB가 기본 디버거입니다. 명령어가 GDB와 유사하지만 일부 문법이 다릅니다.

LLDB 기본 명령어

# LLDB로 프로그램 실행
lldb ./myapp
# LLDB 내부 명령 (GDB와 대응)
(lldb) run                    # 실행
(lldb) b main                 # main에 브레이크포인트 (break의 약자)
(lldb) b file.cpp:42          # file.cpp 42번째 줄
(lldb) b func -c "x > 10"     # 조건부 브레이크포인트
(lldb) c                      # continue
(lldb) n                      # next
(lldb) s                      # step
(lldb) finish                 # 현재 함수 반환까지
(lldb) p x                    # print x
(lldb) p *ptr                 # 포인터 역참조
(lldb) bt                     # backtrace
(lldb) bt all                 # 모든 스레드 백트레이스
(lldb) frame variable         # 현재 프레임의 지역 변수
(lldb) f 2                    # frame 2로 이동
(lldb) quit                   # 종료

macOS 코어 덤프 분석

# macOS에서 코어 덤프 생성 (크래시 시)
# 기본 위치: /cores/core.PID
# LLDB로 코어 분석
lldb -c /cores/core.12345 ./myapp
# 또는
lldb ./myapp
(lldb) target create -c /cores/core.12345 ./myapp
(lldb) bt all

LLDB 고급 예제: 멀티스레드 크래시

// race_example.cpp
#include <thread>
#include <vector>
#include <atomic>
std::vector<int> shared_data;
std::atomic<bool> done{false};
void writer() {
    for (int i = 0; i < 1000; ++i) {
        shared_data.push_back(i);  // 데이터 레이스 가능
    }
    done = true;
}
void reader() {
    while (!done) {
        for (size_t i = 0; i < shared_data.size(); ++i) {
            (void)shared_data[i];  // 레이스
        }
    }
}
int main() {
    std::thread t1(writer);
    std::thread t2(reader);
    t1.join();
    t2.join();
    return 0;
}
# LLDB로 스레드별 백트레이스
lldb ./race_example
(lldb) run
# 크래시 시
(lldb) thread list
(lldb) thread select 1
(lldb) bt
(lldb) thread select 2
(lldb) bt

4. AddressSanitizer (ASan)

개념

AddressSanitizer는 메모리 오류를 런타임에 탐지하는 도구입니다. 버퍼 오버플로우, 언더플로우, use-after-free, double-free 등을 즉시 보고합니다.

flowchart LR
    subgraph normal[일반 빌드]
        N1[버그 있는 코드] --> N2[나중에 크래시]
    end
    subgraph asan[ASan 빌드]
        A1[버그 있는 코드] --> A2[즉시 탐지·보고]
    end

ASan 활성화

# GCC
g++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o myapp main.cpp
# Clang
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o myapp main.cpp

주의: -O0가 아니어도 동작하지만, -fno-omit-frame-pointer가 있으면 스택 트레이스가 정확합니다.

ASan 완전 예제: 버퍼 오버플로우

// asan_example.cpp
#include <cstring>
#include <iostream>
void vulnerable_copy(const char* src) {
    char buf[16];
    strcpy(buf, src);  // 오버플로우: src가 16자보다 길면
    std::cout << buf << "\n";
}
int main() {
    vulnerable_copy("short");
    vulnerable_copy("this is a very long string that overflows");
    return 0;
}
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o asan_example asan_example.cpp
./asan_example
# ASan 출력 예시
=================================================================
==12345==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7ffc...
    #0 0x....in strcpy
    #1 0x....in vulnerable_copy(...) at asan_example.cpp:6
    #2 0x....in main at asan_example.cpp:12
...
=================================================================

ASan 완전 예제: use-after-free

// uaf_example.cpp
#include <iostream>
int* get_ptr() {
    int x = 42;
    return &x;  // 스택 주소 반환 (이미 잘못된 패턴)
}
void use_after_free() {
    int* p = new int(100);
    delete p;
    *p = 200;  // use-after-free
}
int main() {
    use_after_free();
    return 0;
}
clang++ -std=c++17 -g -fsanitize=address -fno-omit-frame-pointer -o uaf_example uaf_example.cpp
./uaf_example
# ASan 출력 예시
==12345==ERROR: AddressSanitizer: heap-use-after-free on address 0x...
    #0 0x....in use_after_free() at uaf_example.cpp:10
    #1 0x....in main at uaf_example.cpp:14
...

ASan 옵션

# 환경 변수로 동작 제어
export ASAN_OPTIONS=detect_leaks=1:abort_on_error=1:symbolize=1
# LeakSanitizer도 함께 (ASan에 포함됨)
# detect_leaks=1 이 기본

5. ThreadSanitizer (TSan)

개념

ThreadSanitizer는 데이터 레이스를 런타임에 탐지합니다. 두 스레드가 동시에 같은 메모리에 접근하고, 그 중 하나라도 쓰기이면 데이터 레이스입니다.

flowchart TB
    subgraph race[데이터 레이스]
        T1[스레드 1: 읽기/쓰기] --> M[공유 메모리]
        T2[스레드 2: 읽기/쓰기] --> M
    end
    subgraph fix[해결]
        F1[뮤텍스]
        F2[atomic]
        F3[스레드 로컬]
    end

TSan 활성화

# GCC (일부 버전)
g++ -std=c++17 -g -fsanitize=thread -o myapp main.cpp
# Clang (권장)
clang++ -std=c++17 -g -fsanitize=thread -o myapp main.cpp

주의: ASan과 TSan은 동시에 사용할 수 없습니다. 각각 별도 빌드로 테스트합니다.

TSan 완전 예제: 데이터 레이스

// tsan_example.cpp
#include <thread>
#include <vector>
#include <chrono>
int counter = 0;  // 공유 변수, 동기화 없음
void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // 데이터 레이스
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "counter = " << counter << "\n";
    return 0;
}
clang++ -std=c++17 -g -fsanitize=thread -o tsan_example tsan_example.cpp
./tsan_example
# TSan 출력 예시
==================
WARNING: ThreadSanitizer: data race (pid=12345)
  Write of size 4 at 0x....by thread T1:
    #0 increment() at tsan_example.cpp:10
    #1 ...
  Previous read of size 4 at 0x....by thread T2:
    #0 increment() at tsan_example.cpp:10
    #1 ...
==================

TSan으로 수정된 예제

// tsan_fixed.cpp
#include <thread>
#include <atomic>
#include <iostream>
std::atomic<int> counter{0};
void increment() {
    for (int i = 0; i < 100000; ++i) {
        counter++;  // atomic으로 레이스 제거
    }
}
int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "counter = " << counter.load() << "\n";
    return 0;
}

6. 구조화 로깅

개념

프로덕션에서는 구조화 로깅이 필수입니다. 단순 printf 대신 레벨, 타임스탬프, 컨텍스트(파일, 라인, 함수), JSON 등으로 출력하면 로그 수집·검색·알림에 유리합니다.

spdlog 기반 로깅 예제

// logging_example.cpp
#include <spdlog/spdlog.h>
#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <memory>
void setup_logging() {
    // 콘솔 + 로테이팅 파일
    auto console_sink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
    auto file_sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/app.log", 1024 * 1024 * 10, 3);
    std::vector<spdlog::sink_ptr> sinks{console_sink, file_sink};
    auto logger = std::make_shared<spdlog::logger>("main", sinks.begin(), sinks.end());
    logger->set_level(spdlog::level::debug);
    logger->set_pattern("[%Y-%m-%d %H:%M:%S.%e] [%^%l%$] [%s:%#] %v");
    spdlog::set_default_logger(logger);
}
int main() {
    setup_logging();
    spdlog::info("Application started");
    spdlog::debug("Debug message with value: {}", 42);
    spdlog::warn("Warning: low memory");
    spdlog::error("Error: connection failed");
    return 0;
}
# 빌드 (vcpkg 또는 시스템 패키지)
# vcpkg install spdlog
g++ -std=c++17 -O2 -o logging_example logging_example.cpp -lspdlog

JSON 구조화 로깅

// json_logging.cpp
#include <spdlog/spdlog.h>
#include <nlohmann/json.hpp>
void log_structured(const std::string& event, const std::string& user_id, int status) {
    nlohmann::json j;
    j[event] = event;
    j[user_id] = user_id;
    j[status] = status;
    j[timestamp] = std::chrono::system_clock::now().time_since_epoch().count();
    spdlog::info("{}", j.dump());
}
int main() {
    log_structured("login", "user123", 200);
    log_structured("request_failed", "user456", 500);
    return 0;
}

매크로 기반 소스 위치 자동 포함

// log_macro.hpp
#define LOG_TRACE(...)    spdlog::trace(__VA_ARGS__)
#define LOG_DEBUG(...)    spdlog::debug(__VA_ARGS__)
#define LOG_INFO(...)     spdlog::info(__VA_ARGS__)
#define LOG_WARN(...)     spdlog::warn(__VA_ARGS__)
#define LOG_ERROR(...)    spdlog::error("[{}:{}] ", __FILE__, __LINE__); spdlog::error(__VA_ARGS__)
#define LOG_CRITICAL(...) spdlog::critical("[{}:{}] ", __FILE__, __LINE__); spdlog::critical(__VA_ARGS__)
// 사용 예
#include "log_macro.hpp"
void process_request(int id) {
    LOG_INFO("Processing request id={}", id);
    if (id < 0) {
        LOG_ERROR("Invalid id: {}", id);
        return;
    }
    // ...
}

7. 자주 발생하는 에러와 해결법

에러 1: “No symbol table” / ”??” 만 보임

원인: 디버그 심볼이 없거나, 릴리즈 빌드만 배포된 상태에서 코어 덤프 분석. 해결:

# 빌드 시 -g 포함
g++ -std=c++17 -g -O2 -o myapp main.cpp
# 릴리즈 + 심볼 분리 시, .debug 파일을 같은 디렉터리에 두고
gdb ./myapp -c core
# GDB가 자동으로 myapp.debug 로드

에러 2: 코어 덤프가 생성되지 않음

원인: ulimit -c가 0이거나, systemd가 다른 위치에 저장. 해결:

ulimit -c unlimited
# systemd 사용 시
coredumpctl list
coredumpctl dump -o /tmp/core ./myapp

에러 3: ASan/TSan으로 빌드 시 링크 에러

원인: 일부 라이브러리가 sanitizer와 호환되지 않음 (예: 정적 링크된 오래된 라이브러리). 해결:

# 해당 라이브러리를 동적 링크로 전환
# 또는 ASan/TSan 없이 해당 부분만 별도 테스트

에러 4: TSan이 “실제 레이스가 아닌데” 보고함 (False Positive)

원인: 초기화 순서, 또는 의도된 lock-free 패턴이 TSan에 의해 레이스로 오탐될 수 있음. 해결:

// TSan 무시가 필요한 극히 제한된 경우 (주의해서 사용)
#if defined(__SANITIZE_THREAD__)
extern "C" void __tsan_ignore_begin();
extern "C" void __tsan_ignore_end();
#define TSAN_IGNORE do { __tsan_ignore_begin(); } while(0)
#define TSAN_IGNORE_END do { __tsan_ignore_end(); } while(0)
#else
#define TSAN_IGNORE do {} while(0)
#define TSAN_IGNORE_END do {} while(0)
#endif

대부분은 실제 레이스이므로, 무시보다는 동기화 추가를 권장합니다.

에러 5: GDB/LLDB에서 STL 컨테이너 내용이 안 보임

원인: pretty printer가 로드되지 않음. 해결:

# GDB: Python pretty printer (보통 기본 포함)
(gdb) info pretty-printer
# 수동 로드 (GCC)
(gdb) source /usr/share/gcc-11/python/libstdcxx/v6/printers.py
(gdb) info pretty-printer
# LLDB: LLVM pretty printer
# lldb는 기본적으로 libc++/libstdc++ 프린터 포함
(lldb) type summary add -s "${var[0]}" std::vector

에러 6: “AddressSanitizer: alloc-dealloc-mismatch”

원인: new로 할당하고 free로 해제, 또는 malloc으로 할당하고 delete로 해제. 해결:

// ❌ 잘못된 예
int* p = new int(42);
free(p);
// ✅ 올바른 예
int* p = new int(42);
delete p;

에러 7: 프로덕션에서 로그가 너무 많아 디스크 full

원인: 로그 레벨이 debug로 설정되어 있거나, 로테이션 없이 단일 파일에 계속 기록. 해결:

// 프로덕션: info 이상만
logger->set_level(spdlog::level::info);
// 로테이팅 파일 (크기·개수 제한)
auto sink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
    "app.log", 10 * 1024 * 1024, 5);

8. 모범 사례

1. 디버그 빌드와 릴리즈 빌드 분리

# CMake 예시
cmake -DCMAKE_BUILD_TYPE=Debug ..
# 또는
cmake -DCMAKE_BUILD_TYPE=Release -DCMAKE_CXX_FLAGS="-g" ..
  • 개발: Debug 또는 RelWithDebInfo로 충분한 심볼
  • 프로덕션: Release + 분리된 .debug 파일 보관

2. CI에서 Sanitizer 빌드 필수화

# GitHub Actions 예시
- name: Build with ASan
  run: |
    cmake -DCMAKE_BUILD_TYPE=Debug \
          -DCMAKE_CXX_FLAGS="-fsanitize=address -fno-omit-frame-pointer" ..
    cmake --build .
- name: Build with TSan
  run: |
    cmake -DCMAKE_BUILD_TYPE=Debug \
          -DCMAKE_CXX_FLAGS="-fsanitize=thread" ..
    cmake --build .

3. 로그 레벨 환경 변수로 제어

const char* level_str = std::getenv("LOG_LEVEL");
if (level_str) {
    if (strcmp(level_str, "debug") == 0)
        logger->set_level(spdlog::level::debug);
    else if (strcmp(level_str, "info") == 0)
        logger->set_level(spdlog::level::info);
    // ...
}

4. assert와 로깅 병행

#define ASSERT_MSG(cond, msg) \
    do { \
        if (!(cond)) { \
            spdlog::critical("Assertion failed: {} at {}:{}", (msg), __FILE__, __LINE__); \
            std::abort(); \
        } \
    } while(0)

5. 크래시 시 로그 flush

std::atexit( {
    spdlog::default_logger()->flush();
});
// 시그널 핸들러에서도
void signal_handler(int sig) {
    spdlog::critical("Received signal {}", sig);
    spdlog::default_logger()->flush();
    std::_Exit(128 + sig);
}

9. 프로덕션 패턴

패턴 1: 자체 크래시 덤프 수집

// crash_handler.cpp
#include <csignal>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <spdlog/spdlog.h>
void write_backtrace_to_file(const std::string& path) {
    std::ofstream out(path);
    // 실제로는 backtrace() 또는 예외 스택을 기록
    out << "Crash backtrace placeholder\n";
    out.close();
}
void crash_handler(int sig) {
    spdlog::critical("Crash signal: {}", sig);
    spdlog::default_logger()->flush();
    std::string path = "crash_dump_" + std::to_string(getpid()) + ".txt";
    write_backtrace_to_file(path);
    std::signal(sig, SIG_DFL);
    std::raise(sig);
}
void install_crash_handlers() {
    std::signal(SIGSEGV, crash_handler);
    std::signal(SIGABRT, crash_handler);
    std::signal(SIGFPE, crash_handler);
}

패턴 2: Breakpad/Crashpad 통합

# Crashpad: Google의 크로스 플랫폼 크래시 리포터
# - minidump 생성
# - 서버 전송
# - 심볼 서버와 연동해 스택 심볼화
# 빌드
git clone https://github.com/google/crashpad
cd crashpad && python build/gyp_crashpad.py
ninja -C out/Default
// Crashpad 초기화 예시 (의사 코드)
#include <crashpad/client/crashpad_client.h>
crashpad::CrashpadClient client;
client.StartHandler(handler_path, db_path, url, annotations);
client.SetFirstExceptionHandler();

패턴 3: 구조화 로그 + ELK/Loki 수집

// JSON 로그로 stdout 출력 → Docker/ systemd가 수집 → ELK/Loki로 전송
logger->set_pattern("%v");  // JSON만 출력
logger->info(R"({"event":"request","path":"{}","status":{}})", path, status);

패턴 4: 디버그 빌드 전용 상세 로깅

#ifdef NDEBUG
    #define LOG_VERBOSE(...) ((void)0)
#else
    #define LOG_VERBOSE(...) spdlog::debug(__VA_ARGS__)
#endif

패턴 5: 메모리 프로파일링 (개발 전용)

# Heob (Windows), Valgrind (Linux)로 메모리 누수 확인
valgrind --leak-check=full --show-leak-kinds=all ./myapp
# 또는 ASan + LSan
ASAN_OPTIONS=detect_leaks=1 ./myapp

10. 정리

도구별 요약

도구용도빌드 옵션비고
GDB크래시 분석, 단계 실행-g -O0Linux
LLDB크래시 분석, 단계 실행-g -O0macOS
ASan메모리 오류-fsanitize=address오버헤드 ~2x
TSan데이터 레이스-fsanitize=threadASan과 동시 사용 불가
LSan메모리 누수ASan에 포함detect_leaks=1
spdlog구조화 로깅-lspdlog프로덕션 추천

구현 체크리스트

  • 디버그 빌드에 -g 포함
  • 프로덕션용 심볼 분리 보관
  • 코어 덤프 활성화 (ulimit -c unlimited)
  • CI에서 ASan/TSan 빌드 및 테스트
  • 구조화 로깅 (레벨, 타임스탬프, 컨텍스트)
  • 로그 로테이션 설정
  • 크래시 핸들러 또는 Breakpad/Crashpad 통합
  • 시그널 수신 시 로그 flush

참고 자료



자주 묻는 질문 (FAQ)

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

A. C++ 디버깅: GDB/LLDB 완전 예제, AddressSanitizer·ThreadSanitizer, 구조화 로깅, 문제 시나리오, 흔한 에러, 모범 사례, 프로덕션 패턴까지 실전 코드로 다룹니다. 실무에서는 위 본문의 예제와 선택 가이드를 참고해 적용하면 됩니다.

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

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

Q. 더 깊이 공부하려면?

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


관련 글

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

이 부록은 앞선 본문에서 다룬 주제(「C++ 디버깅 기법 완벽 가이드 | GDB·LLDB·ASan·TSan·로깅 실전 [#55-8]」)를 구현·런타임·운영 관점에서 다시 압축합니다. 도메인별 세부 구현은 글마다 다르지만, 입력 검증 → 핵심 연산 → 부작용(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·LLDB·ASan·TSan·로깅 실전 [#55-8]」)를 배포·운영 흐름에 맞춰 옮긴 체크리스트입니다. 도메인에 맞게 단계 이름만 바꿔 적용할 수 있습니다.

  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++, 디버깅, GDB, LLDB, address-sanitizer, ThreadSanitizer, ASan 등으로 검색하시면 이 글이 도움이 됩니다.