본문 바로가기

디버깅과 테스트

버그를 재현하고 로그·브레이크포인트로 원인을 좁힌 뒤 개발 테스트와 QA로 회귀를 방지합니다.

게임 시스템 구현에서 탄약 하나를 캐릭터, 픽업, HUD, 저장까지 이었습니다. 코드를 쓰고 기능을 더하는 것이 개발의 중심이지만, 완벽한 코드는 없고 예상하지 못한 버그는 계속 생깁니다. 그래서 필요한 것이 디버깅(Debugging)과 테스트(Testing)입니다. 두 과정은 게임의 품질을 지키고 플레이어에게 안정적인 경험을 주는 데 꼭 필요합니다.

이 글에서는 먼저 앞 글의 탄약 흐름에서 예상과 다른 결과가 나올 때 어디부터 볼지를 정리합니다. 그다음 언리얼 에디터와 외부 IDE의 디버깅 도구, 로그 시스템을 살펴보고, 개발 중 테스트, QA, 플레이테스트, 버그 트래킹으로 같은 문제가 다시 생기지 않게 하는 방법을 알아봅니다.


작은 탄약 흐름의 예상과 실패 확인

게임 시스템 구현의 첫 목표는 새 슬롯에서 0/30 → 픽업 → 10/30 → F5 저장 → 새 실행 10/30으로 이어지는 것입니다. 다음 표는 코드를 같은 실제 클래스에 합치고 에셋과 입력을 설정한 뒤 확인할 예상입니다. 예제를 실행해 얻은 로그나 오류를 재현한 결과가 아니라, 코드를 따라 읽은 예상이라는 점에 주의합니다.

조건이나 행동예상 결과먼저 확인할 곳
처음 쓰는 AmmoLoopSlot, 사용자 인덱스 0읽기 판정 뒤 0/30, 파일을 자동으로 만들지 않음슬롯과 사용자 값, DoesSaveGameExist, 캐릭터 초기화
현재 캡슐로 픽업을 한 번 습득실제 증가량 10, getter와 HUD 10/30, 픽업 한 번 소모현재 조종, 처음 판정, 양쪽 오버랩, 캡슐 필터, AddAmmo의 결과
10/30에서 F5 저장SavedAmmo에 10을 복사하고 API가 true일 때만 성공 표시SaveAmmoAction, 활성 매핑 컨텍스트, 바인딩, 현재 폰과 getter, 쓰기 결과
종료 뒤 같은 유효 슬롯으로 새 실행읽은 탄약 10을 준비된 폰에 적용하고 HUD 10/30LoadAmmoLoopOnce, null, 캐스팅, 범위, 보류 해제, 현재 getter, HUD 현재 값 조회
슬롯은 있지만 null, 다른 타입, 0~30 밖의 값오류 상태, 저장과 습득 중단, 초기값 0을 자동 저장하거나 파일을 지우지 않음bLoadFailed, 실제 SaveGame 타입, SavedAmmo, 값을 잘라 내지 않는지
현재 폰이 없거나, 다른 클래스이거나, 초기화 전이거나, 로드가 끝나지 않음SaveAmmoLoop가 false, 쓰기 호출 전에 멈춤컨트롤러 0의 GetPawn, 실제 캐릭터 타입, 읽기, 준비, 보류 상태
AmmoSaveGameClass가 비었거나 객체 생성 실패false, 쓰기 API와 성공 표시 없음실제 게임 모드의 별도 Ammo Save Game Class, 생성 뒤 캐스팅
SaveGameToSlot이 false쓰기 실패 표시, 이전 파일이 원자적으로 보존된다는 보장 없음API의 bool 결과, 사용 환경, 백업과 롤백은 구현하지 않음
저장값 0, 초기값 0유효한 적용 성공, 보류 해제TryApplyLoadedAmmo의 검증과 true, 증가량 0과 구별
HUD가 적용 뒤에 준비됨파일을 다시 읽지 않고 현재 getter 값 10을 표시AddToViewport와 Super::OnPossess 뒤의 현재 값 조회, OnAmmoChanged
준비 중의 오버랩이 거절됨증가량 0, 픽업이 남음, 준비 뒤 나갔다 다시 들어가야 재시도준비 조건과 새 BeginOverlap, 박스 안에서 자동 재지급 없음
새 실행에서 픽업을 다시 습득복원된 10에서 20이 될 수 있음탄약만 저장하며 레벨 픽업의 소모와 월드는 저장하지 않음

