로그를 더 남길수록 놓친다? 게임 프로그래밍 디버깅의 역설

profile_image
작성자 리플레이설계자 은호
댓글 0건 조회 11회

테스터가 분명히 캐릭터가 허공으로 튕겼다고 말했는데, 개발 PC에서는 아무리 반복해도 정상입니다. 로그 파일은 수십 메가바이트나 쌓였지만 정작 충돌 직전의 입력 순서와 난수 상태는 남아 있지 않습니다. 이런 재현 불가 버그 앞에서는 로그를 더 많이 기록하는 습관이 오히려 중요한 단서를 감추기도 합니다.

게임 프로그래밍에서 필요한 것은 모든 상태를 무작정 출력하는 일이 아니라, 같은 장면을 다시 실행할 수 있는 최소한의 증거를 남기는 것입니다. 아래 방법들은 거대한 디버깅 시스템을 새로 사지 않고도 적용할 수 있는 리플레이, 상태 지문, 역방향 탐색, 자동 캡처 중심의 숨은 활용법입니다.

화면을 녹화하지 말고 입력을 녹화해야 하는 이유

영상은 증상만 보여주고 리플레이는 원인을 다시 만든다

버그 영상을 받으면 발생 시각과 화면의 이상 현상은 확인할 수 있지만, 내부에서 어떤 일이 일어났는지는 알기 어렵습니다. 반면 리플레이 데이터에는 프레임 또는 틱 번호, 플레이어 입력, 난수 시드, 중요한 외부 이벤트를 저장할 수 있습니다. 이 정보로 동일한 빌드에서 장면을 재실행하면 브레이크포인트를 걸고 충돌 판정 이전의 상태까지 직접 관찰할 수 있습니다.

여기서 의외의 요령은 월드 전체를 매 프레임 직렬화하지 않는 것입니다. 초당 60회 실행되는 게임이라면 이동 축, 버튼 변화, 카메라 방향처럼 시뮬레이션에 실제로 영향을 준 입력만 기록해도 파일이 매우 작아집니다. 버튼이 눌리지 않은 프레임까지 같은 값을 반복 저장하는 대신 값이 변한 틱만 저장하면 장시간 플레이도 부담 없이 담을 수 있습니다.

예를 들어 점프 후 경사면에서만 캐릭터가 튀는 버그라면 이동 입력과 점프 버튼, 시작 위치, 물리 시드, 맵 버전이 핵심입니다. 렌더링 해상도나 음량 값까지 함께 넣는 것은 대개 소음에 가깝습니다. 당신의 게임에서 결과를 바꾸는 입력과 단순한 표현 설정을 먼저 구분해 보세요.

  • 필수 헤더: 빌드 식별자, 맵 ID, 리플레이 포맷 버전, 최초 난수 시드를 저장합니다.
  • 틱 입력: 아날로그 축은 필요한 정밀도로 양자화하고 버튼은 비트 필드로 묶습니다.
  • 외부 사건: 서버 응답, 비동기 로딩 완료, 조인·이탈처럼 실행 순서에 영향을 주는 사건을 별도 기록합니다.
  • 검증 지점: 60틱이나 300틱마다 월드 상태의 해시를 넣어 재생이 어긋난 최초 구간을 찾습니다.
  • 개인정보 제외: 채팅, 계정 토큰, 실명 같은 정보는 리플레이에 포함하지 않거나 즉시 익명화합니다.
숨은 팁: 입력은 매 렌더 프레임이 아니라 시뮬레이션 틱을 기준으로 저장하세요. 프레임률이 달라도 같은 순서로 재생할 수 있어 재현 성공률이 크게 높아집니다.

10분짜리 순환 버퍼가 긴 로그보다 쓸모 있다

항상 녹화하되 메모리에는 최근 몇 분만 유지하는 순환 버퍼 방식도 유용합니다. 오류가 감지되거나 테스터가 단축키를 누른 순간, 직전 30초와 직후 10초만 파일로 확정하면 됩니다. 평소에는 저장 장치를 계속 쓰지 않으면서도 버그 이전의 행동을 보존할 수 있습니다.

