Skip to Content
💻 코리아IT아카데미 신촌 - 프로그래밍 학습 자료

Topic 3: 디버깅 기법과 도구 🔍

🎯 학습 목표

  • 체계적인 디버깅 전략과 방법론을 이해할 수 있다
  • assert와 디버그 매크로를 활용할 수 있다
  • 로깅 시스템을 구현하여 프로그램을 추적할 수 있다
  • 단계별 디버깅 기법으로 복잡한 문제를 해결할 수 있다
  • 디버깅 도구들을 효과적으로 활용할 수 있다

🕵️ 디버깅은 탐정 수사

버그를 찾는 것은 범죄 수사와 비슷해요! 🔎

탐정 수사와 디버깅의 유사점:

  • 범죄 현장 = 버그 발생 지점 🎯

    • 정확한 위치 파악이 중요
    • 증거(로그)를 수집해야 함
  • 단서 수집 = 디버그 정보 📝

    • 변수값, 실행 흐름 추적
    • 에러 메시지 분석
  • 가설 검증 = 테스트 코드 🧪

    • 추론한 원인이 맞는지 확인
    • 수정 후 재현 테스트
  • 사건 해결 = 버그 수정

    • 근본 원인 해결
    • 재발 방지책 마련
// 범인을 잡는 것처럼... void findBug() { // 1. 증거 수집 (로깅) LOG("함수 시작: findBug()"); // 2. 가설 검증 (assert) assert(data != nullptr); // 3. 단계별 추적 cout << "현재 상태: " << getCurrentState() << endl; }

🛡️ Assert - 프로그램의 보디가드

Assert의 기본 사용법

#include <iostream> #include <cassert> // assert 헤더 using namespace std; class GamePlayer { private: string name; int level; int hp; public: GamePlayer(string n, int l) : name(n), level(l) { // 생성 시 조건 검증 assert(!name.empty()); // 이름은 비워둘 수 없음 assert(level >= 1 && level <= 100); // 레벨 범위 검증 hp = level * 10; // 레벨에 비례한 체력 cout << "✅ " << name << " (레벨 " << level << ") 생성됨!" << endl; } void takeDamage(int damage) { assert(damage >= 0); // 음수 데미지는 불가능 assert(hp > 0); // 이미 죽은 캐릭터는 데미지 받을 수 없음 cout << "💥 " << name << "이(가) " << damage << " 데미지를 받았습니다!" << endl; hp -= damage; if (hp < 0) hp = 0; cout << "❤️ 현재 HP: " << hp << endl; } void levelUp() { assert(level < 100); // 최대 레벨 체크 level++; int oldHp = hp; hp = level * 10; cout << "🎉 레벨 업! " << level << " (HP: " << oldHp << " → " << hp << ")" << endl; } void showStatus() { cout << "🎮 " << name << " | 레벨: " << level << " | HP: " << hp << endl; } }; int main() { cout << "🛡️ Assert를 활용한 안전한 게임 🛡️" << endl; cout << "===================================" << endl; try { // 정상적인 플레이어 생성 GamePlayer hero("용감한전사", 5); hero.showStatus(); cout << endl; // 정상적인 게임 플레이 hero.takeDamage(15); hero.levelUp(); hero.showStatus(); cout << endl; // ⚠️ 잘못된 사용법들 (assert 실패 유발) cout << "🚨 Assert 테스트 (잘못된 입력들):" << endl; // 1. 음수 데미지 (assert 실패!) // hero.takeDamage(-10); // 주석 해제하면 프로그램 종료 // 2. 잘못된 레벨로 플레이어 생성 (assert 실패!) // GamePlayer badPlayer("", 150); // 주석 해제하면 프로그램 종료 cout << "✅ 모든 Assert가 정상적으로 작동합니다!" << endl; } catch (...) { cout << "❌ 예상치 못한 오류가 발생했습니다!" << endl; } return 0; }

커스텀 Assert 매크로