getter가 10인데 화면이 0이라면 저장이나 습득을 다시 실행하기보다 컨트롤러의 알림, 위젯 갱신, 현재 값 조회를 비교합니다. 반대로 getter도 0이라면 HUD보다 앞 단계인 읽기 판정, 보류 상태, 실제 폰 적용을 확인합니다. 초기값 0이 보이는 화면에서 저장 키를 눌러도, 로드가 끝나지 않았거나 오류 상태라면 준비 조건이 쓰기를 막습니다. 유효한 저장값을 적용한 뒤에 현재 탄약 0을 의도적으로 저장하는 경우와는 구별해야 합니다.

이 예제는 기존 슬롯을 읽지 못했을 때의 삭제, 복구, 재시도 버튼이나 파일 버전 변환을 제공하지 않으며, 위치, 체력, 월드 전체, 픽업 소모도 저장하지 않습니다. 저장 입력은 단순한 bool 액션의 Started 한 경로이고 블루프린트에서 중복 저장하지 않습니다. 단일 로컬 독립 실행의 처음 시작과 로드만 다루므로 리스폰, 전체 재빙의와 빙의 해제, 분할 화면, 네트워크 동기화에는 따로 구현이 필요합니다. 아래의 디버깅 도구와 테스트 방법은 이 작은 흐름에서 확인할 대상을 넓힐 때 참고합니다.


디버깅: 문제의 원인 찾기

디버깅은 코드의 오류를 찾아 고치는 과정입니다. 언리얼 엔진에는 디버깅 도구가 여럿 들어 있고, 외부 IDE와 연동하면 C++ 코드를 더 세밀하게 살펴볼 수 있습니다. 증상이 월드 상태에 있는지, 블루프린트 흐름에 있는지, C++ 호출 경로에 있는지에 따라 먼저 열어 볼 도구가 달라집니다.

언리얼 에디터 안의 디버깅 도구

가장 빠르고 기본적인 도구는 인게임 콘솔입니다. 게임을 실행하는 중에 ~ 키로 콘솔을 열고 명령을 입력해 게임 상태를 확인하거나 바꿀 수 있습니다. stat fps는 현재 프레임 속도를, stat unit은 게임 스레드, 렌더 스레드, GPU의 프레임 시간을 보여 주고, show collision은 콜리전을 시각화하며, toggledebugcamera는 자유 시점의 디버그 카메라를 켭니다. log LogTemp Warning처럼 입력하면 실행 중에 특정 로그 카테고리의 출력 수준을 바꿀 수 있습니다.

