상정하지 않은 예외가 발생했을 때의 체크리스트 - 앱을 종료시킬지 계속할지, 먼저 보는 판단표
상정하지 않은 예외의 이야기가 되면 무심코 「떨어뜨릴 것인가, catch해서 계속할 것인가」의 이택으로 생각하기 쉽습니다.
다만 실무에서는 이 이택의 놓는 방식이 조금 거칩니다.
정말로 보고 싶은 것은 망가졌을 가능성이 있는 범위를 가두어 둘 수 있는가입니다.
- 그 조작만 실패로 끝낼 수 있는가
- 그 화면 / 접속 / 워커만 재초기화하면 되는가
- 이미 프로세스 전체의 정합성이 의심스러운가
이 순서로 보면 꽤 정리하기 쉬워집니다.
이 글에서는 C# / .NET의 Windows 앱, 상주 앱, Windows 서비스, 장치 연계 도구 등을 전제로 상정하지 않은 예외가 발생했을 때 계속해도 되는 조건과, 종료하는 편이 좋은 조건을 판단표로 정리합니다.
1. 먼저 결론
catch (Exception)으로 움켜쥐고 속행, 은 대체로 위험합니다.- 계속해도 되는 것은 실패한 단위를 버릴 수 있다, 공유 상태를 원래대로 되돌릴 수 있다, 외부 부작용을 설명할 수 있다의 3가지가 갖춰질 때입니다.
- UI의 1조작, 1건의 입력, 1건의 잡처럼 처리 경계가 명확하다면 계속할 수 있는 경우가 있습니다.
- 반대로 공유의 쓸 수 있는 상태, 상주 루프, 메인 스레드, 기동 처리, 네이티브 경계, 메모리 파괴 냄새가 얽히면 종료 쪽입니다.
StackOverflowException,AccessViolationException,OutOfMemoryException같은 「프로세스 전체의 건전성」을 의심하는 예외는 계속 전제로 생각하지 않는 편이 안전합니다.- WPF나 Windows Forms에는 미처리 예외를 주워서 겉보기에 계속하는 길도 있지만, 계속할 수 있는 것과 계속해도 안전한 것은 별개입니다.
- 장시간 도는 서비스나 감시 앱은 반쯤 망가진 채 살아남기보다, 떨어져 재기동되는 편이 진단도 쉽고 안전한 경우가 많습니다.
요컨대 판단의 축은 「catch할 수 있는가」가 아니라 불변 조건을 회복할 수 있는가입니다.
2. 이 글에서 말하는 「상정하지 않은 예외」
2.1 상정 내와 상정 외를 나눈다
우선 드문 예외와 상정 외 예외는 같지 않습니다.
예를 들어 다음은 빈도가 낮아도 상정 내로 할 수 있습니다.
- 사용자가 존재하지 않는 파일을 선택했다
- 통신처가 일시적으로 타임아웃됐다
- 취합 CSV의 1행이 망가져 있었다
- 캔슬 조작으로
OperationCanceledException이 나왔다 - 업무 규칙 위반으로 그 처리만 실패로 하고 싶다
이것들은 그 실패를 어떻게 다룰지를 설계에서 먼저 정할 수 있는 종류입니다.
한편으로 이 글에서 주로 다루는 상정하지 않은 예외는 예를 들어 다음입니다.
- 자기 코드의 전제가 무너져
NullReferenceException이나InvalidOperationException이 나왔다 - 공유 상태의 갱신 도중에 예외가 날아가 어디까지 반영됐는지 의심스럽다
- 감시 루프나 메시지 처리의 부모 루프가 떨어졌다
- COM / P/Invoke / vendor SDK 경계에서 이상이 나왔다
AccessViolationException이나StackOverflowException처럼 애초에 프로세스의 건강 진단이 빨갛다
즉 「이 예외가 일어난 뒤에 앱의 상태를 아직 신뢰해도 될지 알 수 없다」는 것입니다.
2.2 이택으로 보여도 실은 삼택 있다
이 이야기를 까다롭게 하는 범인은 계속을 1종류로 생각해 버리는 것입니다.
실무에서는 대체로 다음 3단계로 나뉩니다.
| 선택 | 의미 |
|---|---|
| 그 조작만 실패시키고 계속 | 화면은 남기지만 이번 저장이나 취합만 실패 처리 |
| 서브시스템만 멈추고 계속 | 접속, 화면, 워커, 자식 프로세스만 재초기화 |
| 프로세스를 종료 | 상태 파괴의 범위를 읽을 수 없으므로 재기동 전제로 |
「앱을 계속한다」고 해도 아무 일도 없었다는 얼굴로 그대로 계속하는 것과 망가진 부분을 떼어내 계속하는 것은 무게가 다릅니다.
3. 먼저 보는 판단표
3.1 전체상
우선 이 표부터 보면 대체로의 방침이 정해집니다.
| 상황 | 우선의 선택 | 이유 |
|---|---|---|
| 1개의 입력, 1개의 화면 조작, 1개의 잡만 실패하고 상태를 버릴 수 있다 | 계속 쪽 | 실패 단위를 가둘 수 있으므로 |
| 예외 후에 대상 객체나 접속을 파기하고 다시 만들 수 있다 | 서브시스템 재초기화 쪽 | 망가진 범위를 국소화할 수 있으므로 |
| 공유 상태를 도중까지 갱신했지만 어디까지 반영됐는지 모른다 | 종료 쪽 | 불변 조건이 무너져 있을 가능성이 있으므로 |
| DB / 파일 / 장치 커맨드 등 외부 부작용이 어중간해 중복이나 미반영을 설명할 수 없다 | 종료 쪽 | 밖의 세계와의 정합을 읽을 수 없으므로 |
| 감시 루프, 재접속 루프, 메시지 처리의 부모 루프가 상정 외 예외로 떨어졌다 | 종료 쪽 | 조용히 일부 기능만 죽으면 좀비화되기 쉬우므로 |
| 기동 처리, 설정 읽기, DI 구성, 필수 의존의 초기화에서 실패 | 기동 실패로 종료 쪽 | 어중간하게 기동하는 편이 위험하므로 |
AccessViolationException, StackOverflowException, 심각한 OutOfMemoryException, 네이티브 쪽의 파괴 냄새 |
즉시 종료 쪽 | 프로세스 전체의 건전성이 의심스러우므로 |
| 위험한 처리가 별도 프로세스에 격리되어 있고, 부모 프로세스는 무사 | 부모는 계속, 자식을 재기동 | 장애 영역을 분리할 수 있으므로 |
flowchart TD
A["상정하지 않은 예외"] --> B{"메모리 파괴 / 스택 고갈 / 치명적 자원 고갈 냄새?"}
B -- "예" --> Z["종료 / FailFast / 재기동"]
B -- "아니오" --> C{"실패한 단위를 버릴 수 있는가?"}
C -- "아니오" --> Y["종료 쪽"]
C -- "예" --> D{"공유 상태를 롤백 / 재초기화할 수 있는가?"}
D -- "아니오" --> X["서브시스템 정지 or 종료"]
D -- "예" --> E{"외부 부작용을 설명할 수 있는가?"}
E -- "아니오" --> X
E -- "예" --> W["그 조작만 실패로 해서 계속"]
3.2 예외 타입보다 먼저 볼 것
예외 타입만으로 즉결하지 않는 편이 좋습니다.
처음에 봐야 할 것은 다음입니다.
| 볼 점 | 무엇을 확인할까 |
|---|---|
| 어디서 일어났는가 | UI 이벤트, 1건 잡, 부모 루프, 기동 처리, 네이티브 경계 중 어디인가 |
| 어디까지 진행됐는가 | 도중에 메모리 상태, DB, 파일, 장치 상태가 바뀌지 않았는가 |
| 망가질 수 있는 범위 | 그 객체만인가, 화면 전체인가, 프로세스 전체인가 |
| 롤백 가능한가 | 파기해 다시 만들 수 있는가, 트랜잭션으로 되돌릴 수 있는가 |
| 외부 부작용 | 송신 완료인가 미송신인가, 이중 실행이 안전한가, 보상 처리할 수 있는가 |
| 감시·재기동 | 떨어뜨린 뒤 자동 재기동이나 복구 도선이 있는가 |
3.3 위험도 높은 예외
세세한 예외 타입 이야기를 전부 할 필요는 없지만, 계속 전제로 보지 않는 편이 좋은 것이 있습니다.
| 예외 / 징조 | 우선의 선택 | 볼 이유 |
|---|---|---|
StackOverflowException |
즉시 종료 쪽 | 호출 스택이 파탄 나 있어 통상의 회복을 전제로 하기 어려움 |
AccessViolationException |
즉시 종료 쪽 | 보호 메모리에의 부정 접근으로 네이티브 경계나 메모리 파괴가 의심됨 |
OutOfMemoryException |
종료 쪽 | 추가 할당 전제의 회복 처리 자체가 불안정해지기 쉬움 |
unexpected한 NullReferenceException / InvalidOperationException |
문맥에 따르지만 종료 쪽 | 자기 전제 무너짐이며 도중 변경이 남아 있을 가능성이 있음 |
| 부모 루프에서 새어나온 상정 외 예외 | 종료 쪽 | 기능의 중핵이 죽었는데 프로세스만 남는 위험이 있음 |
| COM / P/Invoke / vendor SDK callback 기점의 이상 | 즉시 종료~강한 종료 쪽 | managed만 봐도 안전성을 판단하기 어려움 |
4. 어디서 일어났는가로 판단한다
4.1 UI 이벤트
버튼 클릭, 화면 전환, 검색, 파일 선택 같은 UI 이벤트는 계속할 수 있는 여지가 비교적 큽니다.
다만 조건이 있습니다.
계속하기 쉬운 것은 예를 들어 다음입니다.
- 읽기 전에 실패해 업무 상태를 아직 건드리지 않았다
- 대화상자 내의 일시 상태만 망가져 있어 화면을 닫으면 버릴 수 있다
- 예외 후에 ViewModel이나 접속을 다시 만들 수 있다
- 이용자에게 「이번 조작은 실패했다」고 정직하게 전할 수 있다
반대로 종료 쪽이 되는 것은 다음입니다.
- 화면과 도메인 상태의 양쪽을 도중까지 갱신했다
- static / singleton / 캐시 등 다른 화면도 보는 공유 상태를 건드렸다
- 예외가 일어난 뒤 버튼 활성이나 선택 상태만 남아 정합을 모르겠다
- UI 스레드 상에서 unexpected한 예외가 일어나 어디까지 묘화나 통지가 진행됐는지 의심스럽다
4.2 1건씩 처리하는 잡 / 리퀘스트
여기는 계속하기 쉬운 경계입니다.
- 1 메시지
- 1 파일
- 1 HTTP 리퀘스트
- 1 취합 잡
- 1 배치 대상
이런 단위가 명확하다면 그 1건만 실패로 해서 다음으로 진행합니다.
다만 다음이 필요합니다.
- 실패 단위가 밖에서 봐서 명확
- 도중 변경이 트랜잭션이나 보상으로 정리된다
- 같은 처리를 한 번 더 흘려도 결과가 망가지지 않는 성질이 있다
- 실패를 격리 큐나 에러 기록으로 내보낼 수 있다
4.3 상주 루프 / 감시 / 큐 처리
여기는 거칠게 계속하면 가장 위험한 장소입니다.
예를 들어:
- 재접속 루프
- 감시 루프
- 큐 소비 루프
- 정기 폴링
- 장치 상태 감시
- 트레이 앱의 상주 처리
이런 처리에서 무서운 것은 부모 루프가 1회의 상정 외 예외로 죽고, 프로세스만 살아남는 것입니다.
여기서는 방침을 나누는 편이 좋습니다.
- 각 아이템 처리의 경계에서 상정 내 예외를 잡는다
- 부모 루프에서 상정 외 예외가 새어나오면 프로세스 종료 쪽으로 한다
4.4 기동 처리
기동 시의 실패를 「일단 기동한 뒤에 생각한다」로 하면 대체로 나중에 웁니다.
- 필수 설정을 읽을 수 없다
- 버전 이행 / 마이그레이션에 실패했다
- 필수 폴더나 증명서가 없다
- 중핵 서비스의 초기화에 실패했다
- 의존 관계의 구성이 망가져 있다
이런 경우는 기동 실패로 종료하는 편이 이해하기 쉽습니다.
4.5 네이티브 경계 / COM / P/Invoke / unsafe
여기는 별도 프레임으로 조금 엄격하게 보는 편이 좋습니다.
- COM
- P/Invoke
- C++/CLI의 앞
- vendor SDK
- callback으로 돌아오는 native 쪽 코드
unsafe를 포함한 처리
특히 다음은 종료 쪽입니다.
AccessViolationException- 힙 파괴나 더블 free를 의심하는 증상
- 핸들 이상, 해제 후 접근 냄새
- callback 경계에서 갑자기 죽는다
5. 계속해도 되는 조건
계속해도 되는 것은 대체로 다음이 갖춰질 때입니다.
| 조건 | 의미 |
|---|---|
| 실패 단위가 명확 | 1 조작, 1 화면, 1 잡, 1 접속 등 버릴 단위를 알 수 있다 |
| 상태를 버릴 수 있다 | 파기해 다시 만들 수 있거나 미반영으로 다룰 수 있다 |
| 공유 상태가 지켜진다 | 다른 기능으로 오염이 번지지 않는다 |
| 외부 부작용을 설명할 수 있다 | 보냈다 / 안 보냈다 / 재송해도 된다를 알 수 있다 |
| 이용자에게 정직하게 전할 수 있다 | 「이번 처리는 실패했다」고 표시할 수 있다 |
| 감시할 수 있다 | 로그, 메트릭, 덤프로 추적 조사할 수 있다 |
6. 종료하는 편이 좋은 조건
반대로 다음에 해당하면 종료 쪽입니다.
- 도중까지 무엇을 변경했는지 모르겠다
- 공유의 쓸 수 있는 상태를 건드려 정합을 읽을 수 없다
- 락, 큐, 스레드, 감시 루프의 수명 관리가 망가졌다
- 외부 부작용의 중복 / 누락 / 어중간을 설명할 수 없다
- 기동 처리나 중핵 인프라의 초기화에서 실패
- 네이티브 경계나 메모리 파괴를 의심한다
이 레벨이라면 깔끔하게 계속하는 궁리보다 떨어뜨려 복구하기 쉽게 하는 궁리가 효과가 있습니다.
7. 전형 패턴별의 추천
| 패턴 | 추천 | 이유 |
|---|---|---|
| 파일을 여는 버튼에서 존재하지 않는 패스를 지정했다 | 그 조작만 실패로 계속 | 상태 파괴가 국소적이므로 |
| CSV 취합의 1행만 망가져 있었다 | 1행 실패 or 1파일 실패로 계속 | 실패 단위를 가두기 쉬우므로 |
화면 저장 도중에 unexpected한 NullReferenceException이 나왔다 |
화면 재작성~종료 쪽 | 어디까지 ViewModel / 업무 상태가 바뀌었는지 의심스러우므로 |
| 큐의 1메시지가 업무 규칙 위반이었다 | 그 메시지만 실패로 계속 | 격리 큐로 내보낼 수 있으므로 |
| 큐 소비의 부모 루프가 상정 외 예외로 떨어졌다 | 프로세스 종료 쪽 | 워커 전체의 수명이 망가져 있으므로 |
| 기동 시에 필수 설정을 읽을 수 없다 | 기동 실패로 종료 | 어중간 기동 쪽이 위험하므로 |
vendor SDK callback 주변에서 AccessViolationException |
즉시 종료 쪽 | 메모리 파괴의 가능성을 무시할 수 없으므로 |
| 비본질인 텔레메트리 송신만 실패했다 | 그 기능만 무효화해 계속 | 주기능과 장애 영역을 나눌 수 있으므로 |
8. 흔한 NG
8.1 catch (Exception)로 로그만 내고 계속
이것은 꽤 위험합니다.
원인을 숨기는 데다 망가진 상태를 연명하기 쉽기 때문입니다.
8.2 마지막의 미처리 예외 핸들러에서 회복하려고 한다
AppDomain.UnhandledException, Application.ThreadException, DispatcherUnhandledException 등은 마지막에 기록하는 장소로서는 유용하지만 마법의 회복 포인트가 아닙니다.
8.3 외부 부작용이 있는데 안이하게 retry한다
장치 커맨드, 메일 송신, 과금, 파일 이동, DB 갱신 등에서 같은 처리의 재실행 안전성이 없는 채 retry하면 이번에는 이중 실행 사고가 주역이 됩니다.
8.4 감시 루프가 죽었는데 UI만 남긴다
겉보기만 살아 있고 일을 하지 않는 앱은 꽤 폐가 됩니다.
8.5 떨어뜨리는 설계를 하고 있지 않은데 「떨어뜨리고 싶지 않다」고 한다
떨어뜨리고 싶지 않다면 먼저 다음을 넣을 필요가 있습니다.
- 자동 재기동
- 세션 복원
- 도중 성과의 저장
- 재실행 안전성
- 장애 영역의 분리
9. 구현 시의 정리 포인트
9.1 catch하는 장소를 경계로 치우친다
깊은 층에서 무엇이든 catch하기보다,
- UI 조작 경계
- 1 리퀘스트 경계
- 1 잡 경계
- 1 접속 경계
- 프로세스 경계
처럼 실패 단위를 정의할 수 있는 장소에서 받는 편이 정리하기 쉽습니다.
9.2 상정 내 예외와 상정 외 예외를 나눈다
- 상정 내: validation, not found, timeout, cancel, 업무 규칙 위반
- 상정 외: 전제 무너짐, 부모 루프 새어나감, 네이티브 경계 이상, 메모리 파괴 냄새
9.3 공유 상태를 작게 한다
공유의 쓸 수 있는 상태가 클수록 계속 판단은 어려워집니다.
반대로 1화면 1세션 1워커 안으로 가둘 수 있을수록 실패도 가두기 쉬워집니다.
9.4 위험한 처리는 별도 프로세스로 내보낸다
COM / ActiveX / vendor SDK / unsafe / 무거운 이미지 처리 / 외부 기기 제어 등 떨어졌을 때의 피해를 넓히고 싶지 않은 것은 별도 프로세스화가 꽤 효과가 있습니다.
9.5 미처리 예외 핸들러는 「회복」보다 「기록」
- 예외 정보
- 조작 문맥
- 직전의 중요 로그
- 설정 / 버전 / 접속처
- dump 채취 도선
이 근처를 갖춰 떨어진 뒤에 파고들 수 있는 형태를 우선하는 편이 결과적으로 안정됩니다.
9.6 WPF / WinForms의 미처리 예외 이벤트를 과신하지 않는다
WPF에서는 DispatcherUnhandledException에서 Handled = true로 하면 미처리 예외 후에도 계속하는 것 자체는 할 수 있습니다.
Windows Forms에서도 메인 UI 스레드에서는 Application.ThreadException이나 SetUnhandledExceptionMode의 설정 나름으로 멈추는 방식을 고를 수 있습니다.
다만 여기서 중요한 것은 계속할 수 있는 것이 아니라 회복 조건이 갖춰져 있는가입니다.
10. 정리
상정하지 않은 예외가 발생했을 때 봐야 할 것은 「이 예외는 catch할 수 있는가」가 아니라 이 후에도 앱의 상태를 신뢰할 수 있는가입니다.
판단의 순서로는 대체로 다음으로 충분합니다.
- 실패한 단위를 버릴 수 있는가
- 공유 상태를 되돌릴 수 있는가, 다시 만들 수 있는가
- 외부 부작용을 설명할 수 있는가
- 메모리 / 스레드 / 네이티브 경계의 건전성을 신뢰할 수 있는가
이 4가지에 자신이 있다면 계속할 수 있습니다.
자신이 없다면 종료 쪽입니다.
특히 장시간 도는 앱, 감시 앱, 서비스, 장치 연계에서는 망가진 채 사는 것이 솔직하게 떨어지는 것보다 위험한 장면이 꽤 있습니다.
예외 처리는 「떨어뜨리지 않는 기술」이 아닙니다.
망가지는 방식을 작게 하고, 망가지면 정직하게 멈추고, 복구하기 쉽게 하는 설계입니다.
11. 참고 자료
- .NET: Best practices for exceptions
- .NET: System.Exception
- .NET: StackOverflowException
- .NET: System.AccessViolationException
- .NET: Environment.FailFast
- .NET: AppDomain.UnhandledException
- WPF: Application.DispatcherUnhandledException
- Windows Forms: Application.SetUnhandledExceptionMode
- .NET: Exceptions in Managed Threads
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
어디서 예외를 `catch`하고 로그를 내며 에러 처리해야 하는가 - 호출 계층의 경계와 책무를 실무용으로 정리
깊은 helper에서 넓게 catch하지 않고 호출 계층의 경계에 책무를 모아, 어디서 예외를 잡고 어디서 단일 주요 로그를 남기며 어디서 결과화·회복을 결정할지 .NET 실무 관점으로 정리한 가이드입니다.
Windows 앱 개발에서 최저한의 보안을 지키기 위한 체크리스트
WPF・WinForms・WinUI・Win32 등 Windows 앱 개발에서 권한, 서명, 비밀 정보, HTTPS, 입력 검증, DLL 읽기, 로그까지 릴리스 전에 빠뜨리면 사고로 이어지는 최저한의 보안 항목을 체크리스트 형태로 정리합니다.
자체 제작 logger를 피할 수 없을 때, 정말 필요한 최소 요건은 무엇인가: 실무 요건과 통합 테스트 관점
자체 제작 logger를 만들 때 처음부터 정해 두고 싶은 형식·필수 항목·flush·순환·실패 처리 등 최소 요건과, 실제 파일과 스레드를 쓰는 통합 테스트의 v1 체크리스트를 정리해, 장애 시에 신뢰할 수 있는 로그 기반을 굳히는 길을 보여 ...
어디까지를 유닛 테스트로 검증하고 어디부터를 결합 테스트로 검증해야 하는가 - 경계를 긋는 방법과 실무 판단표
유닛 테스트와 결합 테스트의 경계를 어디에 그어야 할지 망설이는 분을 위해, 순수 로직과 접속·배선·환경 차이라는 축으로 분류하고 실무에서 바로 쓸 수 있는 판단표와 사례로 정리합니다. 테스트가 빠르고 망가지기 어려워집니다.
Windows에서 타이머 대기보다 이벤트 대기를 우선하는 이유 - 약 15.6ms 입자의 폴링을 피한다
Windows의 짧은 timer wait는 system clock 입자와 스케줄러 지연에 묶여 의도한 정밀도가 나오지 않습니다. 작업 도착·I/O 완료·정지 요청은 event 대기로, 시각 자체는 waitable timer로 나누는 설계 지침을 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
장애 조사 & 원인 분석
간헐적 장애, 장기 가동 중 크래시, 누수, 통신 중단 등 까다로운 프로덕션 이슈를 조사합니다.