저사양 환경이 걱정된다면 처음부터 거대한 시스템을 만들 필요가 없습니다. 30Hz 입력, 16비트 축 두 개, 버튼 비트 필드, 주기적 체크섬만 담은 소형 프로토타입으로 시작하세요. 작게 시작한 리플레이가 실제 버그 한 건을 재현하는지 검증한 뒤 네트워크 사건과 체크포인트를 추가하는 편이 안전합니다.

  1. 문제가 자주 생기는 한 개의 플레이 모드만 대상으로 정합니다.
  2. 게임 결과를 바꾸는 입력과 난수 발생 지점을 목록으로 만듭니다.
  3. 최근 5~10분을 보관하는 메모리 버퍼를 구현합니다.
  4. 수동 신고 키와 치명적 오류 감지 시 파일을 확정합니다.
  5. 개발 빌드에서 자동 재생하고 최초 해시 불일치 틱을 출력합니다.

로그 줄 대신 상태 지문을 남기면 범인이 빨리 보인다

월드 해시로 어긋난 최초 틱을 찾는다

리플레이가 재생되더라도 원본과 결과가 달라지는 순간을 찾지 못하면 수천 틱을 눈으로 훑어야 합니다. 이때 월드의 중요한 상태를 정규화해 해시로 만든 상태 지문이 효과적입니다. 원본 실행과 재생 실행의 지문을 비교하면 잘못된 화면이 나타나기 훨씬 전, 내부 상태가 처음 갈라진 틱을 포착할 수 있습니다.

해시에 모든 메모리 바이트를 그대로 넣으면 포인터 주소, 패딩, 초기화되지 않은 영역 때문에 실행할 때마다 값이 달라질 수 있습니다. 엔티티 ID 순으로 위치·속도·체력·애니메이션 상태처럼 의미 있는 필드를 나열하고, 부동소수점은 목적에 맞게 양자화해야 합니다. 예컨대 화면 연출용 파티클 위치는 제외하고 판정에 쓰이는 캐릭터 좌표는 1밀리미터 또는 게임에 적합한 단위로 반올림할 수 있습니다.

상태 지문은 온라인 동기화 문제에도 쓸 수 있습니다. 서버와 클라이언트가 일정 틱마다 같은 규칙으로 해시를 계산하면 언제부터 예측 결과가 갈라졌는지 알 수 있습니다. 단, 해시가 다르다는 사실만으로 어떤 필드가 문제인지는 알 수 없으므로 불일치 구간에서는 구성 요소별 보조 해시를 출력하는 2단계 방식이 좋습니다.

기록 방식장점주의할 점추천 용도
전체 텍스트 로그사람이 바로 읽기 쉽습니다용량과 문자열 비용이 큽니다드문 상태 전환
월드 상태 해시불일치 시점을 빠르게 찾습니다단독으로 원인 필드를 보여주지 않습니다결정론 검증
구성 요소별 해시문제 범위를 좁힐 수 있습니다정렬과 직렬화 규칙이 필요합니다네트워크 동기화
조건부 스냅샷특정 순간을 깊게 조사합니다파일 크기와 호환성을 관리해야 합니다충돌 직전 분석
  • 컨테이너 순회 순서를 고정하고 엔티티를 안정적인 ID 기준으로 정렬합니다.
  • 주소값, 실행 시간, 스레드 ID처럼 매번 달라지는 값은 해시 대상에서 뺍니다.
  • 부동소수점 원본 비트를 쓸지 양자화 값을 쓸지 시스템별로 명시합니다.
  • 월드 전체 해시가 다르면 물리, AI, 인벤토리 등 하위 해시를 비교합니다.
  • 해시 규칙 버전을 리플레이 헤더에 기록해 오래된 파일의 오판을 막습니다.

로그 예산을 정하면 중요한 사건이 살아남는다

한 프레임에 같은 경고가 수천 번 발생하면 첫 원인은 위쪽에 묻히고 저장 공간은 반복 문장으로 가득 찹니다. 시스템별로 초당 기록 가능한 건수와 바이트를 정하고, 같은 사건은 횟수만 집계해 주기적으로 요약하세요. 이는 정보를 버리는 행동이 아니라 신호의 우선순위를 설계하는 작업입니다.

성능과 저장 비용을 기능 단위로 배분한다는 관점은 계획예산 제도의 개념과도 닮았습니다. 실제 게임 프로젝트에서는 네트워크 오류에 높은 예산을 주고, 정상적인 애니메이션 전환에는 낮은 예산을 주는 식으로 적용할 수 있습니다. 출시 빌드는 원격 설정으로 등급을 잠시 올릴 수 있게 만들되, 민감한 데이터가 출력되지 않도록 항목별 허용 목록을 두는 것이 좋습니다.

  • 첫 발생: 상세 문맥과 호출 위치를 남깁니다.
  • 반복 발생: 동일 키로 묶어 횟수와 최초·최종 시각만 갱신합니다.
  • 임계치 초과: 한 번 경고한 뒤 해당 틱의 추가 출력을 억제합니다.
  • 세션 종료: 가장 빈번했던 오류 키와 누적 횟수를 요약합니다.