AI를 살펴볼 때는 게임플레이 디버거(Gameplay Debugger)를 씁니다. 실행 중에 기본 키인 작은따옴표(')로 열고, 대상 AI를 가리킨 뒤 숫자 키패드로 AI나 Behavior Tree 같은 카테고리를 켜고 끕니다. 단축키와 카테고리의 활성화는 프로젝트 설정에 따라 다를 수 있습니다.

블루프린트 로직은 블루프린트 디버거(Blueprint Debugger)로 실행 흐름을 시각적으로 따라가고 변수 값을 확인합니다. UE 5.8 문서 기준으로 에디터의 Tools 메뉴나 블루프린트 에디터의 Debug 메뉴에서 열 수 있습니다. 노드에 중단점(breakpoint, 브레이크포인트)을 걸면 그 지점에서 실행을 멈추고 단계별로 진행할 수 있고, 와치(Watch)로 특정 변수의 값을 실시간으로 지켜볼 수 있습니다.

비주얼 로거(Visual Logger)는 게임 실행 중의 이벤트, AI 행동, 이동 경로 같은 정보를 시각적으로 기록했다가 나중에 다시 재생해 분석하는 도구로, 복잡한 AI나 물리 시뮬레이션을 디버깅할 때 특히 쓸모 있습니다. 언리얼 엔진 5에서는 Tools 메뉴의 Debug 아래에 있으며, 공식 문서의 일부 페이지는 예전 경로인 Window > Developer Tools로 안내하므로 쓰는 버전의 메뉴에서 확인합니다. 비주얼 로거 창에서 기록을 시작한 뒤 Visual Logger 이벤트가 실제로 기록되는지 확인해야 하며, 일반 로그가 모두 자동으로 공간 기록이 되는 것은 아닙니다.

성능 문제는 언리얼 인사이트의 타이밍 인사이트로 CPU 스레드와 작업을 분석하고, GPU 프로파일과 함께 보며 병목을 구분합니다. stat startfile과 stat stopfile로 만드는 stats 캡처와 Insights 트레이스는 서로 다른 기록 형식입니다. 메모리를 분석하려면 메모리 인사이트를 위해 지원되는 Development 실행을 처음부터 -trace=default,memory로 시작하고, 할당과 해제, 콜스택, 시점별로 살아 있는 메모리를 비교합니다. Session Frontend에 범용 메모리 스냅샷 탭이 있다고 가정하지 않습니다.

외부 IDE(Visual Studio, Xcode)의 디버깅

C++ 코드를 쓰는 프로젝트에서는 IDE 디버거가 가장 세밀한 디버깅 도구입니다. 코드의 특정 줄에 중단점을 걸면 실행을 멈추고 그 지점의 변수 값, 호출 스택, 레지스터를 검사할 수 있습니다. 멈춘 뒤에는 단계별로 실행하는데, Visual Studio에서 Step Over(F10)는 현재 줄을 실행하고 다음 줄로 가며 함수 안으로 들어가지 않고, Step Into(F11)는 함수 호출이 있으면 그 안으로 들어가며, Step Out(Shift+F11)은 현재 함수를 끝까지 실행하고 호출한 곳으로 돌아갑니다.

조사식(Watch)과 지역(Locals) 창에서는 변수의 값을 확인하고 바꿀 수 있습니다. 호출 스택(Call Stack)은 지금 코드가 어떤 함수 호출 경로로 실행되었는지 보여 주므로, 버그의 근본 원인을 추적할 때 빼놓을 수 없습니다.

빌드 구성도 디버깅에 영향을 줍니다. Visual Studio 프로젝트의 DebugGame Editor나 DebugGame 구성은 게임 코드의 최적화를 낮춰 디버깅하기 편하게 합니다. Development와 Shipping도 구성에 따라 심볼을 만들 수 있으며, 정확히 일치하는 실행 파일과 심볼을 함께 보관해야 합니다. 최적화된 코드에서는 일부 변수나 소스 단계가 보이지 않을 수 있습니다.

로그 시스템 활용

C++에서는 UE_LOG(LogCategory, Verbosity, TEXT("Your message here..."));처럼 카테고리와 출력 수준(Error, Warning, Log, Verbose 등)을 정해 메시지를 남깁니다. 이 메시지는 Output Log 창과 로그 파일에 기록됩니다. 블루프린트에서는 Print String 노드로 디버그 메시지를 화면에 표시하거나 Output Log에 출력하는 방법이 가장 흔합니다.


화면의 0에서 원인을 한 단계씩 좁히기

예를 하나 들어 보겠습니다. 현재 실행에서 처음 읽기가 끝났고, 보류도 해제되었으며, 캐릭터의 getter는 10인데 HUD만 0을 보인다고 가정합니다. 이미 데이터는 적용되었으므로 저장 파일부터 덮어쓰거나 픽업을 다시 지급할 이유가 없습니다. 대신 실제 컨트롤러와 위젯 클래스, 자식 위젯의 이름, OnAmmoChanged 전달, HUD가 준비된 뒤 SyncHUDFromPawn의 현재 값 조회를 차례로 비교합니다. 같은 실행 파일, 맵, 슬롯, 입력 순서에서 getter와 화면의 값을 함께 기록해 두면 수정 전과 후를 공정하게 비교할 수 있습니다.

확인의 범위도 나눠서 생각합니다. 단위 확인은 유효 범위와 남은 용량의 계산을, 통합 확인은 캐릭터에서 컨트롤러를 거쳐 HUD로 가는 전달을, 끝까지의 확인은 F5로 저장한 뒤 새 실행에서 읽고 적용하는 과정까지를 다룹니다. 함수 하나의 계산이 맞다고 해서 파일, 입력, 실제 에셋 설정까지 검증되었다고 넓혀 해석하지 않습니다.


테스트: 게임의 품질 보증

테스트는 게임이 의도한 대로 동작하는지, 안정적인지, 플레이어에게 즐거운 경험을 주는지 확인하는 과정입니다. 개발자가 직접 하는 테스트, 체계적인 QA, 실제 플레이어의 플레이테스트로 나눠 살펴봅니다.

개발 중 테스트(Developer Testing)

단위 테스트(Unit Testing)는 함수 하나나 특정 클래스처럼 게임의 가장 작은 단위가 올바르게 동작하는지 확인합니다. 언리얼 엔진은 자동화 테스트 프레임워크(Automation Test Framework)로 이런 테스트를 지원합니다. C++로 작성하며 Misc/AutomationTest.h의 IMPLEMENT_SIMPLE_AUTOMATION_TEST나 IMPLEMENT_COMPLEX_AUTOMATION_TEST 매크로로 테스트를 정의하고, 테스트를 넣을 모듈과 테스트 플래그, 실행 컨텍스트를 정합니다. 행동 주도 설계(BDD) 방식으로 쓰는 Automation Spec과 블루프린트로 레벨 단위의 동작을 시험하는 Functional Testing도 있습니다. 엔진에 의존하지 않는 순수한 단위 테스트라면 Low-Level Tests를 검토할 수 있습니다. 엔진의 자동화 테스트 예제와 쓰는 버전의 모듈 의존성도 함께 확인합니다.

UE 5.8 문서 기준으로 에디터에서 자동화 테스트를 실행하려면 Tools > Test Automation을 열고, Session Frontend의 Automation 탭에서 테스트를 골라 실행한 뒤 결과를 확인합니다. 공식 안내는 그 전에 Functional Testing Editor 플러그인과 실행할 테스트가 들어 있는 플러그인을 켜고 에디터를 다시 시작하라고 합니다.

통합 테스트(Integration Testing)는 인벤토리 시스템과 UI 시스템처럼 여러 모듈이나 시스템이 함께 동작할 때 올바르게 연동되는지 확인합니다. 테스트가 확인하는 범위에 따라 단위, 통합, 시스템, 인수 테스트로 나누는 기준과 변경 뒤에 다시 확인할 회귀 사례를 고르는 방법은 통합·인수·회귀 테스트에서 더 자세히 다룹니다. 엔드 투 엔드 테스트(End-to-End Testing)는 게임의 시작부터 끝까지 전체 흐름이 의도대로 동작하는지, 실제 플레이어처럼 진행하며 확인합니다. 블루프린트 유효성 검사(Blueprint Validation)는 에디터 유틸리티 위젯이나 Python 스크립트로 변수 이름 규칙, 불필요한 노드 같은 블루프린트 에셋의 규칙을 자동으로 검사합니다.

QA(Quality Assurance) 테스트

QA 테스트는 개발팀 내부나 전문 QA 팀이 체계적으로 수행하는 테스트입니다. 기능 테스트(Functional Testing)는 모든 게임 기능이 GDD대로 동작하는지 확인하고, 호환성 테스트(Compatibility Testing)는 다양한 하드웨어 구성, 운영 체제, 드라이버 환경에서 게임이 제대로 동작하는지 확인하며 특히 PC 게임에서 중요합니다. 성능 테스트(Performance Testing)는 목표 프레임 속도, 로딩 시간, 메모리 사용량 같은 지표를 측정해 목표를 달성하는지 확인하며, 스트레스 테스트와 부하 테스트를 포함합니다. 안정성 테스트(Stability Testing)는 장시간 플레이, 반복 행동, 극단적인 상황에서도 크래시 없이 동작하는지 확인합니다.

멀티플레이어 테스트는 네트워크 지연, 패킷 손실, 동시 접속자 증가 같은 다양한 네트워크 환경에서 동기화와 안정성을 시험합니다. 사용성 테스트(Usability Testing)는 UI와 UX가 직관적이고 편리한지 실제 사용자의 피드백으로 검증하고, 지역화 테스트(Localization Testing)는 모든 텍스트와 음성이 번역된 언어로 올바르게 표시되고 동작하는지 확인합니다. 규정 준수 테스트(Compliance Testing)는 콘솔 인증이나 모바일 스토어 정책 같은 플랫폼별 가이드라인을 지키는지 확인합니다.

플레이테스트(Playtesting)

플레이테스트는 재미, 밸런스, 플레이어 경험처럼 정성적인 부분을 평가하기 위해 실제 플레이어에게 게임을 하게 하고 피드백을 모으는 과정입니다. 내부 팀원, 친구, 외부 베타 테스터 그룹을 모아 진행하고, 설문 조사, 인터뷰, 직접 관찰, 인게임 데이터 로깅으로 피드백을 모아 분석합니다.


버그 트래킹과 관리

발견한 버그는 Jira, Trello, Asana, MantisBT 같은 버그 트래킹 시스템에 모두 기록하고 관리합니다. 기록에는 버그 제목, 재현 단계, 예상 결과, 실제 결과, 스크린샷이나 영상, 발생 빈도, 중요도, 담당자, 상태(새로 생성, 할당됨, 수정됨, 검증됨, 닫힘 등)를 남깁니다. 이 가운데 버그를 정확히 재현할 수 있는 단계를 분명히 쓰는 것이 가장 중요합니다.

우선순위는 게임 진행 불가나 크래시 같은 심각도와 발생 빈도에 따라 정하고 수정 계획을 세웁니다. 버그를 고친 뒤에는 회귀 테스트(Regression Testing)로 그 수정 때문에 다른 기능에 새 버그가 생기지 않았는지 확인합니다. 디버깅과 테스트의 결과가 버그 리포트, 자동화 테스트, 회귀 확인으로 이어져야 같은 문제가 다시 숨어들 가능성이 낮아집니다.

증상별로 먼저 볼 곳과 수정한 뒤 확인할 내용은 다음과 같습니다.

증상먼저 볼 곳수정 후 확인
C++ 크래시같은 빌드의 심볼, 콜스택, 로그최소 재현과 주변 경로
블루프린트 분기대상 인스턴스, 중단점, 와치같은 입력과 상태 순서
AI 정지블랙보드, 현재 태스크, 내비메시목표 변경, 실패, 중단(Abort)
성능과 메모리타이밍 인사이트와 GPU 프로파일, 메모리 인사이트같은 장치, 장면, 품질에서의 수치

버그 기록에는 예상 결과와 실제로 본 결과를 나눠 적습니다. 수정 전의 실패를 남겨 두고, 같은 조건에서 다시 검사하고, 필요한 회귀 확인과 연결해야 합니다.


정리

  • 탄약 흐름에서 결과가 예상과 다르면 getter와 화면을 함께 보고, getter가 맞으면 HUD 전달을, getter도 틀리면 읽기 판정, 보류, 폰 적용을 먼저 확인합니다.
  • 인게임 콘솔, 게임플레이 디버거, 블루프린트 디버거, 비주얼 로거, 언리얼 인사이트는 증상이 있는 위치에 따라 골라 씁니다.
  • C++ 코드는 IDE의 중단점, 단계별 실행, 호출 스택으로 추적하며, DebugGame 구성과 일치하는 심볼이 디버깅을 돕습니다.
  • UE_LOG와 Print String은 카테고리와 출력 수준을 정해 실행 흐름을 기록하는 기본 도구입니다.
  • 자동화 테스트 프레임워크로 단위 테스트를 만들고, UE 5.8에서는 Tools > Test Automation에서 실행합니다.
  • QA와 플레이테스트는 기능, 호환성, 성능, 안정성, 재미를 넓게 검증하고, 버그 트래킹과 회귀 테스트로 같은 문제의 재발을 막습니다.

연습 문제

1. 아직 적용하지 않은 값과 표시하지 못한 값

화면도 getter도 0이지만, 로드한 값 17은 유효하고 bHasPendingAmmo가 true이며 현재 폰이 아직 준비되지 않았습니다. 본문의 예처럼 HUD 문제로 확정할 수 있을까요? 준비된 캐릭터에 17을 적용해 보류가 해제된 뒤 HUD가 생기면 기대할 숫자는 무엇이고, 무엇으로 확인할까요?

해설 보기

17이 아직 보류 중이므로 getter 0을 HUD 전달 실패로 확정할 수 없습니다. 먼저 현재 폰의 실제 타입, 초기화, 조종 조건과, 준비 경로에서 TryApplyPendingAmmo가 불리는지를 확인합니다. 유효하게 적용되면 getter 17과 보류 해제를 확인하고, 그 뒤 HUD의 현재 값 조회로 17/30이 보이는지 봅니다. getter가 17인데 화면만 0으로 남는다면 그때 본문 예의 전달과 표시 경로를 좁힙니다. 가정한 조건으로 판정하는 연습이며 실제로 재현한 오류 로그는 아닙니다.

2. 0이 보일 때 저장 키를 누르면

새 실행에서 화면에 0/30이 보이자 F5를 눌렀습니다. 저장 파일의 10이 0으로 덮어써질까요?

해설 보기

처음 읽기가 끝나지 않았거나, 보류 중이거나, 오류 상태라면 SaveAmmoLoop가 쓰기 전에 false를 돌려주므로 덮어쓰지 않습니다. 반대로 유효한 값을 적용한 뒤 현재 탄약이 실제로 0이라면 0을 저장하는 것은 의도된 동작입니다. 어느 경우인지는 읽기 판정, 보류 상태, 현재 getter를 함께 보고 구별합니다.

3. 도구 고르기

AI가 순찰하다가 갑자기 멈춥니다. 어떤 도구로 무엇을 먼저 볼까요?

해설 보기

게임플레이 디버거로 대상 AI를 고르고 Behavior Tree 카테고리를 켜서 블랙보드 값과 현재 실행 중인 태스크를 봅니다. 이동 경로가 의심되면 내비메시가 이동할 영역을 덮는지 확인하고, 시간에 따른 변화를 다시 보고 싶다면 비주얼 로거로 기록해 재생합니다. 고친 뒤에는 목표가 바뀔 때, 태스크가 실패할 때, 중단될 때의 동작도 함께 확인합니다.

디버깅과 테스트 도구는 Epic의 Automation Test Framework 문서, Run Automation Tests 문서, Gameplay Debugger 문서, Blueprint Debugger 문서, Visual Logger 문서, Unreal Insights 문서에서 더 자세히 확인할 수 있습니다.

다음 글 출시와 운영 전략에서는 완성한 게임을 출시하고, 출시 뒤의 업데이트와 운영을 어떻게 계획할지 살펴봅니다.