#include <iostream> #include <string> using namespace std; // 디버그 모드에서만 작동하는 커스텀 assert #ifdef DEBUG #define GAME_ASSERT(condition, message) \ if (!(condition)) { \ cout << "🚨 ASSERTION FAILED! 🚨" << endl; \ cout << "📂 파일: " << __FILE__ << endl; \ cout << "📍 라인: " << __LINE__ << endl; \ cout << "⚠️ 조건: " << #condition << endl; \ cout << "💬 메시지: " << message << endl; \ abort(); \ } #else #define GAME_ASSERT(condition, message) ((void)0) // 릴리즈에서는 무시 #endif // 경고용 매크로 (프로그램을 종료하지 않음) #define GAME_WARNING(condition, message) \ if (!(condition)) { \ cout << "⚠️ WARNING: " << message << endl; \ cout << "📍 " << __FILE__ << ":" << __LINE__ << endl; \ } class BattleSystem { private: int playerHP = 100; int enemyHP = 80; public: void playerAttack(int damage) { GAME_ASSERT(damage > 0, "공격 데미지는 양수여야 합니다!"); GAME_ASSERT(playerHP > 0, "플레이어가 이미 죽었습니다!"); GAME_ASSERT(enemyHP > 0, "적이 이미 죽었습니다!"); cout << "⚔️ 플레이어가 " << damage << " 데미지로 공격!" << endl; enemyHP -= damage; GAME_WARNING(damage > 50, "너무 강한 공격입니다! 게임 밸런스를 확인하세요."); if (enemyHP <= 0) { enemyHP = 0; cout << "🎉 적을 물리쳤습니다!" << endl; } } void showBattleStatus() { cout << "🏛️ 전투 상황:" << endl; cout << " 플레이어 HP: " << playerHP << " ❤️" << endl; cout << " 적 HP: " << enemyHP << " 💀" << endl; } }; int main() { cout << "🛡️ 커스텀 Assert 시스템 🛡️" << endl; cout << "=============================" << endl; BattleSystem battle; battle.showBattleStatus(); cout << endl; // 정상적인 전투 battle.playerAttack(25); battle.playerAttack(30); battle.showBattleStatus(); cout << endl; // 경고 발생 상황 (높은 데미지) battle.playerAttack(60); // WARNING 발생 battle.showBattleStatus(); // Assert 실패 상황들 (주석 해제 시 프로그램 종료) // battle.playerAttack(-10); // 음수 데미지 // battle.playerAttack(10); // 이미 죽은 적 공격 return 0; }

📊 로깅 시스템 - 프로그램의 일기장

기본 로깅 시스템 구현

