본문 바로가기

디버거 사용법

디버그 빌드와 중단점·단계 실행·변수·호출 스택을 활용해 실행 상태를 관찰하고 논리 오류를 추적합니다.

아무리 숙련된 개발자라도 코드를 작성하는 과정에서 오류(버그)를 피할 수는 없습니다.

프로그램이 예상대로 동작하지 않을 때, 가장 강력하고 효율적인 문제 해결 도구는 바로 디버거(Debugger)입니다.

디버거는 실행 중인 프로그램의 내부 상태를 검사하고, 실행 흐름을 제어하여 논리적 오류를 식별하고 수정하는 데 도움을 줍니다.

이번 장에서는 C++ 개발에서 널리 사용되는 디버거의 기본적인 사용법과 핵심 기능들을 학습합니다.

여러분이 효과적으로 버그를 찾아 해결할 수 있도록 돕겠습니다.

아래 그림은 디버거를 단순 실행 버튼 모음이 아니라, 추측을 실행 상태 확인으로 바꾸는 과정으로 정리합니다.

디버깅은 첫 divergence를 증거로 좁힌다

calculate_sum(5)의 실제 10을 재현해 i=4 뒤의 total_sum=10과 다음 검사 5<5가 거짓인 최초 차이를 찾고 수정 후 15를 검증할 수 있다.

C++ · DEBUGGING EVIDENCE

기대와 실제가 처음 갈라지는 경계를 찾는다

본문 calculate_sum(5)는 0부터 4까지만 더해 10을 반환한다. 마지막 5가 제외되는 조건을 관찰한 뒤 의도한 1부터 5의 범위로 수정한다.

calculate_sum 예제로 읽는 디버깅 과정재현·중단·관찰을 거쳐 끝값이 제외된 경계를 찾고, 시작과 조건을 최소 수정한 뒤 같은 입력과 경계 입력을 다시 확인한다. 화살표는 확인 순서이다.
1. 재현limit=5실제 결과10
연결 관계1. 재현 · limit=5 · 실제 결과10 → 2. 중단 · 함수 진입 · for 조건
2. 중단함수 진입for 조건
연결 관계2. 중단 · 함수 진입 · for 조건 → 3. 관찰 · i=4 본문 뒤 · total_sum=10
3. 관찰i=4 본문 뒤total_sum=10
연결 관계3. 관찰 · i=4 본문 뒤 · total_sum=10 → 4. 첫 차이 · 5 < 5 거짓 · 마지막5 제외
4. 첫 차이5 < 5 거짓마지막5 제외
연결 관계4. 첫 차이 · 5 < 5 거짓 · 마지막5 제외 → 5. 최소 수정 · i=1로 시작 · i <= max_val
5. 최소 수정i=1로 시작i <= max_val
연결 관계5. 최소 수정 · i=1로 시작 · i <= max_val → 6. 재검증 · 1..5 합계15 · 경계값 확인
6. 재검증1..5 합계15경계값 확인

재현

limit=5 · 실제 결과 10

중단

함수 진입 · for 조건

관찰

i=4 뒤 total_sum=10

첫 차이

5<5 거짓 · 마지막 5 제외

최소 수정

i=1 · i<=max_val

재검증

1..5 합계 15 · 경계값

본문처럼 시작을 1, 조건을 i <= max_val로 바꾸면 입력 5의 결과는 15이다. 실패 입력과 유효한 경계값의 회귀 검증을 남긴다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.
관계도의 보충 설명
원 도표의 제목·노드·관계·조건·주석
범위원문
노드·연결선 라벨실제 결과 10
노드·연결선 라벨마지막 5 제외
노드·연결선 라벨1..5 합계 15
예의 값과 변화
관찰·상황값·보충 정보변화·이유
실패 재현

calculate_sum(5)의 기대값과 실제값을 기록한다.

  • 기대: 15
  • 실제: 10
  • 재현 입력과 차이가 있어야 최초 오류를 좁힐 수 있다.
경계 검사

i가 4인 회차까지 total_sum은 10이고, 다음 i=5에서는 반복 조건이 거짓이다.

  • 현재: i=5, total_sum=10
  • 조건: 5<5=false
  • 반복이 5를 더하기 전에 끝나는 최초 차이를 찾는다.
