디버깅과 로그 관리
로그 카테고리·콘솔 명령·IDE 디버거·Blueprint 디버거·Visual Logger로 배포 후 문제를 추적합니다.
게임을 배포한 뒤에도 문제는 계속 생깁니다. 개발자의 컴퓨터에서는 한 번도 나지 않던 크래시가 특정 그래픽 카드에서만 나고, 어떤 플레이어는 특정 순서로 퀘스트를 진행할 때만 문이 열리지 않는다고 알려 옵니다. 이런 문제는 에디터에서 다시 만들기 어렵기 때문에, 실행 중에 무슨 일이 있었는지 알려 주는 디버깅 도구와 로그가 없으면 원인을 찾을 방법이 거의 없습니다.
플랫폼별 빌드와 패키징에서 게임을 패키징했으니, 이 글에서는 문제를 찾는 도구를 살펴봅니다. 인게임 콘솔 명령, IDE 디버거, 블루프린트 디버거, 비주얼 로거를 차례로 다루고, UE_LOG로 로그를 남기고 관리하는 방법, 크래시 보고와 배포된 게임을 디버깅하는 전략까지 정리합니다.
디버깅이 하는 일
디버깅은 소프트웨어의 버그나 오류를 찾아 고치는 과정입니다. 게임 개발에서는 예상하지 못한 크래시, 이상한 동작, 기능 오작동을 고치는 데 그치지 않고, 프레임 저하나 로딩 지연 같은 성능 문제의 근본 원인을 찾는 데도 씁니다. 그렇게 해서 게임이 여러 환경에서 안정적으로 동작하게 만들고, 플레이어가 버그 없이 부드럽게 즐길 수 있게 합니다.
언리얼의 디버깅 도구
언리얼 엔진은 자체 디버깅 기능과 함께 Visual Studio, Xcode 같은 외부 IDE와의 연동을 지원합니다. 아래 네 도구는 보는 대상이 서로 다릅니다. 콘솔 명령은 실행 중인 게임의 설정과 수치를 바로 보여 주고, IDE 디버거는 C++ 코드를 멈춰 가며 변수와 호출 경로를 살피게 하며, 블루프린트 디버거는 같은 일을 블루프린트 그래프에서 합니다. 비주얼 로거는 실행하는 동안 기록한 정보를 나중에 시간 순서로 다시 보게 해 주므로, 한 순간에 멈춰서는 알아보기 어려운 AI나 이동 문제에 알맞습니다.
인게임 콘솔 명령
가장 빠르고 기본적인 도구입니다. 콘솔이 들어 있는 개발 환경에서 ~ 키로 콘솔을 열고 명령을 입력합니다. Shipping에서 기본으로 쓸 수 있는 기능과 플랫폼별 지원 여부는 다를 수 있습니다.
| 명령 | 하는 일 |
|---|---|
r.SetRes [Width]x[Height]f | 해상도를 바꿉니다(예: r.SetRes 1920x1080f). |
stat fps, stat unit | 프레임 속도와 CPU, GPU 시간을 표시합니다(프로파일링 도구 사용법 참고). |
stat [Category] | 특정 통계 분류를 표시합니다(예: stat anim, stat gpu, stat net). |
show [Category] | 특정 렌더링 기능을 켜고 끕니다(예: show collision, show postprocess, show particles). |
toggledebugcamera | 자유롭게 움직이는 디버그 카메라를 켜고 끕니다. |
quit | 게임을 종료합니다. |
pause | 지원되는 게임에서 일시 정지를 켜고 끕니다. |
obj list class=[ClassName] | 특정 클래스의 모든 객체를 나열합니다. |
showdebug | 현재 디버그 대상의 HUD 정보를 보여 줍니다. 표시 항목은 게임과 엔진 버전에 따라 다릅니다. |
t.MaxFPS [FPS] | 최대 프레임 속도를 제한합니다(예: t.MaxFPS 60). |
IDE 디버거
C++ 코드를 디버깅할 때 가장 쓸모 있는 도구는 Visual Studio나 Xcode의 디버거입니다. 코드의 특정 줄에 중단점(breakpoint)을 걸면 게임 실행이 그 자리에서 멈추고, 그 순간의 변수 값과 호출 스택을 살펴볼 수 있습니다. 한 줄씩 넘어가는 Step Over, 함수 안으로 들어가는 Step Into, 현재 함수에서 빠져나오는 Step Out으로 실행 흐름을 따라가고, Locals와 Watch 창으로 지금 범위의 변수나 지정한 변수의 값을 확인합니다. 호출 스택(call stack) 창은 지금 코드가 어떤 함수 호출 경로로 실행되었는지 보여 주어 버그의 근본 원인을 추적하게 해 줍니다.
어떤 구성으로 빌드했는지도 중요합니다. DebugGame은 게임 코드를 최적화하지 않아 소스를 따라가기 쉽습니다. Development나 Shipping도 맞는 심볼이 있으면 디버깅할 수 있지만, 최적화 때문에 줄 단위 실행이나 변수 값이 소스와 다르게 보일 수 있습니다.
블루프린트 디버거
블루프린트 로직을 디버깅할 때는 블루프린트 디버거를 씁니다. 언리얼 엔진 5에서는 에디터의 Tools 메뉴 아래 Debug 항목에서 Blueprint Debugger를 엽니다. 언리얼 엔진 4에서는 Window > Developer Tools에 있었으며, 메뉴 위치는 버전에 따라 다를 수 있습니다.
게임을 실행하는 동안 디버거에서 디버깅할 블루프린트 인스턴스를 고르면, 노드가 실행될 때마다 강조 표시되어 로직의 흐름을 따라갈 수 있습니다. 노드를 누르거나 디테일 패널에서 그 노드가 쓰는 변수 값을 확인하고, 노드에 중단점을 걸어 실행을 멈추고 살펴볼 수도 있습니다.
비주얼 로거
비주얼 로거(Visual Logger)는 게임 실행 중의 이벤트나 데이터를 시각적으로 기록했다가 다시 보여 주는 도구로, AI의 동작, 애니메이션 상태, 물리 충돌을 디버깅할 때 특히 쓸모 있습니다. 에디터에서 비주얼 로거 창을 열고 기록을 시작하는데, 언리얼 엔진 5에서는 Tools 메뉴의 Debug 아래에 있으며 정확한 위치는 쓰는 버전에서 확인합니다.
C++에서는 UE_VLOG, UE_VLOG_SEGMENT 같은 매크로로 액터마다 텍스트나 도형 정보를 기록합니다. VisualLogger/VisualLogger.h를 include하고, 지원되는 빌드 구성인지 확인합니다. 게임이 끝난 뒤 비주얼 로거 창에서 기록한 세션을 불러오면 저장된 시각 로그와 스냅샷을 시간 순서로 볼 수 있습니다. 이것은 게임 상태 전체를 다시 실행하는 리플레이와는 다릅니다.
로그 남기기와 관리하기
로그는 게임 실행 중의 이벤트, 경고, 오류를 텍스트로 기록하는 시스템입니다. 버그 보고를 분석하거나 서비스 중인 게임의 문제를 진단할 때 가장 먼저 보게 되는 자료입니다.
UE_LOG 매크로
C++에서 가장 기본적인 로깅 함수는 UE_LOG(LogCategory, Verbosity, Format, ...)입니다. LogCategory는 로그의 분류로, LogTemp, LogNet, LogPhysics 같은 미리 정의된 분류가 있고 직접 만들 수도 있습니다. Format은 printf 형식의 문자열입니다.
Verbosity는 메시지의 중요도 수준입니다.
| 수준 | 뜻 |
|---|---|
Fatal | 기록한 뒤 프로세스를 종료하는 치명적 오류 |
Error | 심각한 오류 |
Warning | 잠재적인 문제 |
Display | 콘솔에도 표시할 중요한 정보 |
Log | 일반적인 정보 |
Verbose | 자세한 디버깅 정보 |
VeryVerbose | 아주 자세한 디버깅 정보(성능에 영향) |
다음은 직접 만든 로그 분류를 쓰는 부분 예제입니다. AMyActor의 헤더에 BeginPlay와 프로젝트의 bool SomeCondition이 선언되어 있고, cpp가 그 헤더를 include한다고 가정합니다. 헤더에서는 DECLARE_LOG_CATEGORY_EXTERN으로 분류를 선언합니다.
DECLARE_LOG_CATEGORY_EXTERN(LogMyActor, Log, All); // 커스텀 로그 카테고리 선언cpp 한 곳에서는 DEFINE_LOG_CATEGORY로 그 분류를 정의하고, 분류를 써서 로그를 남깁니다. 선언과 정의를 나누는 이유는 전역 변수와 같습니다. 헤더의 EXTERN 선언은 이 헤더를 include한 모든 파일이 LogMyActor라는 이름을 쓸 수 있게 하고, 정의는 프로그램 전체에 하나만 있어야 하므로 cpp 한 곳에만 둡니다. 선언의 두 번째 인자 Log는 실행할 때 기본으로 남길 수준이고, 세 번째 인자 All은 컴파일할 때 코드에 남겨 둘 가장 자세한 수준입니다.
DEFINE_LOG_CATEGORY(LogMyActor); // 커스텀 로그 카테고리 정의
void AMyActor::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogMyActor, Log, TEXT("MyActor spawned at location: %s"), *GetActorLocation().ToString());
if (SomeCondition)
{
UE_LOG(LogMyActor, Warning, TEXT("Warning: Some condition met!"));
}
else
{
UE_LOG(LogMyActor, Error, TEXT("Error: Critical condition failed!"));
}
}이 코드의 흐름을 따라가 봅시다. 먼저 BeginPlay의 선언과 부모 호출, SomeCondition의 실제 초기값을 확인합니다. 위치를 남기는 Log 줄은 if보다 앞에 있으므로 함수가 그 줄을 지나면 언제나 메시지를 요청하고, 조건이 참이면 Warning, 거짓이면 Error 메시지를 요청합니다. 거짓 분기의 Error는 조건이 실패했음을 표시할 뿐, Fatal처럼 프로세스를 일부러 끝내는 코드가 아닙니다.
로깅을 지원하고 이 세 수준이 모두 필터에 허용된 개발 실행에서 SomeCondition이 거짓이라면, 위치 Log와 Error가 차례로 남고 Warning은 남지 않습니다. Epic의 Logging 문서는 분류의 선언과 정의, 수준, 실행 중의 필터를 구분해 설명합니다. 출력 창만 보고 판단하지 말고, 지금 빌드가 로깅을 지원하는지, 분류의 수준이 어떻게 설정되었는지, 로그 파일에 무엇이 있는지를 함께 확인합니다.
더 깊이: 필드를 이름으로 남기는 UE_LOGFMT
언리얼 엔진 5.2부터는 UE_LOGFMT 매크로로 구조화된 로그(structured log) 이벤트를 남길 수 있습니다. printf 형식 대신 {Name}, {Error}처럼 중괄호로 필드를 가리키는 형식 문자열을 쓰고, 값은 위치(positional) 매개변수나 이름(named) 매개변수 가운데 한 방식으로 넘깁니다. 두 방식은 한 호출 안에서 섞을 수 없습니다. 위치 매개변수는 형식 문자열이 가리키는 필드와 하나씩 정확히 맞아야 하고, 이름 매개변수는 가리키는 필드를 모두 넣되 순서는 상관없으며 형식 문자열에 없는 필드를 더 넣어도 됩니다.
필드 이름은 영문자, 숫자, 밑줄로만 짓고 한 이벤트 안에서 겹치지 않게 합니다. 매크로는 분류, 수준, 형식 문자열과 함께 필드를 최대 16개까지 받으며, 쓰려면 Logging/StructuredLog.h를 include해야 합니다. 분류와 수준은 UE_LOG와 같으므로, 앞에서 만든 LogMyActor와 수준 필터를 그대로 쓸 수 있습니다.
블루프린트에서 로그 남기기
블루프린트에서는 Print String과 Print Text 노드로 로그를 남깁니다. Print String은 화면에 디버그 텍스트를 띄우는 데도 씁니다.
로그 파일의 위치와 수준 조절
로그 파일은 실행 환경의 Saved/Logs 경로에 생깁니다. 패키지, 플랫폼, 사용자 데이터 정책에 따라 설치 폴더가 아니라 사용자 데이터 영역에 있을 수 있고, 빌드에서 로깅이 켜져 있어야 합니다. 에디터에서 실행했다면 [ProjectName]\Saved\Logs\에 저장됩니다.
콘솔에서 Log [Category] [Verbosity] 명령으로 특정 분류의 출력 수준을 바꿀 수 있습니다. 예를 들어 log LogNet Verbose는 네트워크 로그를 자세하게 남기게 합니다. 같은 설정을 DefaultEngine.ini 같은 설정 파일에서도 할 수 있습니다.
크래시 보고
게임이 크래시하면 언리얼은 크래시 보고서를 만듭니다. 패키지 게임에서 이를 쓰려면 크래시 리포트 클라이언트(Crash Report Client)를 패키지에 포함해야 하고, 보고서를 받을 주소와 사용자 동의, 자동 전송 정책을 프로젝트에서 정해야 합니다. 패키지 게임의 크래시가 저절로 Epic에 전송된다고 가정하면 안 됩니다.
크래시 로그 파일([ProjectName]\Saved\Crashes\[Timestamp]\)에는 크래시 순간의 호출 스택이 들어 있어, Visual Studio 같은 도구에서 디버그 심볼 파일인 .pdb(Program Database)를 이용해 크래시 지점을 정확히 찾을 수 있습니다. 그래서 Shipping처럼 최적화된 빌드의 크래시를 분석하려면, 빌드할 때 만들어진 .pdb 파일을 심볼 서버에 올리거나 따로 보관해 두어야 합니다.
실제 문제를 다룰 때는 도구 하나만 보기보다, 증상을 관찰하고, 알맞은 진단 도구를 고르고, 로그 수준을 조정하고, 파일로 확인 자료를 모으고, 재현해 고치는 과정을 짧게 되풀이하는 편이 효율적입니다.
배포된 게임을 디버깅하는 전략
패키지 게임, 특히 최종 Shipping 빌드는 디버깅 기능이 빠지거나 줄어들어 직접 디버깅하기 어렵습니다. 그래서 QA나 내부 시험에는 DebugGame이나 Development 빌드를 써서 더 많은 디버깅 정보를 담고, 필요하면 원격 디버깅을 시도합니다.
심볼과 진단 기능은 따로 생각합니다. 디버그 파일을 포함하는 설정은 심볼을 함께 담는 것일 뿐 Shipping의 콘솔을 켜는 옵션이 아니므로, 필요한 진단은 Development나 Test 구성, 또는 지원되는 타깃 설정으로 설계합니다. 배포된 게임에 원격으로 연결해 디버깅할 때는 플랫폼이 지원하는 원격 디버거와 같은 빌드의 심볼을 씁니다. 콘텐츠를 원격으로 쿠킹하는 서버와 C++ 디버거 연결은 서로 다른 기능입니다.
플레이어에게서 문제가 생겼다는 연락을 받으면 로그 파일을 받아 분석하는 것이 일반적이며, 게임 안에서 로그 파일의 위치를 안내하는 기능을 넣어 둘 수 있습니다. 더 나아가 원격 분석(telemetry) 시스템을 통합해 플레이 데이터, 성능 데이터, 오류 정보를 모아 분석하면, 많은 사용자 사이에서 생기는 버그를 미리 알아채고 해결하는 데 큰 도움이 됩니다.
좋은 로그를 남기는 방법
로그 메시지는 구체적이고 쓸모 있어야 합니다. UE_LOG(LogTemp, Warning, TEXT("Error!"));처럼 남기기보다 UE_LOG(LogCombat, Error, TEXT("Player %s tried to attack with no weapon equipped!"), *GetPlayerName());처럼 상황을 함께 남깁니다.
수준도 알맞게 씁니다. Error와 Warning 같은 중요한 사건은 빌드와 실행 중의 필터에서 남도록 설정하고, Log와 Display는 중요한 정보에 씁니다. Verbose와 VeryVerbose는 개발과 디버깅 중에만 켜고 배포 빌드에서는 꺼서 성능 부담을 줄입니다. 매 프레임 도는 루프나 자주 불리는 함수에서 로그를 마구 남기면 성능이 떨어지므로 꼭 필요한 곳에만 남기고, AI, UI, 네트워크, 저장처럼 게임의 주요 시스템마다 분류를 따로 만들어 관리하고 걸러 보기 쉽게 합니다.
재현할 빌드, 맵, 입력 순서와 같은 빌드의 심볼을 함께 보관합니다. 로그에는 필요한 객체, 상태, 세션의 단서를 남기되, 비밀번호나 토큰 같은 민감한 값은 출력하지 않습니다.
더 깊이: 멈출지 알리기만 할지 고르는 어서션
로그는 일어난 일을 기록할 뿐이지만, 언리얼의 어서션(assertion) 매크로는 '이 조건은 반드시 참이어야 한다'는 가정이 깨진 순간을 잡아냅니다. check와 checkf는 식이 거짓이면 실행을 멈추며, checkf는 형식 문자열로 만든 메시지도 로그에 남깁니다. 이 매크로들은 기본적으로 Debug와 Development 빌드에서만 동작하고, USE_CHECKS_IN_SHIPPING=1로 설정해야 Test와 Shipping 빌드에서도 동작합니다. Asserts 문서는 이 값을 기본값 0으로 둔 채 출시하라고 권합니다.
조건을 끈 빌드에서 식이 사라진다는 점은 실수를 낳기 쉽습니다. check 안에 꼭 실행되어야 하는 함수 호출을 넣으면, 조건이 꺼진 빌드에서는 그 호출 자체가 일어나지 않을 수 있습니다. 이런 경우에는 verify나 verifyf를 씁니다. 이 매크로들은 check가 꺼진 빌드에서도 식을 평가하며, 다만 그 빌드에서는 멈추거나 로그를 남기지 않습니다.
실행을 멈추지 않고 알리기만 하려면 ensure 계열을 씁니다. ensure는 식이 거짓이면 크래시 리포터에 알린 뒤 실행을 계속하고, ensureMsgf는 메시지도 로그에 남깁니다. 기본 ensure와 ensureMsgf는 크래시 리포터가 넘치지 않도록 엔진이나 에디터 세션마다 한 번만 보고하며, 거짓이 될 때마다 보고하려면 ensureAlways나 ensureAlwaysMsgf를 씁니다. ensure 계열의 식은 모든 빌드에서 평가되지만, 크래시 리포터에 알리는 것은 Debug, Development, Test, Shipping Editor 빌드에서만입니다.
정리
- 인게임 콘솔 명령은 가장 빠른 진단 도구이며, Shipping에서는 기능이 제한될 수 있습니다.
- IDE 디버거로 중단점, 단계별 실행, 변수와 호출 스택을 살펴보며, 최적화된 빌드에서는 소스와 다르게 보일 수 있으므로 맞는 심볼과 구성을 씁니다.
- 블루프린트 디버거는 노드 실행과 변수를, 비주얼 로거는
UE_VLOG로 기록한 시각 정보를 시간 순서로 보여 줍니다. UE_LOG는 분류와 수준을 정해 로그를 남기며, 분류는 헤더에서DECLARE_LOG_CATEGORY_EXTERN, cpp에서DEFINE_LOG_CATEGORY로 만듭니다.Error는 프로세스를 끝내지 않고Fatal은 끝냅니다.- 로그가 보이지 않는 것은 필터, 빌드 지원, 파일 수집 조건 때문일 수도 있으므로, 그것만으로 함수가 실행되지 않았다고 판단할 수 없습니다.
- 크래시 분석에는 크래시 리포트 클라이언트 설정과 같은 빌드의
.pdb심볼이 필요하며, 로그에는 민감한 값을 남기지 않습니다.
연습 문제
1. Warning 이상만 남기면
앞의 BeginPlay에서 실행 중의 필터가 Warning 이상만 허용한다면, SomeCondition이 거짓일 때와 참일 때 각각 어떤 메시지가 파일에 남을까요?
해설 보기
거짓이면 Error는 허용되어 남고, 앞의 위치 Log는 필터에서 빠집니다. 참이면 Warning이 허용되어 남고, Error 분기는 아예 실행되지 않습니다. 두 경우 모두 위치 Log는 남지 않습니다. 이것은 코드와 필터 규칙으로 따져 본 결과이며, 실제로 로그를 기록해 확인한 결과는 아닙니다.
2. 위치 로그가 보이지 않는다
로그 파일에 위치 Log 메시지가 보이지 않습니다. 이것으로 BeginPlay가 실행되지 않았다고 확정할 수 있을까요?
해설 보기
확정할 수 없습니다. 위치 로그가 빠진 이유는 실행 중의 필터, 빌드의 로깅 지원, 파일을 모은 조건 때문일 수도 있으므로, 그것만으로 함수가 실행되지 않았다고 판단할 수 없습니다. 같은 실행에서 Warning이나 Error가 남았다면 오히려 BeginPlay가 실행되었다는 단서가 됩니다.
필요하면 맞는 심볼로 중단점을 걸거나, 허용된 수준의 다른 로그로 흐름을 확인합니다. 확인 자료를 늘리겠다고 개인 정보 같은 값을 더 출력하지는 않습니다.
3. 최적화된 빌드의 이상한 변수 값
Development 빌드에 디버거를 붙이고 중단점에서 멈췄더니, 분명히 값을 넣은 지역 변수가 <optimized out>으로 보이거나 한 줄씩 넘어갈 때 실행 순서가 소스와 다르게 튑니다. 버그일까요?
해설 보기
버그가 아니라 최적화 때문일 가능성이 큽니다. Development 빌드는 코드를 최적화하므로, 컴파일러가 변수를 레지스터에만 두거나 없애고, 줄의 순서를 바꾸거나 합칠 수 있습니다. 그래서 디버거에서 변수 값이 보이지 않거나 실행 순서가 소스와 다르게 보입니다.
소스를 정확히 따라가야 한다면 게임 코드를 최적화하지 않는 DebugGame 구성으로 빌드해 디버깅합니다. 다만 최적화 때문에만 생기는 버그라면 DebugGame에서는 재현되지 않을 수 있으므로, 그때는 로그로 확인 자료를 모읍니다.
다음 글 핫픽스와 업데이트 전략에서는 출시한 게임에 버그 수정과 새 콘텐츠를 전달하는 핫픽스와 업데이트의 방법을 살펴봅니다.