Unity 로그 시스템 깊게 파기 ① — 로그 콜백의 함정 4가지
logMessageReceived와 Threaded의 정확한 차이, 콜백 안에서 터지는 UnityException, 재귀 로깅, 구독 해제 누락까지 — 로그 후킹에서 실제로 밟는 함정들.
- unity
- logging
- csharp
Unity 로그 시스템 3부작 — ① 콜백의 함정 · ② GC 없는 링 버퍼 · ③ 빌드에서 달라지는 것 (②·③ 순차 공개 예정)
버그 캡처 최소 구현에서 로그 콜백 후킹을 한 문단으로 다뤘습니다. 이 시리즈는 그 한 문단을 제대로 폅니다 — 저희가 SDK의 로그 수집기를 만들며 실제로 밟았던 함정들이 소재입니다.
함정 1. logMessageReceived vs logMessageReceivedThreaded — 겹침의 문제
두 이벤트의 차이는 "메인 스레드 로그만 오느냐, 전부 오느냐"로 알려져 있지만, 정확한 동작은 이렇습니다.
logMessageReceived: 메인 스레드에서 발생한 로그만, 메인 스레드에서 호출logMessageReceivedThreaded: 모든 스레드의 로그를, 발생한 그 스레드에서 호출
중요한 건 Threaded 가 메인 스레드 로그도 포함한다는 점입니다. 둘을 동시에 구독하면 메인 스레드 로그가 두 번 들어옵니다. 워커 스레드 예외까지 잡을 거라면 Threaded 하나만 구독해야 합니다.
함정 2. 콜백 안의 Unity API — 잡으려던 예외가 새 예외를 만든다
Threaded 콜백은 로그가 발생한 스레드에서 실행됩니다. 그런데 Time.realtimeSinceStartup 을 비롯한 다수의 Unity API 는 메인 스레드 전용이라, 워커 스레드에서 호출하면 UnityException이 납니다.
악질적인 건 실패 조건입니다. "워커 스레드 예외를 잡겠다"고 Threaded 를 선택한 바로 그 상황에서만 터집니다. 개발 중 메인 스레드 로그로는 멀쩡히 돌다가, 배포 후 백그라운드 스레드가 처음 예외를 던지는 순간 로그 수집기 자신이 죽습니다.
대응은 두 가지를 조합합니다 — 메인 스레드에서 갱신해 둔 값으로 폴백하고, 콜백 전체를 방어합니다.
void OnLog(string condition, string stackTrace, LogType type)
{
double time;
try { time = Time.realtimeSinceStartupAsDouble; _lastMainThreadTime = time; }
catch { time = _lastMainThreadTime; } // 워커 스레드 — 마지막 값으로 대체
// ... 버퍼 기록 (lock 필수)
}
함정 3. 콜백 안에서 로그를 찍으면 — 재귀
콜백 안에서 Debug.Log(또는 Debug.LogWarning 등)를 호출하면 그 로그가 다시 콜백을 부릅니다. 조건 없이 찍으면 무한 재귀, 조건부라도 경계 상황에서 폭주할 수 있습니다.
이게 위험한 이유는 에러 처리 코드가 가장 유혹적인 위반 지점이기 때문입니다. "버퍼 기록에 실패하면 경고라도 남기자"는 자연스러운 코드가 정확히 이 함정입니다. 콜백 내부의 실패는 로그가 아닌 다른 채널(카운터 증가, 플래그)로 남기고, 메인 루프에서 보고해야 합니다.
함정 4. 구독 해제 누락 — 에디터에서만 겪는 유령
OnDisable에서 -=를 빠뜨리면 빌드에서는 그냥 누수지만, 에디터에서는 더 이상한 증상이 됩니다. Enter Play Mode Settings 에서 domain reload 를 끈 프로젝트라면 static 이벤트 구독이 플레이 세션을 넘어 살아남아, 파괴된 오브젝트를 참조하는 콜백이 다음 플레이에서 호출됩니다. "두 번째 플레이부터 이상한 예외가 난다"는 미스터리의 단골 원인입니다.
구독·해제를 OnEnable/OnDisable 쌍으로 강제하고, 콜백 첫 줄에서 자기 상태 유효성을 확인하는 것이 안전합니다.
정리 — 콜백은 국경이다
네 함정의 공통점은, 로그 콜백이 평범한 C# 이벤트처럼 보이지만 실제로는 스레드 경계이자 생명주기 경계라는 점입니다. 국경에서 하는 일은 최소화하는 것이 원칙입니다 — 시각과 데이터를 챙겨 버퍼에 넣는 것까지만 하고, 판단과 가공은 메인 루프로 넘깁니다.
그 "버퍼에 넣는" 부분이 다음 문제입니다. 로그 수집기가 매 프레임 GC 할당을 만들면 성능 버그의 원인이 수집기 자신이 됩니다. 다음 편에서 할당 없는 링 버퍼 설계를 다룹니다.