수정 확인

포함해야 할 끝값 5를 처리하는 수정으로 재검사한다.

  • 입력: 5
  • 검증 결과: 15
  • 수정 뒤 같은 입력의 결과와 관련 경계 사례를 다시 확인한다.

디버깅은 첫 divergence를 증거로 좁힌다

calculate_sum(5)의 실제 10을 재현해 i=4 뒤의 total_sum=10과 다음 검사 5<5가 거짓인 최초 차이를 찾고 수정 후 15를 검증할 수 있다.

C++ · DEBUGGING EVIDENCE

기대와 실제가 처음 갈라지는 경계를 찾는다

본문 calculate_sum(5)는 0부터 4까지만 더해 10을 반환한다. 마지막 5가 제외되는 조건을 관찰한 뒤 의도한 1부터 5의 범위로 수정한다.

calculate_sum 예제로 읽는 디버깅 과정재현·중단·관찰을 거쳐 끝값이 제외된 경계를 찾고, 시작과 조건을 최소 수정한 뒤 같은 입력과 경계 입력을 다시 확인한다. 화살표는 확인 순서이다.
1. 재현limit=5실제 결과10
연결 관계1. 재현 · limit=5 · 실제 결과10 → 2. 중단 · 함수 진입 · for 조건
2. 중단함수 진입for 조건
연결 관계2. 중단 · 함수 진입 · for 조건 → 3. 관찰 · i=4 본문 뒤 · total_sum=10
3. 관찰i=4 본문 뒤total_sum=10
연결 관계3. 관찰 · i=4 본문 뒤 · total_sum=10 → 4. 첫 차이 · 5 < 5 거짓 · 마지막5 제외
4. 첫 차이5 < 5 거짓마지막5 제외
연결 관계4. 첫 차이 · 5 < 5 거짓 · 마지막5 제외 → 5. 최소 수정 · i=1로 시작 · i <= max_val
5. 최소 수정i=1로 시작i <= max_val
연결 관계5. 최소 수정 · i=1로 시작 · i <= max_val → 6. 재검증 · 1..5 합계15 · 경계값 확인
6. 재검증1..5 합계15경계값 확인

재현

limit=5 · 실제 결과 10

중단

함수 진입 · for 조건

관찰

i=4 뒤 total_sum=10

첫 차이

5<5 거짓 · 마지막 5 제외

최소 수정

i=1 · i<=max_val

재검증

1..5 합계 15 · 경계값

본문처럼 시작을 1, 조건을 i <= max_val로 바꾸면 입력 5의 결과는 15이다. 실패 입력과 유효한 경계값의 회귀 검증을 남긴다.

이 예의 조건

  • 본문과 원 도표에 제시된 사례의 입력·조건을 사용합니다. 설명용 수치나 독립 비교 경로는 각 단계에 표시합니다.
  • 현재 상태·값과 전환 근거의 위치는 모든 단계에서 같습니다. 조건이 맞지 않거나 실행되지 않은 경로를 성공한 동작으로 해석하지 않습니다.

관계도의 보충 설명

원 도표의 제목·노드·관계·조건·주석
범위원문
노드·연결선 라벨실제 결과 10
노드·연결선 라벨마지막 5 제외
노드·연결선 라벨1..5 합계 15
예의 값과 변화
관찰·상황값·보충 정보변화·이유
실패 재현

calculate_sum(5)의 기대값과 실제값을 기록한다.

  • 기대: 15
  • 실제: 10
  • 재현 입력과 차이가 있어야 최초 오류를 좁힐 수 있다.
경계 검사

i가 4인 회차까지 total_sum은 10이고, 다음 i=5에서는 반복 조건이 거짓이다.

  • 현재: i=5, total_sum=10
  • 조건: 5<5=false
  • 반복이 5를 더하기 전에 끝나는 최초 차이를 찾는다.
수정 확인

포함해야 할 끝값 5를 처리하는 수정으로 재검사한다.

  • 입력: 5
  • 검증 결과: 15
  • 수정 뒤 같은 입력의 결과와 관련 경계 사례를 다시 확인한다.

왜 디버거를 사용해야 하는가?

