크래시 직전 상태를 남기는 법

스택트레이스는 어디서 죽었는지만 말합니다. logMessageReceived 훅, quitting 이벤트의 한계, 워치독 프로세스, 주기적 스냅샷까지 — 왜 그 상태였는지를 남기는 네 가지 방법과 각각의 대가를 정리합니다.

  • unity
  • crash
  • logging

크래시 로그를 확보하고 크래시 핸들러가 무엇을 잡는지도 알았다고 합시다. 여전히 못 얻는 정보가 있습니다. 스택트레이스는 어디서 죽었는지 말합니다. 왜 그 상태에 이르렀는지는 말하지 않습니다.

스택트레이스가 말하지 않는 것

NullReferenceException이 몇 번째 줄에서 났는지는 나옵니다. 그 레퍼런스가 왜 null이었는지, 직전에 어떤 씬 전환이 있었는지, 인벤토리가 몇 개였는지는 스택트레이스 어디에도 없습니다. 크래시 리포트는 사건의 위치를 주지 사건의 경위를 주지 않습니다. 경위를 재구성하려면 크래시 이전 구간의 정보가 따로 필요합니다.

logMessageReceived — 가장 손쉬운 시작점

가장 먼저 떠오르는 방법은 로그 콜백을 후킹해 최근 로그를 링 버퍼에 쌓아두는 겁니다. Unity의 Application.logMessageReceived는 로그 메시지를 받았을 때 메인 스레드에서만 호출되는 이벤트입니다. 따라서 Unity API를 건드려야 하는 작은 버퍼 갱신에는 편하지만, 네이티브 크래시 뒤에도 호출되거나 디스크 기록이 끝날 때까지 기다려 준다는 보장은 아닙니다.

private bool snapshotRequested;
private float nextWriteAt;

void OnEnable()
{
    Application.logMessageReceived += HandleLog;
}

void OnDisable()
{
    Application.logMessageReceived -= HandleLog;
}

void HandleLog(string condition, string stackTrace, LogType type)
{
    ringBuffer.Push($"[{type}] {condition}");
    snapshotRequested = true;
}

void LateUpdate()
{
    if (!snapshotRequested || Time.unscaledTime < nextWriteAt) return;

    snapshotRequested = false;
    nextWriteAt = Time.unscaledTime + 1f;
    ScheduleSnapshotWrite(ringBuffer.Copy()); // Unity 객체를 참조하지 않는 복사본만 전달
}

위 코드는 개념 예시입니다. ScheduleSnapshotWrite는 Unity API가 아니라, Unity 객체를 포함하지 않는 제한된 크기의 복사본을 안전하게 기록하는 처리의 자리표시자입니다. 로그 콜백에서 곧바로 FlushToFile을 호출하면 파일 I/O가 메인 스레드를 막고, 기록 중 발생한 로그가 콜백을 다시 부르는 재진입 문제도 만들 수 있습니다. 특히 메모리에만 있는 링 버퍼는 프로세스와 함께 사라집니다. 하드 크래시 뒤에도 남길 후보를 만들려면 오류·예외가 난 때만이 아니라, 콜백 밖에서 짧은 주기로 복사본을 미리 기록해야 합니다. 실제 구현에서는 버퍼 크기를 제한하고, 실패를 다시 Debug.Log로 무한히 내보내지 않도록 해야 합니다. 여기서 중요한 한계는 그대로입니다 — 로그로 남긴 것만 재구성됩니다. 로그를 찍지 않은 구간의 상태 변화는 애초에 없는 정보입니다.

quitting과 OnApplicationPause의 함정

"종료 직전 상태를 저장하자"는 발상으로 Application.quitting이나 OnApplicationPause에 스냅샷 저장 로직을 넣는 경우가 있습니다. Application.quitting은 Player가 종료할 때 알리지만, Unity 문서도 강제 종료나 크래시에서는 발생하지 않는다고 명시합니다. OnApplicationPause는 일시정지 상태 변화를 받는 콜백이지 크래시 감지 훅이 아닙니다. 둘은 정상 종료·일시정지 때 마지막 상태를 더 남기는 보조 수단일 수는 있어도, 네이티브 크래시나 OS 종료에서 저장 완료를 보장하지 않습니다.

