Windows 앱이 프로그램 실수에 의한 예외로 떨어져도 확실히 로그를 남기려면 - in-process에 걸지 않는 설계와 WER / 최종 로그 / 감시 프로세스의 베스트 프랙티스
· 小村 豪 · 예외 처리, 로그, WER, 크래시 덤프, 불구 조사
Windows 앱의 불구 조사에서 가장 힘든 것은 떨어진 것만은 알 수 있는데, 왜 떨어졌는지가 남아 있지 않은 상태입니다.
특히 다음과 같은 안건에서는 이 문제가 꽤 무거워집니다.
- 고객 환경에서만 떨어진다
- 장시간 운전 끝에만 떨어진다
- WPF / WinForms / Windows 서비스 / 상주 앱에서 재현률이 낮다
- COM, P/Invoke, native DLL, vendor SDK가 얽힌다
- 「예외 메시지만」은 얻어지지만 직전의 문맥이 없다
다만, 처음에 정직하게 말하면 떨어지는 측의 프로세스만으로 「반드시」 로그를 남길 수는 없습니다. 스택 파손, 메모리 파괴, fast fail, 강제 종료, 전원 단절까지 포함하면, in-process의 마지막 로그는 본질적으로 best effort입니다.
실무에서 목표로 해야 할 것은 떨어지는 프로세스 속에만 기대지 않는 구성으로 하는 것입니다. 즉,
- 통상시의 시계열 로그
- 떨어지는 순간의 최종 크래시 마커
- OS 또는 별도 프로세스 측에서 남기는 크래시 증적
의 3층으로 생각합니다.
이 글에서는 Windows 데스크톱 앱, 상주 앱, Windows 서비스, 장치 연계 도구를 전제로, 프로그램 실수에 의한 예외로 떨어져도 조사 가능성을 잃지 않기 위한 베스트 프랙티스를 정리합니다.
1. 먼저 결론
먼저 결론만 나열합니다.
- 「최후의 로그」를 1개의 in-process 핸들러에 걸지 않는 것이 가장 중요합니다.
- 실무에서 가장 무난한 것은 통상 로그 + 최종 크래시 마커 + WER LocalDumps의 조합입니다.
- 장시간 운전, 장치 연계, 플러그인, native SDK 혼재라면, 감시 프로세스(watchdog / launcher / service)를 추가하면 꽤 강해집니다.
- 크래시 핸들러에서는 무거운 처리를 하지 않는 것이 철칙입니다. 압축, HTTP 송신, DI 해결, UI 다이얼로그, 복잡한 JSON 생성은 뺍니다.
- 크래시 시는 로컬에 짧게 남기기만 하고, 압축・업로드・알림은 다음 회 기동 후나 별도 프로세스로 돌립니다.
- WinForms의
ThreadException이나 WPF의DispatcherUnhandledException을 사용해 외견상 연명하는 설계는, 프로그램 실수 상대로는 위험합니다. - .NET에서도 native에서도 파손 상태를 의심하는 예외는 「회복」보다 「기록해서 종료」를 기본으로 하는 편이 안전합니다.
- 덤프를 취한다면 PDB와 배포 바이너리의 보관을 동시에 하지 않으면 나중에 읽을 수 없습니다.
요컨대 베스트 프랙티스는 「떨어지는 순간에 전부 하려고 하지 않는다. 떨어지기 전・떨어지는 순간・떨어진 후로 역할 분담한다」 입니다.
2. 왜 in-process만으로는 「확실」히 할 수 없는가
여기를 애매하게 하면 설계가 흔들립니다.
2.1 떨어진 스레드의 문맥 그 자체가 망가져 있는 경우가 있다
미처리 예외의 훅이나 최상위 예외 필터는 망가진 측의 스레드 문맥에서 동작하는 경우가 있습니다. 이 시점에서,
- 스택이 이미 위험
- 힙 파괴로 추가 확보가 위험
- 예외 발생 시에 취하고 있던 락 때문에 대기하면 멈춘다
- logger 자체가 의존하고 있는 오브젝트가 이미 망가져 있다
는 것이 평범하게 있습니다.
그래서 최후의 핸들러는 「뭐든 할 수 있는 장소」가 아니라 「할 수 있는 것이 꽤 적은 장소」로 보는 편이 안전합니다.
2.2 fast fail나 파손 상태 예외는 「최소한의 in-process 동작」 전제
메모리 파괴나 치명적인 상태에서는 통상의 예외 처리에 기대지 않는 편이 좋습니다.
특히 native 측의 __fastfail 계나 파손 상태를 의심하는 이상은 「가능한 한 적은 오버헤드로 즉시 종료한다」 방향으로 설계되어 있습니다.
즉, 최후의 in-process 로그는 쓸 수 있으면 운이 좋은, 주 증적은 OS / 별도 프로세스 측이라는 사고방식이 자연스럽습니다.
2.3 .NET의 미처리 예외 이벤트도 「무거운 회복 처리」의 장이 아니다
.NET의 AppDomain.UnhandledException은 편리하지만,
여기서 해도 좋은 것은 짧은 기록까지라고 생각하는 편이 좋습니다.
- 예외 발생 시에 보유하고 있던 락의 영향을 받을 수 있다
- 파손 상태 예외까지 뭐든 안전하게 취할 수 있는 것은 아니다
- 여기서 지속 방침을 무리하게 만들면 반쯤 망가진 상태로 연명하기 쉽다
요컨대 「미처리 예외 이벤트 = 최후의 통지」이며 「안전한 회복 지점」은 아닙니다.
3. 추천 아키텍처 - crash-time과 after-restart를 나눈다
가장 정리하기 쉬운 것은 크래시 시에 하는 것과 재기동 후에 하는 것을 나누는 방법입니다.
| 페이즈 | 목적 | 어디서 동작시키는가 | 하는 일 |
|---|---|---|---|
| 통상시 | 시계열을 남긴다 | 앱 내 | 구조화 로그, heartbeat, 경계 이벤트 |
| 크래시 시 | 최저한의 증적을 떨어뜨린다 | 앱 내 + OS | 최종 크래시 마커, WER 덤프 |
| 종료 직후 | unexpected exit를 검지한다 | 별도 프로세스 | exit code 기록, 재기동 판단, 알림 |
| 다음 회 기동 후 | 무거운 후처리를 한다 | 새로운 건전한 프로세스 | 압축, 업로드, 사용자 알림, 오래된 로그 정리 |
이 분할 방식으로 하면 설계가 꽤 안정됩니다.
3.1 최소 구성
작은 업무 도구나 사내용 WPF / WinForms라면 우선은 다음으로 충분한 경우가 많습니다.
- 통상 로그: 로컬의 append-only 파일
- 최종 크래시 마커: 전용의 짧은 파일
- 덤프: WER LocalDumps
- 다음 회 기동 시: 「전회 이상 종료했습니다. 진단 정보가 있습니다」를 낸다
3.2 강한 구성
다음과 같은 요건이라면 1 단계 강화하는 편이 좋습니다.
- 24/7 운전
- 장치 제어, 감시, 상주
- COM / P/Invoke / native SDK가 많다
- 자식 프로세스, 플러그인, 스크립트 실행이 있다
- 고객 환경에서 「멈춘 채로」가 허용되지 않는다
이 경우는,
- worker 프로세스: 본체 처리
- launcher / watchdog / service: 기동 감시, exit 기록, 재기동
- WER LocalDumps: worker 측
- 다음 회 기동 또는 watchdog: 진단 정보 회수
로 나누면 꽤 실무 쪽이 됩니다.
4. 통상 로그의 베스트 프랙티스
크래시 시의 마지막 1행만으로 싸우려고 하면 대개 집니다. 정말로 효과적인 것은 직전까지의 통상 로그입니다.
4.1 로그는 「인간용 문장」보다 「나중에 상관시킬 수 있는 정보」
통상 로그에는 최소한 다음을 넣습니다.
- UTC 타임스탬프
- 프로세스 개시부터의 경과 시간
- PID / TID
- 앱 이름, 버전, 빌드 번호, 커밋 식별자
- 세션 ID
- 조작 ID / 작업 ID / 상관 ID
- 모듈 이름 / 화면 이름 / 워커 이름
- 직전의 외부 작용
- 파일 쓰기
- DB 갱신
- 장치 커맨드 송신
- 통신 요구
- 예외형, HRESULT / Win32 에러 / 예외 코드
- 주요 입력 파라미터의 요약
- 기밀을 포함하지 않는 범위에서의 대상 ID
추천은 1행 1이벤트의 JSON Lines이나 key=value 형식입니다.
인간용으로 장문을 남기기보다 「나중에 3 파일을 맞춰 볼 수 있다」는 것이 더 중요합니다.
4.2 크리티컬 이벤트는 동기적으로 남긴다
통상 로그를 전부 동기 쓰기로 하면 무거워집니다. 다만, 전부를 비동기 버퍼에 맡기면 떨어진 순간에 한꺼번에 사라집니다.
그래서 실무에서는 다음 분할이 다루기 쉽습니다.
Information의 세세한 이벤트: 버퍼해도 좋다Warning이상: 일찍 flush- 중요한 경계 이벤트: 동기적으로 남긴다
- ProcessStart
- ConfigLoaded
- WorkerStarted
- ExternalCommandSent
- TransactionCommitted
- RecoveryStarted
- FatalPathEntered
요점은 업무상의 경계만큼은 제대로 지면에 떨어뜨린다는 것입니다.
4.3 「지금 쓰고 있는 통상 로그」와 「최후의 크래시 마커」는 나눈다
이것은 꽤 중요합니다.
1개의 rolling log에만 전부 넣으려고 하면,
- 로테이션 중이었다
- 비동기 큐에 남아 있었다
- 예외 발생 직후에 logger 자체가 죽었다
- 로그 행의 도중에 끊겼다
는 일이 일어납니다.
그래서 최소한 다음 2개로 나누는 것이 추천입니다.
app-<session>.jsonl통상 시계열 로그fatal-last.log또는fatal-<session>.log최종 크래시 마커 전용
「마지막 1행을 어디에 남길지」가 명확하기만 해도 현장에서 꽤 도움이 됩니다.
4.4 로그 저장처는 로컬 고정, 네트워크처는 사용하지 않는다
크래시 시에 UNC 패스, NAS, HTTP, 클라우드 API에 의존하는 것은 위험합니다.
- 네트워크 순간 끊김
- DNS 지연
- 자격 정보 실효
- UI 스레드에서의 대기
- 서비스 계정 권한 부족
이 얽히기 때문입니다.
크래시 시는 먼저 로컬 고정 패스에 떨어뜨립니다. 보내는 것은 다음 회 기동 후나 별도 프로세스입니다.
4.5 파일 이름에는 session을 넣는다
날짜만으로는 부족합니다. 같은 날에 몇 번이나 재기동하기 때문입니다.
추천은 예를 들어 다음입니다.
Logs\
MyApp_20260318_101530_pid1234_session-4f1c.jsonl
MyApp_fatal_20260318_101533_pid1234_session-4f1c.log
MyApp_watchdog_20260318.jsonl
「어느 기동 인스턴스의 이야기인가」가 명확한 것만으로 해석의 속도가 꽤 변합니다.
5. 최종 크래시 마커의 베스트 프랙티스
여기는 풀 기능 logger를 만드는 장소가 아닙니다. 1회만, 짧게, 확실 쪽으로 남기는 장소입니다.
5.1 목적은 「원인의 상세」가 아니라 「입구의 고정」
최종 크래시 마커에 넣어야 할 정보는 좁히는 편이 강합니다.
- 발생 UTC
- PID / TID
- 세션 ID
- 버전 / 빌드 번호
- 어느 훅에서 왔는가
AppDomain.UnhandledExceptionApplication.ThreadExceptionDispatcherUnhandledExceptionSetUnhandledExceptionFilter_set_invalid_parameter_handlerset_terminate
- 예외형 또는 예외 코드
- 가능하면 간단한 메시지
- 직전의 조작 ID
- 통상 로그의 파일 이름
- dump 상정 폴더
이것만으로 충분합니다.
5.2 크래시 핸들러에서 해서는 안 되는 것
다음은 꽤 높은 확률로 지뢰입니다.
- DI 컨테이너에서 logger를 해결한다
- async / await를 사용한다
- Task를 던진다
- 락 대기를 한다
- 복잡한 JSON을 조립한다
- COM 오브젝트를 만진다
- UI 다이얼로그를 낸다
- 압축한다
- HTTP / SMTP / Slack / Teams 송신
- dump를 해석해서 요약한다
- 예외를 쥐어 뭉개고 지속한다
크래시 핸들러는 보통의 처리 플로우의 연속이 아닙니다. 「최소한의 로컬 쓰기만 하고 끝난다」로 기울입니다.
5.3 크래시 핸들러에서 하는 것
반대로 하는 것은 꽤 단순합니다.
- 다중 진입을 방지한다
- 1행만 쓴다
- flush한다
- 종료한다
이 순서입니다.
가능하면,
- 사전에 만들어 둔 전용 폴더
- 사전에 존재 확인이 끝난 패스
- ACL을 확인이 끝난 저장처
를 사용합니다.
통상 로그에서는 flush를 너무 하면 무겁지만, fatal 마커는 건수가 극소이므로 여기만은 강하게 flush해도 좋습니다.
.NET이라면 FileStream.Flush(true), native라면 FlushFileBuffers처럼 「이 1행만은 지금 즉시 지면에 떨어뜨린다」 취급으로 기울이면 설계하기 쉬워집니다.
5.4 지속시키려고 하지 않는다
프로그램 실수 기점의 unexpected한 예외라면, 최종 핸들러는 회복 장치가 아니라 기록 장치라고 생각하는 편이 안전합니다.
특히 다음은 「지속하지 않는다」가 기본입니다.
NullReferenceException이나InvalidOperationException이라도 공유 상태 갱신의 도중이었다- UI 스레드에서의 unexpected한 예외
- 감시 루프나 부모 루프에서 샌 unexpected 예외
AccessViolationExceptionStackOverflowException- native 경계의 이상
- CRT의 invalid parameter / purecall / terminate
「떨어뜨리고 싶지 않다」는 마음은 알지만, 반쯤 망가져 살아남는 편이 진단도 운용도 힘든 경우가 많습니다.
종료시킬 때는, .NET이라면 Environment.FailFast, native라면 RaiseFailFastException이나 __fastfail과 같은 즉시 종료계 API를 검토하고, finally나 통상의 후처리에 기대지 않는 설계 편이 안전합니다.
6. 프레임워크별의 주의점
6.1 .NET 공통: AppDomain.CurrentDomain.UnhandledException
이것은 최후의 통지로서 유용합니다. 다만, 여기서의 무거운 회복 처리는 피합니다.
사용법의 기본은 다음입니다.
- 최종 크래시 마커를 쓴다
- 필요하면 Windows Event Log에 최소 메시지를 남긴다
- 지속하지 않는다
- 여기서 대기나 재시행을 하지 않는다
UnhandledException은 편리하지만, 여기서 앱을 건강한 상태로 돌릴 수 있다는 전제는 하지 않는 편이 안전합니다.
6.2 WinForms: Application.ThreadException
이것은 UI 스레드의 미처리 예외를 주워 외견상 지속할 수 있는 것이 어려운 점입니다.
업무 입력의 상정 내 에러를 다이얼로그화하는 용도라면 모를까, 프로그램 실수 기점의 unexpected 예외로 지속하는 용도에는 어울리지 않습니다.
정말로 원인 조사를 우선한다면,
ThreadException에서 최소 기록만 한다- 또는
UnhandledExceptionMode.ThrowException에 기울인다 - 그 위에서 프로세스를 종료시키고 덤프와 로그를 남긴다
쪽이 안전합니다.
6.3 WPF: Application.DispatcherUnhandledException
WPF에서도 비슷합니다.
- UI 스레드 위의 예외만이 주 대상
Handled = true로 하면 외견상 지속할 수 있다- 하지만 프로그램 실수 상대로 그것을 하면 화면 상태와 내부 상태가 어긋나기 쉽다
그래서 WPF에서도 지속을 위한 연명 장치로 사용하지 않고, 기록의 입구로 사용하는 편이 무난합니다.
6.4 TaskScheduler.UnobservedTaskException은 주 경로로 하지 않는다
이것은 「떨어지기 직전의 최후의 보루」가 아닙니다.
Task의 예외 놓침을 검지하는 보조에는 사용할 수 있지만,
크래시 시의 확실한 기록 경로로서는 약합니다.
그래서,
- 예외의 관측 누락을 조기에 발견한다
- 개발 중에
Task의 설계 누락을 불거내게 한다
용도에는 사용해도 최종 크래시 핸들러의 주역으로는 하지 않는 편이 좋습니다.
6.5 native Win32 / C++: SetUnhandledExceptionFilter를 과신하지 않는다
native 측에서는 무심코 SetUnhandledExceptionFilter에 기대고 싶어집니다.
다만, 이것은 faulting thread의 문맥에서 동작하므로,
- 무효 스택
- 깊은 재귀
- 이미 망가진 힙
- 예외 발생 시의 락 보유
의 영향을 받습니다.
따라서 SetUnhandledExceptionFilter는
최후의 통지를 받는 best effort의 입구라고 생각하는 것이 딱 좋습니다.
6.6 native C++는 CRT의 종료 경로도 잡는다
native C++에서는 미처리 SEH만 보면 샙니다.
봐두고 싶은 것은 예를 들어 다음입니다.
_set_invalid_parameter_handler_set_purecall_handlerset_terminate
이 계통은 C 런타임이나 C++ 런타임 기점의 「종료 경로」를 잡기 위한 것입니다.
실무에서는,
- 이들 핸들러에서도 최종 크래시 마커를 쓴다
- 다만 무거운 회복 처리는 하지 않는다
- 확실히 종료시킨다
- 주 증적은 WER / dump에 맡긴다
가 무난합니다.
7. WER LocalDumps를 토대로 한다
여기가 실무에서는 꽤 강합니다.
7.1 먼저의 추천은 WER LocalDumps
「떨어진 후에 최저한의 증적을 확실 쪽으로 남긴다」는 의미에서는, 우선은 WER LocalDumps가 가장 다루기 쉽습니다.
이유는 단순합니다.
- OS 측에서 덤프를 남길 수 있다
- 추가 도구 없이 넣기 쉽다
- 앱 단위로 설정할 수 있다
- 크래시 시의 주 증적을 in-process 이외로 도망시킬 수 있다
로그만으로는 모르는
- 어느 스레드가 떨어졌는지
- 어느 스택에서 떨어졌는지
- 어느 모듈 경계였는지
- managed / native / COM / SDK 중 어디가 수상한지
를 나중에 볼 수 있는 것이 강합니다.
7.2 전형 설정
예를 들어 MyApp.exe에 대해 C:\CrashDumps\MyApp에 덤프를 남긴다면 다음과 같이 할 수 있습니다.
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpFolder /t REG_EXPAND_SZ /d "C:\CrashDumps\MyApp" /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpCount /t REG_DWORD /d 10 /f
reg add "HKLM\SOFTWARE\Microsoft\Windows\Windows Error Reporting\LocalDumps\MyApp.exe" /v DumpType /t REG_DWORD /d 2 /f
처음에는 다음 사고방식으로 좋습니다.
| 값 | 먼저의 추천 |
|---|---|
DumpFolder |
전용 폴더 |
DumpCount |
5~10 |
DumpType |
개발기는 2, 현장은 용량과 기밀 요건을 보고 1 or 2 |
7.3 dump 저장처의 ACL을 반드시 확인한다
로그에서도 덤프에서도 마찬가지지만, 쓸 수 없는 폴더에 설정해도 의미가 없습니다.
특히,
- Windows 서비스
- 권한 분리한 자식 프로세스
- 현장기의 제한 계정
- UAC 관련
에서는 저장처 ACL이 헛스윙의 주 원인이 됩니다.
저장처는,
- 사전 작성
- 쓰기 테스트
- 보유 수 제한
- 운용 담당이 보러 갈 수 있는 장소인가
까지 확인합니다.
7.4 WER 리포트에 현재 로그를 첨부하고 싶을 때
Microsoft에 대한 WER 리포트나 독자 WER 운용을 사용하는 경우는, WerRegisterFile로 현재의 로그 파일을 에러 리포트에 포함하기 위한 등록을 하는 방법도 있습니다.
다만 여기는 로컬 저장의 대체가 아닌 추가 도선으로 생각하는 편이 안전합니다. 크래시 시에 정말로 원하는 것은 우선 수중의 단말에 확실 쪽으로 남는 것이기 때문입니다.
순서로서는,
- 로컬 통상 로그
- 로컬 fatal 마커
- 로컬 dump
- 필요하면 WER 송신 경로에서도 관련 파일을 등록
쪽이 실무 쪽입니다.
7.5 덤프뿐만 아니라 버전 관리를 남긴다
덤프를 취해도 나중에
- 그 시점의 EXE / DLL이 없다
- PDB가 없다
- 어느 커밋의 빌드인지 모른다
가 되면 꽤 약해집니다.
최저한 다음은 남깁니다.
- 배포한 바이너리
- 대응하는 PDB
- 버전
- 빌드 일시
- 커밋 식별자
- 인스톨러 판
덤프 수집과 PDB 보관은 세트입니다.
8. MiniDumpWriteDump나 독자 크래시 리포터를 사용할 때의 사고방식
독자 구현이 필요해지는 장면도 있습니다.
- UI에서 「진단 정보를 저장」 버튼을 내고 싶다
- 로그나 설정 파일도 묶고 싶다
- 자식 프로세스군을 함께 다루고 싶다
- 자동 업로드 전에 독자 마스킹을 넣고 싶다
다만 여기서 가장 중요한 것은 dump를 취하는 처리도 떨어지는 측에 너무 짊어지게 하지 않는 것입니다.
8.1 self-dump보다 별도 프로세스
MiniDumpWriteDump는 강력하지만,
크래시한 그 프로세스 속에서 부르기보다 별도 프로세스에서 부르는 편이 안전합니다.
전형 구성은 다음입니다.
- worker 본체가 이상을 검지
- 가능하면 이벤트나 이름 있는 파이프로 helper에 통지
- helper가 worker의 dump를 취한다
- helper가
tail로그나 설정 파일을 묶는다 - helper가 종료 후에 업로드 큐로 놓는다
이것이라면 worker가 망가져도 helper 측은 아직 건전합니다.
8.2 아무래도 in-process라면 전용 스레드로 기울인다
별도 프로세스화할 수 없는 경우라도, 전용 스레드를 dump 전용으로 해 두는 것이 낫습니다.
다만, 그래도 본질은 best effort입니다. 「독자 dump 구현을 넣었으니 100% 안심」이 되지는 않습니다.
8.3 무거운 것은 다음 회 기동 후로 돌린다
독자 리포터에서 하고 싶어지기 쉬운 것이 있습니다.
- zip 압축
- symbol 정보와의 대조
- 서버 업로드
- 화면 캡처
- DB에서 추가 정보 취득
이들은 크래시 시가 아니라 재기동 후나 helper 측에 돌립니다.
9. 감시 프로세스를 넣으면 무엇이 바뀌는가
장시간 운전계에서는 감시 프로세스가 꽤 효과적입니다.
9.1 감시 프로세스가 남기는 것
watchdog / launcher / 부모 서비스는 예를 들어 다음을 남길 수 있습니다.
- 자식 프로세스 개시 시각
- 기동 인수
- PID
- 감시 대상 버전
- heartbeat의 최종 수신 시각
- 종료 시각
- exit code
- restart 횟수
- dump의 유무
- 재기동했는지 여부
이것이 있는 것만으로도,
- 정말로 크래시했는가
- OS 셧다운이었는가
- 사용자가 닫은 것인가
- hang에서 kill된 것인가
- 몇 번 재기동 루프한 것인가
가 꽤 보입니다.
9.2 특히 어울리는 케이스
다음이라면 분리를 꽤 적극적으로 생각해도 좋습니다.
- vendor SDK를 안은 worker
- 이미지 처리 / 동영상 처리 / device I/O
- 감시나 폴링의 부모 루프
- 스크립트나 플러그인 실행
- COM / ActiveX 기존 자산의 호스트
- 64bit / 32bit 브리지나 상호 운용
위험한 처리를 1개의 worker에 가두면 로그 설계도 복구 설계도 편해집니다.
10. 자주 있는 NG
10.1 catch (Exception)으로 로그만 내고 지속한다
가장 흔하고 가장 위험합니다.
- 도중 변경이 남는다
- 공유 상태가 망가진다
- 후속 장해가 늘어난다
- 진정한 원인 지점이 모호해진다
로그가 1개 늘어나는 대신에 사고가 길어지는 경우가 많습니다.
10.2 async logger의 큐만을 믿는다
비동기 로그 자체는 나쁘지 않습니다. 문제는 fatal path에서도 같은 큐에 쌓아 끝내는 것입니다.
떨어진 순간에 워커가 멈추면 그 큐째로 사라집니다.
fatal path만은 직접 쓴다 도망 길을 가지는 편이 안전합니다.
10.3 크래시 핸들러에서 HTTP 송신한다
구현하고 싶어지지만 꽤 위험합니다.
- DNS
- TLS
- proxy
- 인증
- 타임아웃
- 재송 대기
전부가 떨어진 문맥에 탑니다.
보내는 것은 재기동 후에 합니다.
10.4 dump는 있지만 통상 로그와 결합되지 않는다
이것은 많습니다.
- 덤프 파일 이름에 session이 없다
- 로그 측에 PID / session이 없다
- watchdog 측에 PID가 없다
- build 번호가 일치하지 않는다
결과적으로 3개의 증적이 별개의 이야기로 보이게 됩니다.
10.5 WinForms / WPF의 미처리 예외 이벤트로 연명한다
외견상 「떨어지지 않게」 되므로 처음에는 기뻐됩니다. 하지만 실제로는
- 화면만 남는다
- 워커는 죽어 있다
- 버튼 활성만 남는다
- 저장할 수 있었는지 모른다
는 좀비 상태를 만들기 쉽습니다.
10.6 native 측의 종료 경로를 보고 있지 않다
SetUnhandledExceptionFilter만으로 안심하면,
- invalid parameter
- purecall
- terminate
- fast fail
측을 놓칩니다.
native C++에서는 SEH뿐만 아니라 CRT / C++ 런타임 측의 종료 경로도 의식하는 편이 좋습니다.
11. 최저한의 도입 체크리스트
다음을 충족하면 꽤 실전적이 됩니다.
- 통상 로그가 1행 1이벤트로 남는다
- 모든 로그에 UTC, PID, TID, version, session이 있다
ProcessStart와ProcessExit가 남는다- 중요 경계 이벤트는 동기적으로 flush된다
- 최종 크래시 마커 전용 파일이 있다
- fatal path에서는 async logger를 경유하지 않는다
- WER LocalDumps가 앱 단위로 설정되어 있다
- dump 저장처의 ACL을 검증 완료
- PDB와 배포 바이너리를 보관하고 있다
- 다음 회 기동 시에 전회 이상 종료를 검지할 수 있다
- 압축 / 업로드 / 알림은 재기동 후 또는 별도 프로세스에서 한다
- native C++에서는 invalid parameter / purecall / terminate도 정리했다
- 검증기에서 의도적으로 떨어뜨려 정말로 남는지를 확인했다
마지막 1행이 특히 중요합니다. 설계만으로는 의미가 없고, 반드시 「취하기까지 하는 시험」을 할 필요가 있습니다.
12. 어디까지 시험할까
추천 확인 항목은 다음입니다.
| 시험 | 무엇을 확인하는가 |
|---|---|
| managed의 미처리 예외 | 통상 로그, fatal 마커, dump가 전부 갖춰지는가 |
| UI 스레드 예외 | WinForms / WPF의 이벤트 경로가 상정대로인가 |
| worker 스레드 예외 | AppDomain.UnhandledException까지 오는가, watchdog이 검지할 수 있는가 |
| native 예외 | WER dump가 정말로 취해지는가 |
| invalid parameter / terminate | CRT / C++ 런타임 경로에서도 최소 기록이 남는가 |
| 강제 kill | in-process에서는 무리여도 watchdog 측이 unexpected exit를 기록할 수 있는가 |
| 재기동 | 다음 회 기동 후의 알림, 회수, 업로드가 동작하는가 |
「예외가 날면 로그가 나올 터」가 아니라 「이 조건에서 이 파일이 남는다」고 확인하는 것이 중요합니다.
13. 정리
Windows 앱이 프로그램 실수에 의한 예외로 떨어져도 조사에 필요한 정보를 남기고 싶다면, 사고방식의 축은 꽤 단순합니다.
- 떨어지는 측의 프로세스만에 기대지 않는다
- 통상 로그, 최종 크래시 마커, OS / 별도 프로세스 측의 증적으로 나눈다
- 크래시 시는 로컬에 짧게 남기기만 한다
- 무거운 처리는 재기동 후나 별도 프로세스로 돌린다
- WER LocalDumps를 토대로 한다
- 지속보다 기록해서 종료를 기본으로 한다
요컨대, 「마지막 1행을 분발한다」보다 「마지막 1행이 없어도 쫓을 수 있는 구성을 만든다」 쪽이 강합니다.
그래도 마지막 1행은 원하므로, 최종 크래시 마커는 별도 파일에 짧게 남긴다. 그리고 진짜 주 증적은 WER의 dump와 직전까지의 통상 로그에 가지게 한다. 이것이 Windows 앱의 실무에서는 꽤 안정된 방식입니다.
관련 기사
- Windows 앱의 크래시 덤프 수집 입문 - 먼저 WER / ProcDump / WinDbg을 어떻게 구분 사용할까
- 상정하지 않은 예외가 일어났을 때, 앱을 종료시켜야 할지 지속해야 할지 - 먼저 보는 판단표
참고 자료
- Microsoft Learn: Collecting User-Mode Dumps https://learn.microsoft.com/en-us/windows/win32/wer/collecting-user-mode-dumps
- Microsoft Learn: Using WER https://learn.microsoft.com/en-us/windows/win32/wer/using-wer
- Microsoft Learn: MiniDumpWriteDump function https://learn.microsoft.com/en-us/windows/win32/api/minidumpapiset/nf-minidumpapiset-minidumpwritedump
- Microsoft Learn: SetUnhandledExceptionFilter function https://learn.microsoft.com/en-us/windows/win32/api/errhandlingapi/nf-errhandlingapi-setunhandledexceptionfilter
- Microsoft Learn: System.AppDomain.UnhandledException event https://learn.microsoft.com/en-us/dotnet/fundamentals/runtime-libraries/system-appdomain-unhandledexception
- Microsoft Learn: Application.ThreadException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.forms.application.threadexception
- Microsoft Learn: Application.DispatcherUnhandledException Event https://learn.microsoft.com/en-us/dotnet/api/system.windows.application.dispatcherunhandledexception
- Microsoft Learn: TaskScheduler.UnobservedTaskException Event https://learn.microsoft.com/en-us/dotnet/api/system.threading.tasks.taskscheduler.unobservedtaskexception
- Microsoft Learn: Environment.FailFast https://learn.microsoft.com/en-us/dotnet/api/system.environment.failfast
- Microsoft Learn: Registering for Application Recovery https://learn.microsoft.com/en-us/windows/win32/recovery/registering-for-application-recovery
- Microsoft Learn: RegisterApplicationRecoveryCallback https://learn.microsoft.com/en-us/windows/win32/api/winbase/nf-winbase-registerapplicationrecoverycallback
- Microsoft Learn: WerRegisterFile https://learn.microsoft.com/en-us/windows/win32/api/werapi/nf-werapi-werregisterfile
- Microsoft Learn: _set_invalid_parameter_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-invalid-parameter-handler-set-thread-local-invalid-parameter-handler
- Microsoft Learn: _set_purecall_handler https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/get-purecall-handler-set-purecall-handler
- Microsoft Learn: set_terminate https://learn.microsoft.com/en-us/cpp/c-runtime-library/reference/set-terminate-crt
- Microsoft Learn: __fastfail https://learn.microsoft.com/en-us/cpp/intrinsics/fastfail
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
어디서 예외를 `catch`하고 로그를 내며 에러 처리해야 하는가 - 호출 계층의 경계와 책무를 실무용으로 정리
깊은 helper에서 넓게 catch하지 않고 호출 계층의 경계에 책무를 모아, 어디서 예외를 잡고 어디서 단일 주요 로그를 남기며 어디서 결과화·회복을 결정할지 .NET 실무 관점으로 정리한 가이드입니다.
Windows 앱의 크래시 덤프 수집 입문 - 우선 WER / ProcDump / WinDbg를 어떻게 구분해서 쓸까
Windows 앱의 재현 어려운 크래시를 쫓는 첫걸음으로, WER LocalDumps, ProcDump, MiniDumpWriteDump의 구분 사용과 미니/풀 덤프 선택, PDB 보관과 권한 설계, WinDbg에서 먼저 볼 포인트까지 입문자에게...
상정하지 않은 예외가 발생했을 때의 체크리스트 - 앱을 종료시킬지 계속할지, 먼저 보는 판단표
상정 외 예외 시 앱을 종료할지 계속할지를 실패 단위 격리·공유 상태 회복·외부 부작용 설명·네이티브 경계 건전성의 네 축으로 판단하는 흐름을 표와 플로차트로 정리한 글입니다. 독자는 catch 가능 여부가 아니라 불변 조건 회복 가능성으로 가르...
Windows의 문자 코드와 개행 코드를 정리한다 - Shift_JIS / UTF-8 / UTF-16, 문자 깨짐, CRLF / LF, 왜 혼란스러운가
Windows에서 자주 섞이는 Shift_JIS와 UTF-8, UTF-16, BOM, CRLF/LF의 차이를 bytes 시점에서 분해하고, 문자 깨짐과 개행 문제를 나누어 다루는 실무 규칙과 사고 조사의 5문 체크리스트까지 정리했습니다.
의사난수와 진짜 난수의 차이란 - 어떻게 구별하는지 정리
의사난수와 진짜 난수(NRBG)를 구별하는 핵심을 정리합니다. 출력의 겉모습이 아니라 생성기 구성, seed, reseed, health test로 봐야 한다는 점과 보안 용도에서는 OS의 secure RNG와 CSPRNG가 본진임을 짧게 설명합니다.
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
장애 조사 & 원인 분석
간헐적 장애, 장기 가동 중 크래시, 누수, 통신 중단 등 까다로운 프로덕션 이슈를 조사합니다.
저자 프로필
기사 저자의 프로필 페이지입니다.
Go Komura
합동회사 코무라소프트 대표
Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.
공개 링크