많은 개발자가 std::cout을 이용한 출력으로 디버깅을 시도합니다.

물론 간단한 경우에는 유용하지만, 복잡한 오류나 예측 불가능한 동작을 추적하는 데는 한계가 있습니다.

  • 실시간 상태 확인: 프로그램이 특정 시점에서 어떤 변수 값을 가지고 있는지, 어떤 함수가 호출되었는지 등을 실시간으로 정확하게 파악할 수 있습니다.
  • 실행 흐름 제어: 코드를 한 줄씩 실행하거나, 함수 내부를 펼치지 않고 실행하는 등 프로그램의 실행 흐름을 원하는 대로 제어할 수 있습니다.
  • 복잡한 오류 추적: 여러 스레드 간의 상호작용 문제, 메모리 손상, 예측 불가능한 크래시(crash) 등 cout 디버깅으로는 찾기 어려운 문제를 해결할 수 있습니다.
  • 시간 절약: 문제를 재현하고 cout을 삽입하며 재빌드하는 과정을 반복하는 것보다 훨씬 효율적입니다.

디버깅 빌드 (Debugging Build)

디버거를 효과적으로 사용하려면, 소스 수준 관찰에는 디버깅 심볼을 포함하는 것이 좋습니다. 최적화 빌드도 디버깅할 수 있지만 일부 변수가 사라지거나 실행 줄이 재배치될 수 있습니다.

  • 디버깅 심볼 포함: 컴파일러에게 소스 코드의 라인 번호, 변수 이름, 함수 이름 등 디버깅 정보를 실행 파일에 포함하도록 지시합니다. (GCC/Clang: -g, MSVC: /Zi)
  • 최적화 비활성화: 컴파일러 최적화(-O2, -O3 등)를 비활성화합니다. 최적화는 코드의 실행 순서를 바꾸거나 일부 코드를 제거할 수 있으므로, 디버거가 소스 코드와 일치하지 않게 동작하게 만들 수 있습니다. (GCC/Clang: -O0, MSVC: /Od)

일반적으로 IDE(통합 개발 환경)에서 Debug 구성을 선택하면 이러한 설정이 자동으로 적용됩니다.


주요 디버거와 기본적인 사용법

널리 사용되는 C++ 디버거는 다음과 같습니다.

  • GDB (GNU Debugger): Linux/macOS 환경에서 가장 보편적으로 사용되는 커맨드라인 기반 디버거.
  • LLDB: macOS 환경에서 주로 사용되는 LLVM 프로젝트의 디버거. GDB와 유사한 인터페이스를 가집니다.
  • Visual Studio Debugger: Windows 환경의 Visual Studio IDE에 통합된 GUI 기반 디버거.
  • IDE 통합 디버거: CLion (JetBrains), VS Code (Microsoft) 등 대부분의 현대 IDE는 GDB나 LLDB를 백엔드로 사용하는 통합 디버깅 환경을 제공합니다. 이 책에서는 IDE 통합 디버거를 사용하는 일반적인 방법을 위주로 설명하겠습니다.
디버거의 핵심 기능
브레이크포인트 (Breakpoint)
  • 프로그램 실행을 특정 지점에서 일시 중지시키는 마커입니다.
  • 코드 라인에 설정하는 라인 브레이크포인트, 특정 조건이 참일 때만 중지하는 조건부 브레이크포인트, 특정 함수가 호출될 때 중지하는 함수 브레이크포인트 등이 있습니다.
  • IDE에서는 보통 코드 라인 옆 여백을 클릭하여 설정/해제합니다.
실행 제어 (Execution Control)
  • 계속 실행 (Continue): 다음 브레이크포인트에 도달하거나 프로그램이 끝날 때까지 실행합니다.
  • 한 단계씩 실행 (Step Over): 현재 라인을 실행하고 다음 라인으로 이동합니다. 함수 호출이 있을 경우, 함수 내부로 들어가지 않고 함수 전체를 실행합니다.
  • 한 단계씩 코드 안으로 (Step Into): 현재 라인을 실행하고 다음 라인으로 이동합니다. 함수 호출이 있을 경우, 함수 내부로 들어가서 실행을 계속합니다.
  • 한 단계씩 코드 밖으로 (Step Out): 현재 함수에서 호출자 함수로 돌아갈 때까지 실행합니다.
  • 커서까지 실행 (Run to Cursor): 커서가 있는 라인까지 실행합니다.
