이 Part에서 다루는 것
- 디버깅 파트를 “동적 할당 이후”에 배우는 이유
- 실무에서 자주 만나는 버그 4종
- Null 크래시
- 정수 오버플로우/언더플로우
- 메모리 누수
- 메모리 오염(UAF/버퍼 오버플로우/잘못된 캐스팅)
- 크래시형 버그와 오동작형 버그의 추적 방식 차이
- 재현 중심 디버깅 루프(가설 -> 검증 -> 수정 -> 회귀 확인)
학습 목표
- 버그 증상만 보고도 우선 의심할 범주를 빠르게 좁힐 수 있다.
- “왜 이 포인터가 nullptr인가?”, “왜 이 값이 범위를 벗어났나?”를 역추적할 수 있다.
- 재현이 어려운 버그에서 로그/계측을 어떻게 설계할지 설명할 수 있다.
왜 이 시점에 디버깅을 배우나
- 동적 할당(
new/delete), 포인터, 상속이 섞이기 시작하면
버그가 “컴파일 에러”보다 “런타임 사고” 형태로 많이 나타납니다.
- 즉, 문법을 아는 것만으로는 부족하고
실행 중 상태를 추적하는 능력이 필수입니다.
버그 유형 분류표(우선순위 관점)
| 빈번도(실무 체감) | 버그 유형 | 대표 증상 | 1차 대응 |
|---|
| 매우 높음 | Null 크래시 | 특정 줄에서 즉시 크래시 | 포인터 null 여부, 호출 스택 역추적 |
| 중간 | 정수 오버플로우/언더플로우 | 값이 갑자기 음수/비정상 | 범위 체크, 타입 확장, 계산식 검증 |
| 중간 | 메모리 누수 | 메모리 사용량이 계속 상승 | 할당/해제 짝 점검, 소유권 점검 |
| 낮지만 치명적 | 메모리 오염 | 랜덤 크래시/지연 크래시 | UAF/범위초과/잘못된 캐스팅 의심 |
크래시 vs 오동작
- 크래시형: 바로 종료되므로 “터진 지점”은 분명한 편
- 오동작형: 프로그램은 살아 있으나 결과가 틀림 (추적이 더 어려운 경우가 많음)
Null 크래시(가장 흔한 사고)
정의
- null 포인터(
nullptr)를 역참조할 때 발생하는 크래시
대표 시나리오
Player* FindPlayer(int id)
{
return nullptr;
}
Player* p = FindPlayer(100);
p->hp = 100;
핵심:
- “못 찾음”을
nullptr로 표현하는 함수는 매우 많습니다.
- 호출자 쪽에서 null 체크를 하지 않으면 크래시가 납니다.
void Test(Player* p)
{
if (p == nullptr)
return;
p->hp = 10;
}
추적 루틴(실전)
- 크래시 줄 확인
- 해당 포인터가 왜 null인지 호출 스택 역추적
- “누가 null을 넣었는지” 또는 “왜 생성 실패/조회 실패했는지” 원인 고정
정수 오버플로우·언더플로우
개념
- 정수 타입의 표현 범위를 벗어나면 값이 깨집니다.
- 특히 게임 로직(HP/골드/데미지 누적)에서 자주 숨어듭니다.
중요한 정확성:
- unsigned 오버플로우는 모듈러 래핑이 정의됨
- signed 오버플로우는 C++에서 UB(정의되지 않은 동작)
(실무에서는 래핑처럼 보이는 경우가 많아도 “보장”은 아님)
자주 터지는 패턴
int * int 곱셈 누적
- 루프 누적 합
- 체력/경험치 상한 미보정
방어 습관
- 상/하한 클램프 (
0 ~ maxHp)
- 필요 시 더 큰 타입(
long long, std::int64_t)
- 곱셈 전 캐스팅/범위 검증
메모리 누수(Memory Leak)
정의
- 할당된 메모리가 해제되지 않아 회수 불가능한 상태로 남는 현상
증상
- 메모리 사용량이 시간에 따라 계속 상승
- 장시간 실행 후 성능 저하 -> OOM/크래시
감지 힌트
- 메모리 그래프의 우상향 추세
- 장시간/반복 시나리오(스폰, 로딩, 전투 반복)에서 가속
- 32비트 환경 테스트로 조기 재현을 유도하는 방법도 실무에서 자주 사용
기본 대응
new/delete 짝 점검
- 소유권(누가 해제?) 계약 점검
- 부모 포인터 삭제 구조면
virtual 소멸자 확인
메모리 오염(Memory Corruption)
정의
- 잘못된 메모리 접근으로 데이터가 조용히 손상되는 현상
- “문제 발생 시점”과 “터지는 시점”이 달라 추적이 매우 어렵습니다.
대표 3종
- Use-After-Free (UAF)
삭제된 객체를 계속 참조해서 접근
- 버퍼 오버플로우
할당 범위를 넘는 인덱스/복사
- 잘못된 캐스팅
관계 없는 타입으로 강제 변환해 메모리 해석을 깨뜨림
주의할 점
delete p; p = nullptr;는 “p 하나”만 안전해집니다.
- 같은 대상을 가리키던 다른 포인터(alias)는 여전히 댕글링일 수 있습니다.
디버깅 방법론: 눈대중이 아니라 실험
핵심 루프
- 재현
- 가설 수립
- 계측/브레이크포인트로 검증
- 수정
- 회귀 확인(비슷한 경로까지 재검증)
왜 중요한가
- 좋은 개발자는 “버그를 안 만드는 사람”보다
“버그를 빨리 좁히고 정확히 고치는 사람”에 가깝습니다.
재현이 어려운 버그 대응
- 발생 조건(맵/인원/시간/입력)을 로그로 구조화
- 랜덤 시드는 고정해서 재실행 가능하게 만들기
- “한 달에 한 번” 이슈는 샘플 부족이 핵심이므로
계측 지점을 늘려서 확률을 데이터로 바꿔야 합니다.