재현이 안 되면 시간을 거꾸로 좁혀라

체크포인트 사이를 이진 탐색하는 디버깅

20분 뒤에 발생하는 버그를 조사하려고 매번 처음부터 플레이하면 수정 한 번을 검증하는 데 하루가 사라집니다. 일정 간격으로 월드 스냅샷을 만들고 그 사이의 입력을 기록하면 15분 지점에서 상태를 복원해 이후만 재생할 수 있습니다. 더 나아가 정상 상태와 비정상 상태 사이를 절반씩 좁히는 시간축 이진 탐색을 적용할 수 있습니다.

가령 18,000틱에서는 정상이고 19,200틱에서는 인벤토리 수량이 틀렸다고 합시다. 중간인 18,600틱의 상태를 검사하고, 결과에 따라 앞뒤 절반만 다시 확인합니다. 약 11회의 비교면 1,200틱 구간에서 최초 오류 틱을 찾을 수 있습니다. 영상 재생 바를 끌듯 시간을 이동하지만, 실제로는 체크포인트 복원과 결정론적 입력 재생을 조합하는 방식입니다.

스냅샷 간격은 무조건 짧을수록 좋은 것이 아닙니다. 간격이 짧으면 이동은 빠르지만 메모리와 직렬화 비용이 커지고, 너무 길면 복원 후 재생 시간이 늘어납니다. 전투 게임은 5~15초, 턴제나 퍼즐은 턴 경계, 오픈월드는 스트리밍 구역 전환처럼 게임 구조상 안정적인 경계를 먼저 후보로 삼아 보세요.

  1. 오류를 판정할 수 있는 불변 조건을 만듭니다. 예: 아이템 총량은 생성량과 소비량의 차이와 같아야 합니다.
  2. 정상 체크포인트와 오류 체크포인트의 틱 범위를 확보합니다.
  3. 중간 틱까지 복원·재생한 뒤 불변 조건을 검사합니다.
  4. 오류가 있으면 앞 절반, 없으면 뒤 절반을 새 탐색 구간으로 선택합니다.
  5. 최초 오류 틱에서 상태 변경자와 호출 경로만 상세 추적합니다.
실전 요령: 화면이 깨진 순간을 찾지 말고 데이터가 처음 불변 조건을 어긴 순간을 찾으세요. 시각적 증상은 내부 오류보다 수십 프레임 늦게 나타나는 경우가 많습니다.

불변 조건은 숨어 있는 자동 브레이크포인트다

크래시가 아닌 논리 오류는 프로그램이 정상적으로 계속 실행되기 때문에 발견이 늦습니다. 체력은 최대치를 넘지 않는다, 부모가 제거된 객체는 다음 틱까지 자식 목록에 남지 않는다, 투사체 소유자는 유효한 엔티티여야 한다 같은 규칙을 코드로 표현하면 오류 발생 지점에서 자동으로 멈출 수 있습니다.

모든 객체를 매 틱 검사하면 비용이 클 수 있으므로 개발 빌드에서는 전체 검사를, 테스트 빌드에서는 표본 검사를, 출시 빌드에서는 치명적인 규칙만 낮은 빈도로 실행하세요. 검사에 실패했을 때 단순히 assert로 종료하는 대신 최근 입력 버퍼, 관련 엔티티 스냅샷, 상태 지문을 함께 저장하면 다음 실행에서 바로 재생할 수 있습니다.

  • 경제 시스템에서는 통화의 생성·소비 원장을 합산해 잔액과 대조합니다.
  • 물리 시스템에서는 유효하지 않은 수치와 비정상적으로 큰 속도를 즉시 탐지합니다.
  • AI에서는 현재 상태가 허용된 전이 경로를 통해 도달했는지 검사합니다.
  • 오브젝트 수명 관리에서는 제거된 ID가 활성 목록에 다시 등장하지 않는지 확인합니다.
  • 네트워크에서는 확인된 틱보다 오래된 명령이 뒤늦게 상태를 바꾸지 못하게 막습니다.

난수와 부동소수점은 우연이 아니라 기록 대상이다

난수 시드 하나만 저장하면 충분하다는 착각