변수 검사 (Variable Inspection)
  • 프로그램이 중지된 시점에서 현재 스코프 내의 모든 지역 변수와 매개변수의 값을 확인합니다.
  • 전역 변수나 정적 변수의 값도 확인할 수 있습니다.
  • 대부분의 IDE는 Variables 또는 Locals 창을 통해 이 기능을 제공합니다.
  • 조사식 (Watch Window): 특정 변수나 표현식의 값을 지속적으로 모니터링할 수 있는 창입니다. 복잡한 표현식의 값을 실시간으로 평가하는 데 유용합니다.
호출 스택 (Call Stack / Call Trace)
  • 현재 실행 중인 함수가 어떤 함수에 의해 호출되었는지, 그리고 그 함수는 또 어떤 함수에 의해 호출되었는지 등 함수 호출의 계층 구조를 보여줍니다.
  • 이를 통해 프로그램의 실행 경로를 역추적하여 오류의 근원을 파악할 수 있습니다.
메모리 검사 (Memory Inspection)
  • 특정 메모리 주소의 내용을 바이트 단위로 확인합니다.
  • 포인터가 가리키는 데이터의 실제 내용, 배열의 원시 바이트 데이터 등을 확인하는 데 사용됩니다.
  • IDE의 Memory 또는 Disassembly 창을 통해 접근할 수 있습니다.
어설션 (Assertion)
  • 프로그램의 특정 조건이 항상 참이어야 한다는 것을 명시하는 문입니다.
  • assert() 매크로 (C++의 <cassert>)를 사용합니다.
  • 조건이 거짓일 경우 프로그램이 비정상 종료되며, 디버거가 연결되어 있다면 해당 시점에서 중지됩니다.
  • 릴리스 빌드에서는 NDEBUG 매크로를 정의하여 어설션을 비활성화할 수 있습니다.
    #include <cassert>
    // ...
    int denominator = 0;
    assert(denominator != 0 && "Denominator cannot be zero!"); // 디버깅 시에만 활성화
    // NDEBUG로 assert가 제거되면 아래 0 나눗셈은 UB입니다.
    // 사용자 입력 검증을 assert만으로 대신하면 안 됩니다.
    int result = 10 / denominator;

실제 디버깅 과정 (가상 시나리오)

가상 시나리오를 보기 전에, 문제 재현부터 수정 검증까지 범위를 좁히는 흐름을 먼저 확인해 봅시다.

간단한 프로그램에서 디버거를 사용하는 가상 시나리오를 통해 디버깅 과정을 이해해 봅시다.

아래 완전한 프로그램을 sum_debug.cpp로 저장합니다. GCC를 쓰는 Linux의 예시 빌드는 g++ -std=c++17 -g -O0 sum_debug.cpp -o sum_debug이며, 실행하면 Sum up to 5 is: 10이 나옵니다. IDE에서는 이 실행 파일과 현재 소스가 같은 빌드인지 확인한 뒤 디버깅을 시작합니다. 각 버튼은 소스 줄을 단위로 실행을 요청하지만, 한 번 누르면 반드시 루프 한 회가 끝난다는 뜻은 아닙니다. 중단점·예외·디버그 정보와 현재 실행 위치에 따라 더 일찍 멈출 수 있습니다. GDB의 step·next 설명도 실행 위치와 호출 스택을 구별합니다.

문제: 다음 코드는 1부터 5까지의 숫자를 더해야 하지만, 예상과 다른 결과가 나온다.

디버깅 예제 코드
#include <iostream>

int calculate_sum(int max_val) {
    int total_sum = 0;
    for (int i = 0; i < max_val; ++i) { // 오류 가능성이 있는 부분
        total_sum += i;
    }
    return total_sum;
}

int main() {
    int limit = 5;
    int final_result = calculate_sum(limit);
    std::cout << "Sum up to " << limit << " is: " << final_result << std::endl; // 예상: 1+2+3+4+5=15
    return 0;
}
디버깅 단계

