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 program2. 조건부 컴파일을 활용한 디버깅
// 디버그 빌드와 릴리즈 빌드 구분
#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