동일한 난수 시드를 넣었는데도 전투 결과가 달라진다면 호출 횟수나 순서가 변했을 가능성이 큽니다. 파티클 효과가 난수를 한 번 더 소비했거나, AI 업데이트 순서가 달라져 다음 숫자의 주인이 바뀔 수 있습니다. 하나의 전역 난수 생성기를 모든 시스템이 공유하면 시각 효과 수정 하나가 전투 판정까지 바꾸는 황당한 일이 벌어집니다.

해결법은 목적별 난수 스트림을 분리하는 것입니다. 전투, 전리품, AI, 장식 효과에 독립된 시드와 생성기를 배정하고, 각 호출에 태그 또는 카운터를 붙이세요. 그러면 전투 스트림의 328번째 호출이 원본과 재생에서 다른 위치에서 발생했는지 곧바로 알 수 있습니다. 결과값 전부를 저장하기보다 시드와 호출 순서를 검증하는 편이 용량과 분석 효율 사이의 균형이 좋습니다.

프로시저럴 맵처럼 결과 자체가 플레이의 핵심이면 생성 알고리즘 버전도 함께 저장해야 합니다. 시드는 같아도 알고리즘이 바뀌면 지형이 달라지기 때문입니다. 오래된 리플레이를 반드시 지원해야 한다면 구버전 생성기를 보관하거나, 중요한 생성 결과만 별도 데이터로 고정하는 선택지가 있습니다.

  • 게임플레이 스트림: 명중, 피해량, 전리품처럼 규칙에 영향을 주는 난수입니다.
  • AI 스트림: 목표 선택과 행동 지연을 분리해 전투 효과 변경의 영향을 막습니다.
  • 연출 스트림: 파티클과 카메라 흔들기는 재현 대상에서 제외할 수도 있습니다.
  • 호출 태그: 시스템명과 목적을 붙여 예상하지 못한 소비 위치를 찾습니다.
  • 알고리즘 버전: 생성기나 분포 함수가 바뀔 때 리플레이 호환성을 판단합니다.

결정론은 전체가 아니라 필요한 경계부터 만든다

서로 다른 CPU와 운영체제에서 부동소수점 계산을 비트 단위로 완전히 같게 만드는 일은 예상보다 어렵습니다. 컴파일러 최적화, 연산 결합, 초월 함수 구현, 작업 스케줄 순서가 결과를 바꿀 수 있습니다. 그렇다고 모든 게임이 고정소수점 수학으로 전면 교체해야 하는 것은 아닙니다.

판정에 민감한 영역만 결정론 경계로 묶어 보세요. 네트워크 롤백에 필요한 캐릭터 위치와 충돌 결과는 엄격히 관리하되, 천의 흔들림과 먼지 입자는 재생 때 조금 달라도 허용할 수 있습니다. 위치 비교에도 완전 일치 대신 시스템 목적에 맞는 허용 오차를 두고, 임계값 근처에서는 히스테리시스를 적용하면 미세한 흔들림이 상태 전환을 반복시키는 현상을 줄일 수 있습니다.

게임 개발의 기술 공유 문화와 발표 사례를 살피고 싶다면 GDC에 대한 지식백과 설명을 출발점으로 삼을 수 있습니다. 다만 발표에서 소개된 결정론 기법을 그대로 옮기기보다 자신의 엔진, 플랫폼, 네트워크 방식에서 어떤 상태가 정말 동일해야 하는지 먼저 정의해야 합니다.

  • 업데이트 대상의 순서를 안정적인 키로 고정하고 해시 맵의 우연한 순회 순서에 기대지 않습니다.
  • 판정용 수학과 화면 연출용 수학을 분리해 엄격한 규칙의 적용 범위를 줄입니다.
  • 멀티스레드 작업 결과는 명시된 순서로 병합하고 공유 상태에 직접 누적하지 않습니다.
  • 0으로 나누기, NaN, 무한대는 발생한 지점에서 탐지하고 전파되기 전에 캡처합니다.
  • 지원 플랫폼마다 같은 리플레이를 야간 테스트로 실행해 상태 해시를 비교합니다.

엔진 업데이트 뒤에도 살아남는 디버깅 자료를 설계하라

리플레이 포맷에는 수명과 폐기 규칙이 필요하다