디버깅 빌드: 프로그램을 디버그 모드로 빌드합니다. (IDE에서 Debug 구성 선택)

브레이크포인트 설정
  • main 함수의 int final_result = calculate_sum(limit); 라인에 브레이크포인트를 설정합니다.
  • calculate_sum 함수의 for (int i = 0; i < max_val; ++i) 라인에 브레이크포인트를 설정합니다.

디버거 시작: IDE에서 디버깅 모드로 프로그램을 실행합니다.

프로그램은 첫 번째 브레이크포인트에서 멈출 것입니다.

변수 검사 및 실행 제어
  • 첫 번째 브레이크포인트 (main 함수)
    • Variables 창에서 limit 변수가 5임을 확인합니다.
    • Step Into (F11 또는 해당 키)를 눌러 calculate_sum 함수 내부로 진입합니다.
  • 두 번째 브레이크포인트 (calculate_sum 함수)
    • total_sum 변수가 0이고, max_val이 5임을 확인합니다.
    • 루프 초기화 전이면 i는 아직 그 범위에 선언되지 않았습니다. 초기화 int i = 0을 실행한 뒤에는 0입니다. 범위 밖 표시와 실제 초기화된 값을 구별하며, 임의의 미초기화 값을 읽어 판단하지 않습니다.
    • 조사식(Watch) 창에 i와 total_sum을 추가하여 루프가 돌면서 이 값들이 어떻게 변하는지 실시간으로 관찰합니다.
  • 루프 내부 단계별 실행
    • Step Over (F10 또는 해당 키)를 반복하며 루프의 조건·본문·증가 위치를 차례로 확인합니다.
    • 첫 번째 반복 (i=0): total_sum은 0입니다. 0을 더하는 것 자체는 합계를 바꾸지 않으므로 오답의 직접 원인은 마지막 5가 제외된 경계입니다.
    • 두 번째 반복 (i=1): total_sum이 1이 됩니다.
    • ...
    • 다섯 번째 반복 (i=4): total_sum이 0+1+2+3+4=10이 됩니다.
    • 다음 조건 검사에서 i < max_val (5 < 5)이 거짓이 되어 루프가 끝납니다. 이때 기대 범위의 마지막 값 5가 더해지지 않았다는 점을 확인합니다.
오류 수정
  • 마지막 값이 제외된 경계 오류와 시작 범위의 표현을 확인했습니다.

    루프가 0부터 시작: for (int i = 1; i <= max_val; ++i)로 수정해야 합니다.

    루프 조건이 max_val을 제외: 기대 값이 1부터 5까지의 합이므로 마지막 값 5도 포함해야 합니다.

  • 여기서는 for (int i = 1; i <= max_val; ++i)로 수정합니다.
재빌드 및 재확인
  • 코드를 수정한 후 다시 빌드하고 디버거로 실행하여 수정된 코드가 올바르게 동작하는지 확인합니다.

calculate_sum 예제처럼 watch의 누적 값과 실제 반복 범위를 대조해 첫 차이를 찾습니다.

예시

어느 줄을 실행하기 전인지까지 함께 기록해야 값의 차이를 설명할 수 있습니다. 원 루프에서 i=4인 반복의 본문 실행 전 total_sum은 6이고, total_sum += i 뒤에는 10입니다. 다음 i=5 조건 검사는 거짓이라 5를 더하는 본문을 실행하지 않습니다. 수정 루프는 i=5 본문 실행 전에는 10·실행 후에는 15이며 다음 i=6에서 끝납니다. total_sum=10이라는 값 하나만으로 오류 위치를 확정할 수 없는 이유입니다.

독립 확인: 이 문제의 계약을 0≤max_val≤100에서 1부터 max_val까지의 합으로 정합니다. max_val=4와 0일 때 원 함수·수정 함수의 결과를 각각 구하세요. max_val=0만 통과한 시험으로 수정이 충분하다고 볼 수 있는지도 판단하세요.

반복 범위와 경계 입력 확인

