Unity 로그 시스템 깊게 파기 ③ — IL2CPP·release 빌드에서 달라지는 것들
에디터에서 완벽하던 로그 수집이 빌드에서 다르게 동작하는 이유. 스택트레이스 설정, IL2CPP 심볼과 스트리핑, dev build 전용 API, Debug.Log 의 비용까지.
- unity
- logging
- il2cpp
Unity 로그 시스템 3부작 — ① 콜백의 함정 · ② GC 없는 링 버퍼 · ③ 빌드에서 달라지는 것
1편과 2편의 코드는 에디터에서 완벽하게 돕니다. 문제는 로그 수집이 진짜 필요한 곳이 에디터가 아니라 QA 머신의 빌드라는 점입니다. 빌드 — 특히 IL2CPP release — 는 로그의 모양과 비용을 바꿉니다. 에디터에서 검증한 것과 QA가 받는 것이 다르면, 수집기는 가장 필요한 순간에 반쪽이 됩니다.
1. 스택트레이스는 설정의 함수다
로그 타입별 스택트레이스 수집 여부는 빌드 설정(Player Settings → Stack Trace)이자 런타임 API입니다.
// 타입별로 다르게 — 예외는 풀 스택, 일반 로그는 스택 없음(비용 절약)
Application.SetStackTraceLogType(LogType.Exception, StackTraceLogType.ScriptOnly);
Application.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
이 설정을 모르면 두 방향으로 당합니다. None으로 되어 있으면 콜백의 stackTrace 인자가 빈 문자열로 들어와 "수집기가 고장났다"는 오진을 하게 되고, 반대로 일반 로그까지 ScriptOnly면 로그가 잦은 프로젝트에서 스택 캡처 비용이 눈에 띄게 쌓입니다. 수집기를 배포하기 전에 이 설정을 명시적으로 고정해 두는 것이 안전합니다.
2. IL2CPP — 스택은 나오지만 줄 번호가 없다
IL2CPP 빌드의 스택트레이스는 Mono와 다릅니다. release 빌드에서는 함수 이름까지는 나와도 파일·줄 번호가 없는 형태가 일반적이고, 인라이닝된 함수는 프레임 자체가 사라지기도 합니다.
- 줄 번호가 필요하면: development build + script debugging, 또는 빌드 산출물의 심볼 파일로 사후 해석
- 심볼 파일(
.sym/ line mapping)은 빌드마다 다릅니다. 크래시·로그를 사후 해석하려면 빌드 아티팩트와 심볼을 같은 버전으로 보관하는 규율이 필요합니다 — "빌드 번호를 리포트에 남겨야 하는" 또 하나의 이유입니다
3. 스트리핑 — 리플렉션이 조용히 사라진다
IL2CPP의 managed code stripping은 "참조되지 않는" 코드를 제거합니다. 문제는 리플렉션으로만 참조되는 코드를 정적 분석이 볼 수 없다는 점입니다. 로그 수집기가 리플렉션으로 게임 상태를 긁는 구조라면(필드 덤프, 직렬화 라이브러리), 에디터에서 되던 것이 빌드에서만 조용히 빈 값이 됩니다.
대응은 셋 중 하나입니다 — link.xml로 보존 선언, [Preserve] 어트리뷰트, 또는 애초에 리플렉션 없는 명시적 수집 구조. 저희는 마지막을 권합니다. 스트리핑 수준(Minimal/Low/Medium/High)은 프로젝트 설정에 따라 바뀌므로, 보존 선언에 의존하면 설정 변경 한 번에 회귀합니다.
4. development build 전용 API — 0 이 정상 동작인 세계
Profiler.GetTotalAllocatedMemoryLong 같은 Profiler 계열 API는 release 빌드에서 0을 반환합니다. 예외가 아니라 0입니다. 수집기가 이 값을 그대로 리포트에 넣으면 "메모리 0MB"라는 데이터가 만들어지고, 이걸 믿은 사람이 잘못된 결론을 냅니다.
원칙은 계측 편과 같습니다 — 미지원 환경의 0과 실제 0을 구분해야 합니다. Debug.isDebugBuild로 분기해 release에서는 해당 필드를 빼거나 "n/a"로 명시하는 쪽이, 0을 싣는 것보다 낫습니다.
5. Debug.Log 자체의 비용 — 마지막 반전
시리즈 내내 로그를 "공짜로 주어지는 데이터"처럼 다뤘지만, Debug.Log 호출 자체가 싸다고는 할 수 없습니다. 문자열 조립, 스택 캡처(설정에 따라), 콘솔·파일 출력까지 — 로그가 잦은 핫패스에서는 로그가 곧 성능 문제가 됩니다.
release 빌드에서 개발용 로그를 없애는 표준 수단은 조건부 컴파일입니다.
using System.Diagnostics; // UnityEngine.Debug 와 혼동 주의
public static class Log
{
[Conditional("DEVELOPMENT_BUILD"), Conditional("UNITY_EDITOR")]
public static void Dev(string message) => UnityEngine.Debug.Log(message);
// release 빌드에서는 호출 자체가 컴파일에서 제거된다 — 인자 평가 비용까지 사라진다
}
단, 전부 지우면 시리즈 1편의 문제로 되돌아갑니다 — 남는 게 없어서 재현을 못 합니다. 기준은 "핫패스의 개발용 로그는 제거, 사건성 로그(예외·경고·상태 전환)는 유지"입니다. 링 버퍼는 용량이 고정이라 사건성 로그를 남겨도 메모리가 늘지 않습니다.
정리 — 최종 검증은 빌드에서
세 편의 결론을 한 줄로 줄이면 이렇습니다. 로그 수집기의 단위 테스트는 에디터에서 돌지만, 최종 검증은 반드시 타깃 빌드에서 해야 합니다. 스택트레이스 설정, IL2CPP 심볼, 스트리핑, dev 전용 API — 넷 다 에디터에서는 보이지 않는 차이입니다.
저희 Rekon SDK가 이 함정들을 하나씩 밟으며 정리한 목록이기도 합니다. 직접 구현하신다면 이 시리즈가 그 시행착오를 줄이는 지도가 되기를 바랍니다.