리플레이 시스템이 잘 작동하기 시작하면 팀은 오래된 파일도 언제나 재생될 것이라 기대하기 쉽습니다. 하지만 컴포넌트 구조, 물리 엔진, 맵 데이터, 난수 알고리즘이 바뀌면 같은 입력만으로 과거 장면을 되살릴 수 없습니다. 그래서 파일 헤더에 게임 빌드, 콘텐츠 해시, 플랫폼, 포맷 버전, 주요 알고리즘 버전을 넣고 지원 가능한 호환 범위를 명시해야 합니다.

모든 과거 버전을 영구 지원하는 것은 비용이 큽니다. 라이브 서비스라면 현재 정식 버전과 직전 버전의 리플레이만 완전 재생하고, 더 오래된 자료는 이벤트 목록과 핵심 스냅샷을 읽는 분석 모드로 제한할 수 있습니다. 반대로 대회 판정이나 장기 보존이 중요한 프로젝트라면 실행 파일과 콘텐츠 묶음까지 함께 보관하는 편이 낫습니다.

기획 의도와 실제 규칙이 달라 생기는 버그도 많으므로 재생 도구에 사건 타임라인을 제공하면 협업이 쉬워집니다. 예를 들어 4210틱에 퀘스트 단계 변경, 4212틱에 보상 지급, 4213틱에 연결 종료가 있었다는 식입니다. 역할의 일반적 맥락은 기획자에 관한 용어 설명에서도 살펴볼 수 있지만, 프로젝트 안에서는 이벤트 이름과 정상 순서를 팀이 함께 정의해야 실질적인 분석 도구가 됩니다.

  • 리플레이 파일에 빌드 번호뿐 아니라 콘텐츠와 설정의 해시도 포함합니다.
  • 포맷 필드는 번호로 식별하고 새 필드는 선택적으로 읽을 수 있게 설계합니다.
  • 변환 가능한 구버전에는 마이그레이션 도구를 제공하고 결과를 새 파일로 보존합니다.
  • 재생 불가능한 파일도 사건 타임라인과 메타데이터는 열람할 수 있게 합니다.
  • 보존 기간이 끝난 사용자 파일은 개인정보 정책과 운영 규칙에 맞춰 안전하게 폐기합니다.

도구가 바뀌는 순간을 테스트 항목으로 만든다

엔진 패치나 컴파일러 업그레이드 전에는 대표 리플레이 묶음을 기준선으로 실행하세요. 보스전, 대량 물리 객체, 세이브 로드, 네트워크 재접속, 프로시저럴 생성처럼 서로 다른 위험을 담은 짧은 파일 10~20개가 유용합니다. 업데이트 전후의 상태 해시와 성능 시간을 비교하면 화면을 사람이 끝까지 보지 않아도 변화 지점을 찾을 수 있습니다.

중요한 점은 해시 차이를 모두 실패로 취급하지 않는 것입니다. 의도한 밸런스 변경이나 물리 수정이라면 기준선을 승인해 갱신하고, 예상하지 못한 시스템까지 함께 달라졌는지 확인합니다. 승인 기록에 변경 이유와 담당자를 남기면 몇 달 뒤 결과가 달라진 이유를 추측하느라 시간을 쓰지 않아도 됩니다.

이 방식의 비용은 상용 도구 가격보다 저장량과 유지 시간에서 발생합니다. 초기에는 개발자 한 명이 반나절 안에 검토할 수 있는 작은 대표 세트로 시작하고, 실제 장애를 잡아낸 리플레이만 회귀 묶음에 추가하세요. 엔진 버전, 플랫폼 SDK, 컴파일러 최적화, 운영체제 스케줄러는 시간이 지나며 달라질 수 있으므로 업데이트 직후 대표 리플레이의 최초 불일치 틱을 확인하는 절차 자체를 계속 갱신해야 합니다.

  1. 각 리플레이가 보호하는 기능과 예상 최종 상태를 문서화합니다.
  2. 업데이트 전 기준 빌드에서 해시, 실행 시간, 메모리 사용량을 저장합니다.
  3. 업데이트 후보 빌드로 동일 파일을 같은 설정에서 재생합니다.
  4. 불일치가 생기면 월드 해시에서 시스템별 해시, 개별 엔티티 순으로 범위를 좁힙니다.
  5. 의도된 변화만 승인하고 이유를 기록한 뒤 새로운 기준선을 발행합니다.
  6. SDK나 엔진의 재현성 보장이 바뀌면 저장 필드와 허용 오차도 다시 검토합니다.

로그를 더 남길수록 놓친다? 게임 프로그래밍 디버깅의 역설

댓글목록

등록된 댓글이 없습니다.