#include <iostream> #include <fstream> #include <string> #include <ctime> #include <iomanip> using namespace std; enum LogLevel { DEBUG = 0, INFO = 1, WARNING = 2, ERROR = 3, CRITICAL = 4 }; class Logger { private: ofstream logFile; LogLevel currentLogLevel; string getLevelString(LogLevel level) { switch (level) { case DEBUG: return "🔍 DEBUG"; case INFO: return "ℹ️ INFO"; case WARNING: return "⚠️ WARNING"; case ERROR: return "❌ ERROR"; case CRITICAL: return "🚨 CRITICAL"; default: return "❓ UNKNOWN"; } } string getCurrentTime() { time_t now = time(0); tm* timeinfo = localtime(&now); char buffer[100]; strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", timeinfo); return string(buffer); } public: Logger(const string& filename = "game.log", LogLevel level = INFO) : currentLogLevel(level) { logFile.open(filename, ios::app); // 추가 모드로 열기 if (logFile.is_open()) { log(INFO, "Logger", "로깅 시스템이 시작되었습니다."); } } ~Logger() { if (logFile.is_open()) { log(INFO, "Logger", "로깅 시스템이 종료됩니다."); logFile.close(); } } void log(LogLevel level, const string& category, const string& message) { if (level < currentLogLevel) return; // 로그 레벨 필터링 string timestamp = getCurrentTime(); string levelStr = getLevelString(level); // 콘솔 출력 cout << "[" << timestamp << "] " << levelStr << " [" << category << "] " << message << endl; // 파일 출력 if (logFile.is_open()) { logFile << "[" << timestamp << "] " << levelStr << " [" << category << "] " << message << endl; logFile.flush(); // 즉시 파일에 쓰기 } } void setLogLevel(LogLevel level) { currentLogLevel = level; log(INFO, "Logger", "로그 레벨이 " + getLevelString(level) + "로 변경되었습니다."); } }; // 전역 로거 인스턴스 Logger gameLogger("rpg_game.log", DEBUG); // 편의용 매크로들 #define LOG_DEBUG(category, msg) gameLogger.log(DEBUG, category, msg) #define LOG_INFO(category, msg) gameLogger.log(INFO, category, msg) #define LOG_WARNING(category, msg) gameLogger.log(WARNING, category, msg) #define LOG_ERROR(category, msg) gameLogger.log(ERROR, category, msg) #define LOG_CRITICAL(category, msg) gameLogger.log(CRITICAL, category, msg) class GameEngine { private: bool isRunning = false; int frameCount = 0; public: void initialize() { LOG_INFO("Engine", "게임 엔진 초기화 시작..."); try { // 그래픽 시스템 초기화 LOG_DEBUG("Graphics", "그래픽 시스템 초기화 중..."); initGraphics(); // 사운드 시스템 초기화 LOG_DEBUG("Audio", "사운드 시스템 초기화 중..."); initAudio(); // 입력 시스템 초기화 LOG_DEBUG("Input", "입력 시스템 초기화 중..."); initInput(); isRunning = true; LOG_INFO("Engine", "게임 엔진 초기화 완료!"); } catch (const exception& e) { LOG_CRITICAL("Engine", "초기화 실패: " + string(e.what())); throw; } } void update() { if (!isRunning) { LOG_WARNING("Engine", "엔진이 실행 중이 아닙니다!"); return; } frameCount++; if (frameCount % 100 == 0) { // 100프레임마다 로그 LOG_DEBUG("Engine", "프레임 " + to_string(frameCount) + " 처리 중..."); } // 시뮬레이션: 가끔 경고 상황 발생 if (frameCount % 250 == 0) { LOG_WARNING("Performance", "프레임 드롭 감지 (프레임: " + to_string(frameCount) + ")"); } // 시뮬레이션: 드물게 오류 발생 if (frameCount == 500) { LOG_ERROR("Memory", "메모리 사용량이 임계치에 도달했습니다!"); } } void shutdown() { LOG_INFO("Engine", "게임 엔진 종료 시작..."); isRunning = false; LOG_DEBUG("Graphics", "그래픽 시스템 정리 중..."); LOG_DEBUG("Audio", "사운드 시스템 정리 중..."); LOG_DEBUG("Input", "입력 시스템 정리 중..."); LOG_INFO("Engine", "게임 엔진 종료 완료. 총 " + to_string(frameCount) + " 프레임 실행됨."); } private: void initGraphics() { LOG_DEBUG("Graphics", "OpenGL 컨텍스트 생성..."); LOG_DEBUG("Graphics", "셰이더 로딩..."); LOG_DEBUG("Graphics", "텍스처 로딩..."); } void initAudio() { LOG_DEBUG("Audio", "오디오 디바이스 초기화..."); LOG_DEBUG("Audio", "BGM 로딩..."); LOG_DEBUG("Audio", "효과음 로딩..."); } void initInput() { LOG_DEBUG("Input", "키보드 입력 설정..."); LOG_DEBUG("Input", "마우스 입력 설정..."); LOG_DEBUG("Input", "게임패드 감지..."); } public: bool getIsRunning() const { return isRunning; } int getFrameCount() const { return frameCount; } }; int main() { cout << "📊 게임 로깅 시스템 데모 📊" << endl; cout << "============================" << endl; try { GameEngine engine; // 게임 엔진 초기화 engine.initialize(); cout << endl; // 게임 루프 시뮬레이션 LOG_INFO("Main", "게임 루프 시작!"); for (int i = 0; i < 600; i++) { engine.update(); // 시뮬레이션: 중간에 로그 레벨 변경 if (i == 300) { LOG_INFO("Main", "로그 레벨을 WARNING으로 변경합니다."); gameLogger.setLogLevel(WARNING); } } cout << endl; LOG_INFO("Main", "게임 루프 종료!"); // 게임 엔진 종료 engine.shutdown(); } catch (const exception& e) { LOG_CRITICAL("Main", "게임 실행 중 치명적 오류: " + string(e.what())); } cout << "\n📝 로그 파일(rpg_game.log)을 확인해보세요!" << endl; return 0; }

🔬 단계별 디버깅 기법

디버깅 헬퍼 클래스