4일 때 원 함수는 0+1+2+3=6, 수정 함수는 1+2+3+4=10입니다. 0일 때는 양쪽 모두 본문을 한 번도 실행하지 않아 0입니다. 0만 시험하면 끝값 제외 오류가 드러나지 않으므로 양수 실패 입력과 경계 입력을 함께 남깁니다. 이 작은 범위는 합계와 루프 증가가 int의 표현 범위 안에 있는 연습 조건이며, 임의의 큰 정수 입력까지 안전하다고 일반화하지 않습니다.


고급 디버깅 기법 (간략 소개)

  • 로그포인트 (Logpoint)
    • 브레이크포인트와 유사하지만, 프로그램 실행을 중지시키지 않고 변수 값을 출력하거나 특정 메시지를 출력합니다. cout 디버깅의 장점과 디버거의 편리함을 결합한 기능입니다.
  • 데이터 브레이크포인트 (Data Breakpoint / Watchpoint)
    • 특정 메모리 주소의 값이 변경될 때 프로그램 실행을 중지시킵니다.
    • 어떤 코드가 의도치 않게 변수 값을 변경하는지 찾을 때 매우 유용합니다. (하드웨어 지원이 필요하며, 모든 디버거에서 지원하지 않을 수 있습니다.)
  • 조건부 브레이크포인트
    • 특정 조건(i > 100, ptr == nullptr 등)이 참일 때만 브레이크포인트에서 멈추도록 설정합니다.
    • 수백만 번의 반복이 있는 루프에서 특정 조건이 만족되는 지점에서만 멈추고 싶을 때 유용합니다.
  • 덤프 파일 (Dump File) 분석
    • 프로그램이 비정상 종료(크래시)되었을 때 생성되는 덤프 파일(core dump on Linux, .dmp on Windows)을 디버거로 로드하여 크래시 시점의 스택 트레이스 및 메모리 상태를 분석할 수 있습니다.
  • 스레드 디버깅
    • 멀티스레드 프로그램에서 각 스레드의 실행 상태, 호출 스택, 지역 변수를 개별적으로 검사하고 제어할 수 있습니다.

디버거 기능은 모두 한 번에 쓰기보다 증상에 맞게 고르는 것이 좋습니다.

아래 다이어그램은 버그의 형태별로 우선 확인할 기능과 단서를 연결한 것입니다.

C++ · DEBUGGER QUESTIONS

관찰하려는 상태에 맞춰 디버거 기능을 고른다

값·조건·쓰기 위치·호출 경로·스레드 대기를 구분하면 필요한 기능과 다음 관찰을 명확히 정할 수 있다.

값이 틀어짐 → Locals·Watch·Step

기대 값과 실제 값이 처음 달라지는 위치를 따라간다.

limit = 5
i = 4; total_sum = 10

본문 예제에서는 i=5 조건 검사에서 루프가 끝나 마지막 5가 제외된다.

특정 반복 → 조건부 중단점

필요한 조건에서만 멈춰 관찰할 실행 범위를 좁힌다.

i > 100
ptr == nullptr

조건은 실제로 검사하려는 실패를 표현해야 한다. 최적화 빌드에서는 변수·줄 매핑이 달라질 수 있다.

쓰기 위치·크래시 → Watchpoint·Stack

값을 바꾼 쓰기 위치와 현재 함수의 호출 경로를 구분한다.

데이터 중단점은 주소의 변경을, Call Stack은 호출 관계를 보여 준다. 크래시 후에는 dump도 확인한다.

스레드 정지 → 각 Thread·Stack

각 스레드의 실행 위치와 보유·대기 관계를 비교한다.

중단 시점의 상태는 단서이다. 멈추거나 한 단계씩 실행하는 행위가 타이밍을 바꿀 수 있다.

Watchpoint·dump 지원, 단축키와 명령은 hardware·OS·IDE에 따라 다르다. 관찰된 상태와 C++ 코드의 의미를 함께 확인한다.

디버깅 세션은 재현 조건을 고정하고 멈출 위치, 관찰할 상태, 수정 후 확인 범위를 순서대로 좁히면 훨씬 짧아집니다.

디버거 사용은 버튼 이름을 많이 외우는 것보다 실패를 재현하고, 정상 상태와 오류 상태가 처음 갈라지는 지점을 찾는 과정으로 이해하는 편이 오래 갑니다.