워치독 — 프로세스 밖에서 지켜봐야 하는 이유

하드 크래시 뒤의 징후를 보려면 죽는 프로세스 밖의 관찰자도 한 선택지입니다. 이는 Unity Crash Handler가 남기는 크래시 자료와 별개로 팀의 게임 상태 스냅샷을 보존하려는 경우입니다. 직접 구현한다면 별도 프로세스가 게임 프로세스의 생존을 확인하고, 이미 안전하게 기록된 마지막 스냅샷을 크래시 리포트와 함께 보존하는 구조가 됩니다. 다만 워치독은 플랫폼 정책·권한·일시 멈춤에 따른 오탐을 설계해야 하고, 죽은 프로세스 안의 새 상태를 되살리지는 못합니다. 플랫폼의 크래시 리포팅이나 외부 모니터링을 포함해 프로젝트 환경에 맞는 조합을 검증해야 합니다.

주기적 스냅샷 — 비용과 타협

일정 주기로 게임 상태(Scene 이름, 최근 로그, 주요 변수)를 파일에 기록하는 방법도 있습니다. 게임 상태 스냅샷에서 다룬 것과 같은 대상을 저장하되, 목적이 "지금 상태 진단"이 아니라 "죽기 직전 상태 복원"이라는 점이 다릅니다. 주기를 짧게 잡을수록 크래시 시점에 가까운 데이터를 얻지만 디스크 쓰기 빈도가 늘고, 길게 잡으면 반대입니다. 파일 하나를 덮어쓰는 방식은 저장 공간을 제한할 수 있지만, 쓰기 도중 종료되면 파일이 잘리거나 깨질 수 있습니다. 임시 파일에 쓴 뒤 원자적 교체가 가능한 파일 시스템에서는 교체하고, 직전의 정상 스냅샷도 남겨 두는 편이 안전합니다. 그래도 확보되는 것은 마지막으로 성공적으로 기록된 상태까지입니다.

왜 "순간"이 아니라 "구간"인가

네 방법을 관통하는 결론은 하나입니다. 크래시 순간의 스냅샷 하나로는 부족합니다. 롤링 버퍼 접근에서 다룬 것과 같은 이유입니다 — 원인은 대개 크래시 시점이 아니라 그 이전 어딘가에 있고, 필요한 건 t-0 시점의 사진이 아니라 t-30초 구간의 필름입니다. 로그 링 버퍼든 주기적 스냅샷이든, 결국 직전 구간을 크래시 전에 짧은 주기로 영속화하는 패턴으로 수렴합니다. 사람이 이상을 목격한 경우에는 핫키로 그 구간을 확정할 수 있지만, 하드 크래시 자체가 그 확정을 대신해 주지는 않습니다.

정리

방법잡는 범위진짜 크래시에서
logMessageReceived 링 버퍼로그로 남긴 것사전 영속화된 크래시 전 기록분만 남음 (플러시 보장 없음)
quitting / OnApplicationPause정상 종료·일시정지크래시 감지·저장 완료를 보장하지 않음
워치독 프로세스프로세스 생존 여부와 기존 파일플랫폼·권한·오탐을 검증해야 함
주기적 스냅샷 파일마지막 성공 기록 시점까지원자적 교체·직전 파일 보존을 고려

넷 중 무엇을 택하든 원칙은 같습니다. 크래시가 난 다음에 뭘 남길지 고민하면 늦습니다. 크래시가 나기 전부터 뭔가는 계속 쌓이고 있어야 합니다.

저희 Rekon은 Play Mode에서 최근 약 60초의 영상·로그·성능 데이터를 롤링으로 유지하고, QA가 이상을 목격한 뒤 핫키를 누르면 그 구간을 캡처합니다. Scene 같은 게임 상태는 그 핫키 캡처 시점에 함께 붙습니다. 이는 관찰된 이상을 재현 가능한 증거로 남기는 흐름이며, 하드 크래시 때 자동 저장하거나 자동 제출하는 기능은 아닙니다. 하드 크래시까지 다룰 필요가 있다면 이 글의 방법과 프로젝트의 플랫폼·크래시 빈도를 함께 검증해 조합을 고르시길 권합니다.