#include <iostream> #include <vector> #include <map> #include <string> using namespace std; class DebugHelper { private: static int indentLevel; static bool debugEnabled; public: // 함수 진입/종료 추적 class FunctionTracker { private: string functionName; public: FunctionTracker(const string& name) : functionName(name) { printIndent(); cout << "🟢 진입: " << functionName << "()" << endl; indentLevel++; } ~FunctionTracker() { indentLevel--; printIndent(); cout << "🔴 종료: " << functionName << "()" << endl; } }; static void printIndent() { for (int i = 0; i < indentLevel; i++) { cout << " "; } } static void setDebugMode(bool enabled) { debugEnabled = enabled; cout << "🔧 디버그 모드: " << (enabled ? "ON" : "OFF") << endl; } template<typename T> static void printVariable(const string& name, const T& value) { if (!debugEnabled) return; printIndent(); cout << "📊 " << name << " = " << value << endl; } template<typename T> static void printVector(const string& name, const vector<T>& vec) { if (!debugEnabled) return; printIndent(); cout << "📋 " << name << " [" << vec.size() << "개]: "; for (size_t i = 0; i < vec.size(); i++) { cout << vec[i]; if (i < vec.size() - 1) cout << ", "; } cout << endl; } static void breakPoint(const string& message) { if (!debugEnabled) return; printIndent(); cout << "🔴 BREAKPOINT: " << message << endl; cout << "계속하려면 Enter를 누르세요..."; cin.get(); } static void checkpoint(const string& location) { if (!debugEnabled) return; printIndent(); cout << "📍 체크포인트: " << location << endl; } }; // 정적 멤버 변수 초기화 int DebugHelper::indentLevel = 0; bool DebugHelper::debugEnabled = true; // 편의용 매크로들 #define TRACE_FUNCTION() DebugHelper::FunctionTracker _ft(__FUNCTION__) #define DEBUG_VAR(var) DebugHelper::printVariable(#var, var) #define DEBUG_VECTOR(vec) DebugHelper::printVector(#vec, vec) #define CHECKPOINT(msg) DebugHelper::checkpoint(msg) #define BREAKPOINT(msg) DebugHelper::breakPoint(msg) class BubbleSortDebugger { private: vector<int> data; public: BubbleSortDebugger(const vector<int>& input) : data(input) { TRACE_FUNCTION(); DEBUG_VECTOR(data); } void sort() { TRACE_FUNCTION(); int n = data.size(); DEBUG_VAR(n); for (int i = 0; i < n - 1; i++) { CHECKPOINT("외부 루프 시작"); DEBUG_VAR(i); bool swapped = false; for (int j = 0; j < n - i - 1; j++) { DEBUG_VAR(j); cout << " 🔍 비교: data[" << j << "](" << data[j] << ") vs data[" << (j+1) << "](" << data[j+1] << ")" << endl; if (data[j] > data[j + 1]) { cout << " 🔄 교환 필요!" << endl; swap(data[j], data[j + 1]); swapped = true; DEBUG_VECTOR(data); } } cout << " 📊 " << i+1 << "번째 패스 완료" << endl; DEBUG_VECTOR(data); if (!swapped) { cout << " ✅ 더 이상 교환할 것이 없음 - 정렬 완료!" << endl; break; } // BREAKPOINT("패스 완료 - 계속하시겠습니까?"); cout << endl; } } void printResult() { TRACE_FUNCTION(); cout << "🎉 정렬 결과: "; DEBUG_VECTOR(data); } }; // 더 복잡한 예제: 이진 검색 디버깅 class BinarySearchDebugger { private: vector<int> sortedData; public: BinarySearchDebugger(const vector<int>& data) : sortedData(data) { TRACE_FUNCTION(); DEBUG_VECTOR(sortedData); } int search(int target) { TRACE_FUNCTION(); DEBUG_VAR(target); int left = 0; int right = sortedData.size() - 1; int iteration = 1; while (left <= right) { CHECKPOINT("이진 검색 반복 #" + to_string(iteration)); int mid = left + (right - left) / 2; DEBUG_VAR(left); DEBUG_VAR(right); DEBUG_VAR(mid); DEBUG_VAR(sortedData[mid]); cout << " 🎯 검색 범위: [" << left << ", " << right << "]" << endl; cout << " 🔍 중간값: index " << mid << " = " << sortedData[mid] << endl; if (sortedData[mid] == target) { cout << " ✅ 찾았습니다!" << endl; return mid; } if (sortedData[mid] < target) { cout << " ➡️ 오른쪽 절반으로 이동" << endl; left = mid + 1; } else { cout << " ⬅️ 왼쪽 절반으로 이동" << endl; right = mid - 1; } iteration++; cout << endl; } cout << " ❌ 찾지 못했습니다!" << endl; return -1; } }; int main() { cout << "🔬 단계별 디버깅 시스템 🔬" << endl; cout << "=============================" << endl; // 1. 버블 정렬 디버깅 cout << "📈 버블 정렬 디버깅:" << endl; cout << "==================" << endl; vector<int> unsortedData = {64, 34, 25, 12, 22, 11, 90}; BubbleSortDebugger sorter(unsortedData); sorter.sort(); sorter.printResult(); cout << endl; // 2. 이진 검색 디버깅 cout << "🔍 이진 검색 디버깅:" << endl; cout << "==================" << endl; vector<int> sortedData = {11, 12, 22, 25, 34, 64, 90}; BinarySearchDebugger searcher(sortedData); int target = 25; cout << "🎯 검색 대상: " << target << endl; int result = searcher.search(target); if (result != -1) { cout << "✅ " << target << "을(를) 인덱스 " << result << "에서 찾았습니다!" << endl; } else { cout << "❌ " << target << "을(를) 찾지 못했습니다!" << endl; } cout << endl; // 3. 디버그 모드 끄기 cout << "🔧 디버그 모드를 끕니다..." << endl; DebugHelper::setDebugMode(false); cout << "🔍 디버그 정보 없이 검색:" << endl; int silentResult = searcher.search(64); cout << "결과: " << (silentResult != -1 ? "찾음" : "못찾음") << endl; return 0; }

🛠️ 디버깅 도구들

1. 컴파일러 디버깅 플래그

# 디버그 정보 포함하여 컴파일 g++ -g -O0 -DDEBUG program.cpp -o program # 최적화 없이 컴파일 (디버깅 용이) g++ -O0 -g program.cpp -o program # 추가 경고 메시지 활성화 g++ -Wall -Wextra -g program.cpp -o program

2. 조건부 컴파일을 활용한 디버깅

// 디버그 빌드와 릴리즈 빌드 구분 #ifdef DEBUG #define DBG_PRINT(x) cout << "DEBUG: " << x << endl #define DBG_ASSERT(condition) assert(condition) #else #define DBG_PRINT(x) ((void)0) #define DBG_ASSERT(condition) ((void)0) #endif class OptimizedGameLoop { public: void update() { DBG_PRINT("게임 루프 업데이트 시작"); // 게임 로직 processInput(); updatePhysics(); renderFrame(); DBG_PRINT("게임 루프 업데이트 완료"); } private: void processInput() { DBG_PRINT("입력 처리 중..."); } void updatePhysics() { DBG_PRINT("물리 계산 중..."); DBG_ASSERT(deltaTime > 0); // 시간 간격 검증 } void renderFrame() { DBG_PRINT("프레임 렌더링 중..."); } float deltaTime = 0.016f; // 약 60 FPS };

🎯 디버깅 모범 사례

1. 체계적인 접근 방법 📋

// 문제 해결 단계별 접근 void debugSystematically() { // 1단계: 문제 재현 cout << "1️⃣ 문제 상황을 재현합니다..." << endl; // 2단계: 가설 수립 cout << "2️⃣ 가능한 원인들을 분석합니다..." << endl; // 3단계: 검증 cout << "3️⃣ 각 가설을 테스트합니다..." << endl; // 4단계: 수정 cout << "4️⃣ 근본 원인을 수정합니다..." << endl; // 5단계: 검증 cout << "5️⃣ 수정 사항을 검증합니다..." << endl; }

2. 효과적인 로깅 전략 📝

// 로그 레벨별 전략 void effectiveLogging() { // DEBUG: 상세한 실행 흐름 LOG_DEBUG("Function", "변수 x의 값이 변경됨: " + to_string(x)); // INFO: 주요 이벤트 LOG_INFO("Game", "새로운 레벨 로딩 완료"); // WARNING: 주의가 필요한 상황 LOG_WARNING("Performance", "프레임 레이트 저하 감지"); // ERROR: 복구 가능한 오류 LOG_ERROR("FileSystem", "설정 파일 로딩 실패 - 기본값 사용"); // CRITICAL: 심각한 오류 LOG_CRITICAL("Memory", "메모리 할당 실패!"); }

💡 핵심 정리

  • 체계적 디버깅: 문제 재현 → 가설 수립 → 검증 → 수정 → 재검증의 단계적 접근
  • Assert 활용: 개발 중 조건 검증으로 버그 조기 발견
  • 로깅 시스템: 프로그램 실행 과정을 추적하고 문제점 분석
  • 디버그 헬퍼: 함수 추적, 변수 출력, 브레이크포인트 등 디버깅 지원 도구
  • 조건부 컴파일: DEBUG/RELEASE 모드별로 다른 코드 실행
  • 도구 활용: 컴파일러 플래그, 디버거, 프로파일러 등 개발 도구 활용

✅ 실습 체크리스트

🚀 다음 시간 예고

다음 시간에는 메모리 관리와 최적화에 대해 알아볼 거예요!

  • 메모리 누수 방지 기법과 도구들
  • valgrind와 메모리 디버깅 도구 활용
  • 성능 프로파일링으로 병목 지점 찾기
  • 게임 최적화를 위한 실전 기법들

“체계적인 디버깅으로 버그 없는 완벽한 프로그램을 만들어보세요! 🔍✨”

Last updated on