앱에서 본 Windows 종료 ── 종료 알림, 다시 시작, 정전을 올바르게 견디기

· · Windows, 종료, Windows 개발, Windows 서비스, 장치 PC, 데이터 무결성, 장시간 가동, UPS

「야간 Windows Update 다시 시작으로 장치 PC의 계측 앱이 쓰기 도중에 내려가, 아침에는 계측 파일이 깨져 있었다」. 「공유 PC에서 누군가 로그아웃했는데 저장하지 않은 편집이 사라졌다고 항의받았다」── 장시간 가동 Windows 앱에서 이 두 상담은 고전입니다.

두 현장의 공통점은 종료를 「일어나면 안 되는 이상 이벤트」로 다루는 것입니다. 그러나 현실은 Windows Update 자동 다시 시작, 사용자 로그아웃, UPS가 시작한 종료부터 예고 없는 정전까지, 앱 밖에서 실행을 자르는 이벤트는 조만간 옵니다. 오는 것을 막을 수는 없습니다. 막을 수 있는 것은 「왔을 때 데이터를 잃는 것」입니다.

다행히 Windows에는 GUI 앱, 콘솔 앱, 서비스 모두에 종료 전에 앱에 알리는 메커니즘이 있습니다. 중소기업의 IT 담당자와 Windows 앱 개발자(특히 장치 PC·장시간 가동 앱)를 대상으로, 그 알림을 받는 법, 「수 초 안에 끝나는」 정리를 설계하는 법, 다시 시작 뒤의 자동 복구, 알림이 없는 정전에 대비하는 법을 2026년 8월 시점의 Microsoft Learn 1차 정보로 정리합니다.

1. 먼저 결론

  • 종료를 「조만간 오는 일상 이벤트」로 설계합니다. 알림을 받은 뒤 쓸 수 있는 시간은 원칙적으로 약 5초뿐이므로, 그 자리에서 허겁지겁 전부를 저장하는 설계는 무너집니다. 전제는 「종료 때 저장해야 할 차분」이 작아지도록 빈번히 자동 저장하는 것입니다.1
  • Windows 8 이후 클라이언트 OS에서 빠른 시작이 켜져 있으면(최대 절전을 지원하는 대부분의 PC에서 기본값) 「종료」는 하이브리드 종료이며, 커널은 최대 절전만 합니다. 완전히 리셋되는 것은 「다시 시작」뿐입니다. 「종료해도 안 나았는데 다시 시작하니 나았다」의 진짜 이유입니다.2
  • GUI 앱은 WM_QUERYENDSESSION에 즉시 TRUE를 돌려주고, 정리는 WM_ENDSESSION에서 합니다. 원칙적으로 FALSE(거부)를 돌려주면 안 됩니다.1
  • 정말로 중단할 수 없는 작업이 있을 때만 ShutdownBlockReasonCreate로 이유를 표시합니다. 그때도 사용자와 OS는 강제 계속할 수 있으므로, 「막을 수 있다」고 가정하는 설계는 성립하지 않습니다.34
  • 콘솔 앱은 SetConsoleCtrlHandler로 알림을 받습니다. 유예는 더 짧아, 콘솔 닫기의 기본은 5초입니다. gdi32.dll 또는 user32.dll을 로드한 프로세스에서는 이 이벤트 일부가 오지 않는 함정도 있습니다.56
  • .NET의 AppDomain.ProcessExit에 의존하는 정리는, .NET 10부터 「밖에서 종료되는」 경로에서 돌지 않습니다. Main에서 돌아오는 정상 종료에서는 이전과 같이 돌지만, 런타임이 콘솔 닫기·종료 같은 종료 시그널의 기본 처리를 더 이상 제공하지 않으므로, 그 경로의 정리는 앱 모델에 맞는 알림으로 옮겨야 합니다.7
  • Windows 서비스는 SERVICE_ACCEPT_PRESHUTDOWN을 SERVICE_ACCEPT_SHUTDOWN(유예 약 20초)보다 먼저, 설정 가능한 유예와 함께 받을 수 있습니다. 다만 기본 PRESHUTDOWN 타임아웃은 Windows 10 Creators Update 이후 10초로 짧아졌으므로, 어느 쪽이든 유예에 너무 기대지 않는 설계가 필요합니다.89
  • 다시 시작 뒤의 자동 복구는 RegisterApplicationRestart와 ARSO(자동 사인온)를 조합해 달성할 수 있습니다. 크래시, 응답 없음, 업데이트에 의한 다시 시작에 복구 경로가 제공됩니다.1011
  • 정전은 알림이 전혀 없습니다. 표준 패턴은 임시 파일에 완전히 쓰고, 플러시하고, ReplaceFile로 바꾸는 것이지만, ReplaceFile도 정전을 넘는 원자성을 보장하지 않으므로, 백업(.bak)과 로드 시 검증의 복구 경로가 세트의 일부입니다. 사후 분리는 이벤트 로그(1074/41/6008)로 할 수 있습니다.121314

한 문장으로 이 글의 결론은 「알림이 오면 수 초 안에 가게를 닫을 수 있는 상태를 항상 유지하고, 알림이 없는 정전에도 깨지지 않게 쓴다」입니다.

2. 종료 때 일어나는 일 ── 끝나는 네 가지 방법

2.1. 로그아웃, 종료, 다시 시작, 정전

앱의 관점에서 중요한 축은 둘입니다. 「사용자 세션이 어떻게 끝나는가」와 「커널에 무슨 일이 일어나는가」입니다.

조작 사용자 세션 커널과 드라이버 앱으로의 알림
로그아웃 끝남 계속 돈다 WM_QUERYENDSESSION (ENDSESSION_LOGOFF) → WM_ENDSESSION
종료(빠른 시작 켜짐) 끝남 최대 절전(hiberfil.sys에 저장) WM_QUERYENDSESSION → WM_ENDSESSION, 서비스에는 (PRE)SHUTDOWN
다시 시작 끝남 완전히 끝남. 다음 기동은 완전 부팅 위와 같음
정전 즉시 사라짐 즉시 사라짐 없음

로그아웃과 종료는 앱의 관점에서 거의 같은 이벤트입니다. WM_QUERYENDSESSION의 lParam에 ENDSESSION_LOGOFF 비트가 서 있으면 로그아웃이고, 0이면 종료 또는 다시 시작입니다(둘은 구별할 수 없습니다).1「로그아웃일 뿐이니 괜찮을 것이다」는 안이함은 성립하지 않고, 같은 정리 코드가 호출되는 설계가 정답입니다.

끝나는 네 가지 방법과 앱으로의 알림로그아웃, 종료, 다시 시작은 WM_QUERYENDSESSION에서 WM_ENDSESSION 알림을 주고 정리는 수 초 안에 끝낸다. 정전만 알림이 전혀 없으므로 8장의 쓰기 설계와 UPS로 대비한다로그아웃QUERY → ENDSESSION종료다시 시작정전알림 없음: 쓰기 + UPS수 초의 정리

그림 1: 로그아웃, 종료, 다시 시작은 WM_QUERYENDSESSION에서 WM_ENDSESSION 알림을 주고 정리는 수 초 안에 끝낸다. 정전만 알림이 전혀 없으므로 8장의 쓰기 설계와 UPS로 대비한다.

2.2. 「종료해도 안 나았다」의 진짜 이유 ── 하이브리드 종료

놓치기 쉬운 행은 표의 둘째입니다. Windows 8 이후 클라이언트 OS에서는 최대 절전을 지원하는 PC에서 빠른 시작(하이브리드 종료)이 기본으로 켜져 있고, 「종료」의 동작이 바뀌었습니다. 사용자 세션의 로그아웃은 여전하지만, 커널 세션은 닫히지 않고, 장치 드라이버까지 포함해 최대 절전 파일(hiberfil.sys)에 저장되어 다음 기동에서 그대로 복원됩니다. 기동은 빨라지지만, 전원을 잘라도 커널과 드라이버 상태는 살아남습니다.2 다만 이것은 조건부 동작입니다. 최대 절전 자체가 꺼진 환경(powercfg /hibernate off), 정책이나 전원 옵션에서 빠른 시작을 끈 환경, Windows Server에서는 종료가 종래의 완전 종료입니다. 해당 PC가 어느 쪽인지는 전원 옵션의 「빠른 시작 켜기」 확인란, 또는 powercfg /a(사용 가능한 절전 상태)에 「Fast Startup」이 나오는지로 알 수 있습니다.

종료 조작에서 커널에 일어나는 일종료 조작은 빠른 시작이 켜져 있는지에 따라 완전 종료 또는 커널 최대 절전으로 갈리고, 다시 시작은 항상 완전 부팅을 한다빠른 시작 켜짐최대 절전 끔 / Server종료다시 시작세션 종료 + 커널 최대 절전완전 종료다음: 커널 복원다음: 완전 부팅

그림 2: 종료 조작은 빠른 시작이 켜져 있는지에 따라 완전 종료 또는 커널 최대 절전으로 갈리고, 다시 시작은 항상 완전 부팅을 한다.

반면 「다시 시작」은 항상 완전한 부팅 주기를 돌립니다. 예를 들어 드라이버 업데이트 뒤에는 완전히 새로운 상태가 필요합니다.2 여기서 현장에서 듣는 여러 현상이 맞아떨어집니다.

  • 「종료했다가 다시 켰는데 장치 문제가 안 없어졌다」── 커널과 드라이버는 최대 절전에서 복원만 되었고, 리셋되지 않았다
  • 「다시 시작하니 나았다」── 완전 부팅이 초기화했기 때문
  • 장치 PC 장애 절차에는 「껐다 켜기」가 아니라 「다시 시작」을 써야 한다

명령줄에서 완전 종료를 명시하려면 shutdown /s(Shutdown.exe의 기본은 완전 종료), 기본 하이브리드 동작을 원하면 shutdown /s /hybrid입니다.2 빠른 시작을 끄는 것은 권장되지 않습니다. 앱 측은 「종료 때 커널은 최대 절전만 할 수 있다」고 가정하고── 예를 들어 OS 부팅 시각으로 「누적 가동 시간」을 추정하지 말고── 어느 쪽이든 깨지지 않게 설계합니다(빠른 시작의 켜짐/꺼짐은 환경마다 다릅니다).

3. GUI 앱이 취해야 할 동작 ── WM_QUERYENDSESSION과 WM_ENDSESSION

3.1. 두 메시지가 일을 나누는 방식

창과 메시지 큐가 있는 앱은 세션 종료를 두 단계로 알림받습니다.1

  1. WM_QUERYENDSESSION ── 「끝나도 되는가?」라는 쿼리. 앱은 즉시 TRUE를 돌려줘야 하며, DefWindowProc의 기본 응답도 TRUE입니다. 여기서 정리를 시작하지 마십시오.
  2. WM_ENDSESSION(wParam=TRUE) ── 「세션이 정말로 끝난다」는 확정 알림. 정리는 여기서 합니다.

WM_QUERYENDSESSION에 FALSE를 돌려주면 종료를 중단할 수 있지만, 문서는 「TRUE를 돌려주고 사용자의 의도를 존중해야 한다」고 명시하며, FALSE를 돌려준 앱은 「종료를 막고 있는 앱」으로 전체 화면 UI에 노출됩니다. 콘솔 앱과 보이는 창이 없는 앱은 애초에 종료를 중단할 수 없고, 5초 안에 응답하지 않으면 자동으로 종료됩니다.14

두 단계 세션 종료 알림의 흐름WM_QUERYENDSESSION 쿼리에 TRUE를 돌려주면 WM_ENDSESSION으로 확정되고 정리가 돈다. FALSE로 거부하면 앱이 종료를 막는 것으로 표시되고, 약 5초의 무응답은 강제 계속될 수 있다TRUE(원칙)FALSE(거부)무응답 약 5초강제 계속취소WM_QUERYENDSESSIONWM_ENDSESSION(확정)종료를 막는 것으로 표시행으로 취급정리는 여기서프로세스 종료종료 중단

그림 3: WM_QUERYENDSESSION 쿼리에 TRUE를 돌려주면 WM_ENDSESSION으로 확정되고 정리가 돈다. FALSE로 거부하면 앱이 종료를 막는 것으로 표시되고, 약 5초의 무응답은 강제 계속될 수 있다.

3.2. 응답하지 않으면 일어나는 일 ── 5초의 벽

WM_QUERYENDSESSION과 WM_ENDSESSION 모두, 응답을 약 5초 늦출 수 있습니다. 넘으면 시스템이 「이 앱이 종료를 막고 있습니다」화면을 표시하고, 사용자는 강제 계속(= 앱 강제 종료)을 고를 수 있습니다.4 강제 종료된 프로세스에는 저장을 끝낼 두 번째 기회가 없습니다.

따라서 설계 포인트는 둘입니다.

  • 정리를 5초 안에 끝나는 양으로 둡니다. Microsoft 자신도 평소 운용에서 데이터를 자주 저장해 종료 때 저장할 것을 줄이고, 저장하지 않은 데이터를 임시 위치에 남겨 다음 기동에서 복원하라고 권합니다.1
  • 종료 중에 확인 대화상자를 띄우지 마십시오. 「저장하시겠습니까?」에서 기다리는 동안 5초가 지나갑니다. 조용히 안전한 쪽(자동 저장)으로 떨어지십시오.

3.3. WinForms와 WPF에서의 구현

.NET 데스크톱 앱에서는 이 메시지가 프레임워크 이벤트로 번역됩니다. WinForms에서는 FormClosing이 일어나며, CloseReason으로 종료가 원인인지 알 수 있습니다.

// WinForms: FormClosing is also raised on shutdown / sign-out
private void MainForm_FormClosing(object sender, FormClosingEventArgs e)
{
    if (e.CloseReason == CloseReason.WindowsShutDown)
    {
        // Do only an idempotent snapshot save. Do not show a dialog.
        // Do not set e.Cancel = true (refuse) either.
        SaveWorkingStateToTempFile();
        return;
    }

    // For ordinary closes such as the user clicking the × button, you may confirm here
}

WPF에서는 Application.SessionEnding 이벤트(XAML의 SessionEnding 특성, 또는 OnSessionEnding 오버라이드)가 대응합니다.

WinForms/WPF 이벤트와 메시지의 대응WM_QUERYENDSESSION의 쿼리 단계는 WinForms FormClosing과 WPF SessionEnding에 대응하고, 거기서 하는 일은 멱등한 스냅샷 저장이 한도이다. 확정된 WM_ENDSESSION에 대응하는 이벤트는 없으므로 WndProc이나 훅으로 받아 확정 뒤에만 할 수 있는 정리를 한다WM_QUERYENDSESSIONWinForms: FormClosingWPF: SessionEnding멱등한 스냅샷만WM_ENDSESSION이벤트 없음: WndProc 훅확정 뒤의 정리

그림 4: WM_QUERYENDSESSION의 쿼리 단계는 WinForms FormClosing과 WPF SessionEnding에 대응하고, 거기서 하는 일은 멱등한 스냅샷 저장이 한도이다. 확정된 WM_ENDSESSION에 대응하는 이벤트는 없으므로 WndProc이나 훅으로 받아 확정 뒤에만 할 수 있는 정리를 한다.

// WPF: App.xaml.cs
protected override void OnSessionEnding(SessionEndingCancelEventArgs e)
{
    base.OnSessionEnding(e);

    // You can distinguish ReasonSessionEnding.Logoff / Shutdown,
    // but the baseline is to run the same snapshot save in either case
    SaveWorkingStateToTempFile();

    // Do not set e.Cancel = true unless you have an exceptional reason
}

여기에 주의가 하나 있습니다. FormClosing(CloseReason.WindowsShutDown)과 WPF의 SessionEnding은 모두 쿼리 단계(WM_QUERYENDSESSION)에 대응합니다. 다른 앱이 거부하면 종료가 중단되고 앱은 계속 돕니다. 따라서 이 이벤트에서 해도 되는 것은 종료가 중단되어도 해가 없고 몇 번 돌려도 같은 결과가 되는 멱등한 스냅샷 저장입니다. 「정말로 끝날 때만 해야 하는 정리」(연결 끊기, 자원 반납 등)가 필요하면, 확정된 WM_ENDSESSION(wParam=TRUE)을 WndProc에서 직접 훅해 거기서 합니다.

어느 경로든 본체를 공통 「스냅샷 저장」함수에 모으고, 정상 종료, 종료, (가능하면) 크래시의 복원 데이터를 같은 형식으로 쓰면, 다음 기동의 복원 로직이 하나의 경로가 됩니다. 크래시에서도 정보를 남기는 설계는 「Windows 앱이 프로그램 실수에 의한 예외로 떨어져도 확실히 로그를 남기려면」에서 다룹니다.

4. 정말로 막아야 할 때 ── ShutdownBlockReasonCreate

CD 쓰기나 펌웨어처럼 도중에 잘리면 물리적으로 깨지는 작업은 예외입니다. 여기서의 올바른 실무는 중단할 수 없는 작업이 시작할 때 ShutdownBlockReasonCreate로 이유 문자열을 등록하고, 끝나면 즉시 ShutdownBlockReasonDestroy를 호출하는 것입니다. 종료가 요청되면 그 이유가 「이 앱이 종료를 막고 있습니다」화면에 표시되고, 사용자는 계속할지 취소할지를 결정할 수 있습니다.3

ShutdownBlockReasonCreate로 보호하는 흐름중단할 수 없는 작업이 시작할 때 이유를 등록하고, 보호 중에 종료 요청이 오면 이유가 전체 화면에 나오며 WM_QUERYENDSESSION을 FALSE로 거부한다. 사용자는 취소하거나 강제 계속할 수 있고, 작업이 끝나면 이유를 지운다취소강제 계속중단 불가 작업 시작ShutdownBlockReasonCreate워커 스레드에서 실행종료: Destroy이 중의 종료이유 표시 + FALSE프로세스 종료

그림 5: 중단할 수 없는 작업이 시작할 때 이유를 등록하고, 보호 중에 종료 요청이 오면 이유가 전체 화면에 나오며 WM_QUERYENDSESSION을 FALSE로 거부한다. 사용자는 취소하거나 강제 계속할 수 있고, 작업이 끝나면 이유를 지운다.

[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern bool ShutdownBlockReasonCreate(IntPtr hWnd, string pwszReason);

[DllImport("user32.dll", SetLastError = true)]
static extern bool ShutdownBlockReasonDestroy(IntPtr hWnd);

// Call from the thread that created the main window (it fails from other threads)
_criticalOperationInProgress = true;
ShutdownBlockReasonCreate(this.Handle, "Writing measurement data to a file");
try
{
    // Run the uninterruptible operation on a worker thread. If you run it
    // synchronously on the UI thread the message pump stops, and the process
    // is force-continued as "Not Responding" before the WM_QUERYENDSESSION
    // refusal code below can run
    await Task.Run(() => WriteMeasurementData());
}
finally
{
    ShutdownBlockReasonDestroy(this.Handle);
    _criticalOperationInProgress = false;
}

// Also, refuse WM_QUERYENDSESSION with FALSE only while protected
protected override void WndProc(ref Message m)
{
    const int WM_QUERYENDSESSION = 0x0011;
    if (m.Msg == WM_QUERYENDSESSION && _criticalOperationInProgress)
    {
        m.Result = IntPtr.Zero;   // Refuse. The registered reason string is shown in the full-screen UI
        return;
    }
    base.WndProc(ref m);
}

여기서 쉬운 오해는 역할 분담입니다. ShutdownBlockReasonCreate가 하는 일은 이유 문자열을 등록하는 것뿐이며, 그 자체가 종료를 멈추지 않습니다. 실제로 종료를 붙잡는 것은 위와 같이 보호 플래그가 서 있는 동안 WM_QUERYENDSESSION에 FALSE를 돌려주는 자기 처리입니다. 둘을 세트로 쓰고, 작업이 끝나면 둘 다 즉시 지웁니다. 또한 보호하는 작업 자체는 워커 스레드에서 돌려 UI 스레드가 메시지를 처리할 수 있게 두십시오── 거부 메커니즘은 메시지가 도착해야 동작합니다(그리고 그때도 사용자와 OS는 강제 계속할 수 있으므로, 「멈추지 않았을 때」에도 깨지지 않는 쓰기 설계── 8장──는 여전히 필요합니다).

운용상의 주의는 셋입니다.

  • 이유 문자열은 짧고 구체적으로. 사용자는 급하고 몇 초만 읽습니다. 문서 자체도 「Burning a CD」를 적절한 예로 듭니다.3
  • 앱 수명 전체 동안 등록해 두지 마십시오. 「중단할 수 없는 작업이 진행 중일 때만」이 API가 가정하는 것입니다.
  • 막을 수 있다고 가정하고 설계하지 마십시오. 사용자는 강제 계속을 고를 수 있고, 강제 종료(ENDSESSION_CRITICAL)는 애초에 기다리지 않습니다. 문서는 명시합니다. 「Applications should not depend on being able to block shutdown」.4

5. 콘솔 앱과 백그라운드 프로세스가 취해야 할 동작

5.1. SetConsoleCtrlHandler와 짧은 유예

콘솔 앱은 창 메시지를 받을 수 없으므로, SetConsoleCtrlHandler로 등록한 핸들러 함수에 제어 시그널이 도착합니다. 시그널별 기본 유예는 다음과 같습니다.5

시그널 일어나는 때 기본 유예
CTRL_C_EVENT / CTRL_BREAK_EVENT Ctrl+C / Ctrl+Break 타임아웃 없음
CTRL_CLOSE_EVENT 콘솔 닫기, 작업 관리자 「작업 끝내기」(「세부 정보」탭에서의 강제 프로세스 종료는 알림 없는 즉시 종료이며 이 표 밖) 약 5초
CTRL_SHUTDOWN_EVENT 시스템 종료(서비스 프로세스) 약 20초

주의할 점은 둘입니다. 첫째, CTRL_LOGOFF_EVENT와 CTRL_SHUTDOWN_EVENT를 받을 수 있는 것은 본질적으로 서비스로 도는 프로세스뿐입니다. 대화형 세션의 앱은 로그아웃 때 종료되므로, 이 시그널을 기다리는 설계는 성립하지 않습니다.5 둘째, gdi32.dll 또는 user32.dll을 로드한 프로세스는 콘솔 앱이라고 생각해도 Windows 앱으로 취급되어, LOGOFF/SHUTDOWN 핸들러가 호출되지 않습니다. 공식 우회는 숨은 창을 만들어 WM_QUERYENDSESSION/WM_ENDSESSION을 받는 것입니다.6

콘솔 시그널별 유예Ctrl+C와 Ctrl+Break에는 명시적 타임아웃이 없고, 콘솔 닫기는 약 5초, 서비스 프로세스로의 종료 시그널은 약 20초이며, 넘으면 프로세스를 강제 종료한다타임아웃 없음약 5초약 20초CTRL_C / BREAKHandlerRoutine 정리CTRL_CLOSECTRL_SHUTDOWN유예 뒤 강제 종료

그림 6: Ctrl+C와 Ctrl+Break에는 명시적 타임아웃이 없고, 콘솔 닫기는 약 5초, 서비스 프로세스로의 종료 시그널은 약 20초이며, 넘으면 프로세스를 강제 종료한다.

// Console app: clean up on Ctrl+C and console close
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool SetConsoleCtrlHandler(HandlerRoutine handler, bool add);

delegate bool HandlerRoutine(int ctrlType);   // 2 = CTRL_CLOSE_EVENT

static readonly HandlerRoutine s_handler = OnCtrlEvent;  // Keep a reference so GC does not collect it

static bool OnCtrlEvent(int ctrlType)
{
    // Do only cleanup that finishes within 5 seconds
    FlushAndCloseDataFile();
    return false;   // Proceed to the default handler; the process exits
}

static void Main()
{
    SetConsoleCtrlHandler(s_handler, add: true);
    // ...
}

5.2. .NET의 함정 ── ProcessExit에 의존하지 말 것

.NET에서는 오래전부터 「AppDomain.ProcessExit에서 정리하면 된다」는 정석 패턴이 있었지만, .NET 10부터 런타임은 기본 종료 시그널 핸들러를 제공하지 않고, CTRL_CLOSE_EVENT/CTRL_SHUTDOWN_EVENT에서 ProcessExit도 AssemblyLoadContext.Unloading도 일어나지 않습니다. OS 기본 핸들러가 프로세스를 즉시 종료합니다.7

.NET 10에서 ProcessExit가 바뀐 방식.NET 9까지는 런타임의 기본 시그널 핸들러가 종료 시그널을 받아 ProcessExit를 일으킨 뒤 종료했다. .NET 10부터 런타임은 기본 핸들러를 제공하지 않고 OS 기본 처리가 프로세스를 즉시 종료하므로, 핸들러를 직접 등록한다CTRL_CLOSE / SHUTDOWN.NET 9까지: ProcessExit.NET 10부터: 즉시 종료핸들러를 직접 등록

그림 7: .NET 9까지는 런타임의 기본 시그널 핸들러가 종료 시그널을 받아 ProcessExit를 일으킨 뒤 종료했다. .NET 10부터 런타임은 기본 핸들러를 제공하지 않고 OS 기본 처리가 프로세스를 즉시 종료하므로, 핸들러를 직접 등록한다.

대신 각 앱 모델의 정석 경로로 옮깁니다.

  • GUI 앱: 앞 장의 FormClosing / SessionEnding
  • Generic Host(Worker Service 포함): IHostApplicationLifetime과 BackgroundService.StopAsync. HostOptions.ShutdownTimeout으로 중지 유예를 명시
  • 맨 콘솔 앱: SetConsoleCtrlHandler(또는 PosixSignalRegistration으로 SIGINT/SIGTERM 상당을 구독)
앱 모델별로 종료 알림을 받는 곳GUI 앱은 FormClosing과 SessionEnding에 더해 확정 작업용 WM_ENDSESSION 훅을 쓰고, Generic Host는 IHostApplicationLifetime과 StopAsync를 쓰며, 맨 콘솔 앱은 SetConsoleCtrlHandler 또는 PosixSignalRegistration을 쓴다. ProcessExit에 의존하면 외부 시그널 경로에서 일어나지 않는다GUIGUI 아님Host콘솔어느 앱 모델인가?FormClosing / SessionEndingHost인가 콘솔인가?ENDSESSION 훅Lifetime + StopAsyncSetConsoleCtrlHandlerShutdownTimeout을 설정ProcessExit에 의존하지 말 것

그림 8: GUI 앱은 FormClosing과 SessionEnding에 더해 확정 작업용 WM_ENDSESSION 훅을 쓰고, Generic Host는 IHostApplicationLifetime과 StopAsync를 쓰며, 맨 콘솔 앱은 SetConsoleCtrlHandler 또는 PosixSignalRegistration을 쓴다. ProcessExit에 의존하면 외부 시그널 경로에서 일어나지 않는다.

유예는 경로마다 다릅니다── GUI와 콘솔 닫기는 약 5초, 서비스는 6장의 SCM 유예(약 20초, 또는 PRESHUTDOWN의 설정값), Ctrl+C는 명시적 타임아웃 없음. 다만 어느 경로든 유예는 한정되어 믿을 수 없으므로, 설계의 축은 「종료 이벤트에서 열심히 한다」가 아니라 「이미 각 처리 체크포인트에서 저장해 둔다」가 정상이라는 것입니다.

6. Windows 서비스가 취해야 할 동작 ── SHUTDOWN과 PRESHUTDOWN

6.1. 두 종류의 종료 알림

서비스는 로그아웃의 영향을 받지 않지만, 종료와 다시 시작에서는 중지됩니다. 알림은 Service Control Manager(SCM)의 제어 코드로 도착하며, 받으려면 수락 플래그를 선언해야 합니다.8

선언 도착하는 알림 시점과 유예
SERVICE_ACCEPT_SHUTDOWN SERVICE_CONTROL_SHUTDOWN 종료 처리 중에 알림. 기본 약 20초, 상한은 WaitToKillServiceTimeout
SERVICE_ACCEPT_PRESHUTDOWN SERVICE_CONTROL_PRESHUTDOWN SHUTDOWN보다 먼저 알림. SCM은 서비스가 멈추거나 타임아웃까지 기다림
서비스로의 종료 알림 순서종료가 시작되면 PRESHUTDOWN을 선언한 서비스에 설정된 유예와 함께 먼저 알림이 가고, 이어서 SHUTDOWN 알림이 기본 약 20초와 함께 가며, 유예가 끝나면 프로세스가 종료된다종료 시작PRESHUTDOWN(선언한 경우)SHUTDOWN(약 20초)유예 만료 → 종료

그림 9: 종료가 시작되면 PRESHUTDOWN을 선언한 서비스에 설정된 유예와 함께 먼저 알림이 가고, 이어서 SHUTDOWN 알림이 기본 약 20초와 함께 가며, 유예가 끝나면 프로세스가 종료된다.

PRESHUTDOWN 타임아웃은 ChangeServiceConfig2(SERVICE_CONFIG_PRESHUTDOWN_INFO)로 설정할 수 있으며, 기본은 Windows 10 Creators Update(빌드 15063) 이후 10초, 그 이전은 3분입니다.9 「PRESHUTDOWN이면 3분」이라는 옛 지식으로 일하고 있으면, 현재 OS에서는 기대한 유예의 1/18뿐입니다. 또한 PRESHUTDOWN은 그 구간 동안 시스템 전체 종료를 붙잡으므로, 문서도 「특수한 상황에서만 써야 한다」고 말합니다.8

핸들러 측 실무도 중요합니다. 제어 핸들러는 30초 안에 돌아와야 하며, 시간이 걸리는 중지 작업은 다른 스레드에 맡기고 SERVICE_STOP_PENDING을 보고한 뒤 즉시 돌아오십시오.8

// Win32 service: accept PRESHUTDOWN and leave stop work to a worker
g_status.dwControlsAccepted = SERVICE_ACCEPT_STOP | SERVICE_ACCEPT_PRESHUTDOWN;

DWORD WINAPI ServiceHandlerEx(DWORD control, DWORD, LPVOID, LPVOID)
{
    switch (control)
    {
    case SERVICE_CONTROL_PRESHUTDOWN:
    case SERVICE_CONTROL_STOP:
        ReportStatus(SERVICE_STOP_PENDING, /*waitHintMs*/ 3000);
        SetEvent(g_stopEvent);   // Tell the worker to stop and return immediately
        return NO_ERROR;
    }
    return ERROR_CALL_NOT_IMPLEMENTED;
}

// Worker side: if cleanup runs longer than waitHint, keep reporting
// SERVICE_STOP_PENDING periodically while incrementing dwCheckPoint.
// The SCM judges "still alive and making progress" from waitHint and
// checkpoint advance. If reporting stops it can be treated as hung and
// shutdown can proceed. Always report SERVICE_STOPPED when finished

6.2. 유예에 의존하지 않는 설계

유예 상한 WaitToKillServiceTimeout을 서비스 측에서 고쳐 늘리는 것은 명시적으로 권장되지 않습니다. 문서가 요구하는 것은 반대입니다── 서비스는 정리를 가능한 한 빨리 끝내, UPS 전원 기기가 배터리가 죽기 전에 종료를 완료할 수 있게 해야 합니다. 안내는 평소 운용에서 자주 저장해 미저장 데이터를 최소화하고, 종료 때 메모리 해방에 시간을 쓰지 않으며, 네트워크 상대에 알릴 때 응답을 너무 오래 기다리지 말라는 것입니다. 또한 종료 때 SCM은 기본적으로 의존성을 고려하지 않으므로, 중지 처리는 「의존하는 서비스가 이미 내려간 뒤에도」 동작해야 합니다.8

유예에 의존하지 않는 중지 처리 설계각 처리 체크포인트에서 저장해 미저장 데이터가 항상 최소이면, 중지 알림이 왔을 때의 정리는 수 초 안에 끝난다. 종료 때 전부를 저장하는 설계는 유예에 들어가지 못하고, 강제 종료가 데이터를 잃는다각 체크포인트에서 저장중지 → 남은 조각 저장 → 끝종료 때 전부를 저장중지 → 저장이 유예를 놓침강제 종료 → 데이터 손실

그림 10: 각 처리 체크포인트에서 저장해 미저장 데이터가 항상 최소이면, 중지 알림이 왔을 때의 정리는 수 초 안에 끝난다. 종료 때 전부를 저장하는 설계는 유예에 들어가지 못하고, 강제 종료가 데이터를 잃는다.

.NET Worker Service(UseWindowsService)에서는 SERVICE_CONTROL_STOP과 SHUTDOWN이 호스트 중지로 번역되어 BackgroundService.StopAsync가 호출됩니다. 현재 작성 시점의 정석 구현은 STOP/SHUTDOWN 계열을 받으며, PRESHUTDOWN도 필요하면 확장 핸들러가 필요합니다. 어느 쪽이든 HostOptions.ShutdownTimeout을 명시하고 StopAsync를 수 초 안에 끝내십시오. 서비스 구축 전반은 「Windows 서비스 만드는 방법과 운영」을 보십시오.

7. 다시 시작 뒤에 자동으로 복구하기

장치 PC나 무인 PC에서는 설계 범위가 「종료를 견디는 것」뿐 아니라 「다시 시작 뒤에 스스로 돌아오는 것」입니다.

7.1. RegisterApplicationRestart와 복구 콜백

RegisterApplicationRestart를 호출해 두면, 앱은 크래시(처리되지 않은 예외), 응답 없음, 업데이트에 의한 앱 다시 시작, 업데이트에 의한 OS 다시 시작의 다시 시작 후보로 등록됩니다. 다시 시작용 명령줄 인수를 등록할 수 있으므로, 「어느 파일이 열려 있었는지」와 「어느 복원 지점인지」를 넣으면 다시 시작 뒤 끊긴 곳에서 재개할 수 있습니다.10

받아들일 사양은 다음과 같습니다.10

  • 등록은 문제가 일어나기 전에 끝내야 합니다(업데이트 시나리오에서 WM_QUERYENDSESSION 처리 중이 마지막 기회)
  • 다시 시작 루프를 막기 위해 60초 미만 가동한 프로세스는 다시 시작되지 않습니다
  • 상승된 권한으로 도는 프로세스는 자동 다시 시작 후보가 아닙니다(상승 동의 없이 프로세스를 다시 만들 수 없습니다). 상승이 필요한 앱의 자동 복구는 UI를 표준 권한에 두고 권한 작업을 서비스로 분리하거나, 작업 스케줄러의 「가장 높은 권한으로 실행」같은 명시적 기동 경로로 설계합니다
  • 크래시나 행 뒤의 다시 시작은 사용자 동의를 거치고, 업데이트 뒤의 다시 시작은 자동입니다
  • OS 다시 시작을 넘어 복구하려면, 다시 시작을 요청하는 쪽(설치 프로그램 등)이 EWX_RESTARTAPPS / SHUTDOWN_RESTARTAPPS 플래그로 종료 API를 호출해야 합니다

RegisterApplicationRecoveryCallback도 등록하면, 크래시 때 WER(Windows Error Reporting)이 콜백을 호출해 진행 중 데이터를 저장할 유예를 줍니다. 다만 저장이 오래 걸리면 등록 때 지정한 ping 간격 안에 ApplicationRecoveryInProgress를 계속 호출하지 않으면 복구 작업이 도중에 잘립니다. 저장이 끝나면 ApplicationRecoveryFinished로 완료를 알립니다. 앱 업데이트 때 「사용 중인 파일을 교체하고 다시 시작」은 Restart Manager의 영역이며, 「사용 중인 exe나 DLL을 어떻게 교체할까」에서 자세히 다룹니다.

7.2. ARSO ── 업데이트 다시 시작 뒤의 자동 사인인

Windows Update 다시 시작 뒤에 아무도 로그인하지 않으면, 사용자 세션 앱은 돌아오지 않습니다. 그 틈을 메우는 것이 ARSO(Winlogon Automatic Restart Sign-On)입니다. Windows Update가 다시 시작을 시작하면 마지막 대화형 사용자의 자격 증명을 안전하게 저장하고 Autologon을 구성한 뒤, 다시 시작 뒤에 그 사용자를 자동으로 로그인하고 화면을 잠급니다.11 다시 시작과 등록된 앱 재개를 요청하는 shutdown /g 같은 명령도 있습니다. 일부 환경은 조직 정책(DisableAutomaticRestartSignOn 등)으로 이것을 끄므로, 무인 복구를 설계할 때는 이 설정을 세트로 확인하십시오. 그리고 항상 필요한 백그라운드 작업을 사용자 세션의 자동 기동에 의존하고 있다면, 처음부터 Windows 서비스로 만드는 것이 올바른 수입니다.

다시 시작 뒤에 앱이 자동으로 복구하는 경로문제가 일어나기 전에 RegisterApplicationRestart로 등록하면, 크래시나 응답 없음에서는 사용자 동의 뒤에, 업데이트에 의한 다시 시작에서는 ARSO 자동 사인인과 화면 잠금 뒤에 앱이 다시 시작된다. 가동 60초 미만과 상승된 프로세스는 대상 밖이다동의RegisterApplicationRestart크래시 또는 행업데이트 다시 시작앱 다시 시작ARSO 사인인 + 잠금해당 없음: 60초 미만 / 상승됨

그림 11: 문제가 일어나기 전에 RegisterApplicationRestart로 등록하면, 크래시나 응답 없음에서는 사용자 동의 뒤에, 업데이트에 의한 다시 시작에서는 ARSO 자동 사인인과 화면 잠금 뒤에 앱이 다시 시작된다. 가동 60초 미만과 상승된 프로세스는 대상 밖이다.

8. 알림이 없는 정전을 견디기 ── 쓰기 설계와 UPS

8.1. 「언제 잘려도 깨지지 않는」 쓰기 ── 임시 파일 + ReplaceFile

차단기가 떨어지거나, 전원 장치가 고장 나거나, 플러그가 뽑히면 WM_ENDSESSION도 PRESHUTDOWN도 오지 않습니다. 설정이나 계측 결과를 「원본 파일을 제자리에서 덮어쓰는」 한, 쓰기 도중의 정전은 옛것과 새것이 섞인 깨진 파일을 남길 수 있습니다.

표준 패턴은 같은 볼륨의 임시 파일에 완전히 쓴 뒤 바꾸는 것입니다. ReplaceFile은 「새 파일에 저장 → 원본을 옆으로 치움 → 이름 변경 → 삭제」의 순서를 하나의 API로 묶고, 원본 파일의 생성 시각, ACL, 대체 스트림 같은 특성도 이어받습니다(세 파일은 같은 볼륨에 있어야 합니다).12 .NET의 File.Replace는 이것을 그대로 호출합니다.

임시 파일과 ReplaceFile의 저장·복구 흐름저장 때 임시 파일에 완전히 쓰고 플러시한 뒤 ReplaceFile로 바꿔 옛 내용을 .bak에 남긴다. 다음 기동에서 주 파일을 검증하고 깨져 있으면 .bak로 떨어진다다음 기동저장 때온전깨짐어느 단계의 정전이든주 파일을 검증그대로 씀.bak로 떨어짐디스크로 플러시완전한 임시 파일을 씀ReplaceFile → .bak

그림 12: 저장 때 임시 파일에 완전히 쓰고 플러시한 뒤 ReplaceFile로 바꿔 옛 내용을 .bak에 남긴다. 다음 기동에서 주 파일을 검증하고 깨져 있으면 .bak로 떨어진다.

// The standard pattern for settings and data: write completely to a temporary file, then swap, and keep the old contents
public static void SaveAtomically(string path, string content)
{
    string dir = Path.GetDirectoryName(path)!;
    string tmp = Path.Combine(dir, Path.GetRandomFileName());  // Create it on the same volume

    try
    {
        using (var fs = new FileStream(tmp, FileMode.CreateNew, FileAccess.Write))
        using (var writer = new StreamWriter(fs))
        {
            writer.Write(content);
            writer.Flush();
            fs.Flush(flushToDisk: true);   // FlushFileBuffers equivalent. Write the OS buffer
                                           // out to disk (device-side cache limits are in 8.2)
        }

        if (File.Exists(path))
            File.Replace(tmp, path, path + ".bak");  // Calls ReplaceFile. Keep the old contents as .bak
        else
            File.Move(tmp, path);
    }
    catch
    {
        // If we fail mid-way, do not leave the temporary file. Repeated failures
        // of a periodic save would fill the volume with complete copies
        try { File.Delete(tmp); } catch { /* Prefer the original exception if delete fails */ }
        throw;
    }
}

이렇게 하면 보통 운용에서는 항상 「완전한 옛 파일」 또는 「완전한 새 파일」을 읽을 수 있게 남습니다. 다만 ReplaceFile은 여러 단계의 네임스페이스 조작이며, 정전을 넘는 원자성은 사양으로 보장되지 않습니다. 그래서 위 예는 백업(.bak)을 남깁니다── 읽기 측이 기동 때 주 파일을 검증하고 깨져 있으면 백업으로 떨어지는 것이 세트입니다. 추가 전용 로그나 CSV에는 이것을 쓸 수 없으므로, 「한 줄 = 한 레코드이고, 읽기 때 깨진 마지막 줄을 버린다」처럼 깨짐을 전제로 한 형식을 씁니다.

8.2. WriteFile의 성공은 디스크 도착이 아니다

다른 전제는 WriteFile이 성공을 돌려줘도 데이터는 아직 OS 캐시에만 있을 수 있다는 것입니다. Windows는 파일 읽기·쓰기를 시스템 버퍼에 두고, 지연 쓰기로 주기적으로 디스크에 반영합니다. 데이터를 확실히 디스크에 보내려면 FlushFileBuffers로 명시적으로 플러시하거나, CreateFile 때 FILE_FLAG_WRITE_THROUGH를 지정해 각 쓰기가 캐시를 통과하게 합니다. 파일 시스템 메타데이터는 항상 캐시되므로, 메타데이터 확인에도 플러시 또는 write-through가 필요합니다.13

다만 FlushFileBuffers를 매번 호출하는 것은 비효율이며, 문서도 잦은 호출 대신 FILE_FLAG_NO_BUFFERING+WRITE_THROUGH를 고려하라고 권합니다.13 실무에서는 「트랜잭션 체크포인트나 파일을 닫기 직전에만 플러시」가 현실적인 타협입니다. 이 층의 구조── 캐시 관리자, 지연 쓰기, 하드웨어 캐시 때문에 「플러시했는데도 디스크에 안 갔을 수 있다」는 점──는 「캐시 관리자: WriteFile은 언제 디스크에 도달하는가?」에서 깊게 다룹니다.

8.3. UPS와 배터리 감시 ── 정전을 종료로 바꾸기

장치 PC에서 정전의 진짜 대책은 UPS입니다. UPS의 역할을 「정전을 막는 것」이 아니라 「알림 없는 정전」을 「알림 있는 계획 종료」로 바꾸는 것으로 생각하십시오. 설계는 2단계 구성입니다.

  1. 유예 설계: UPS 배터리 유지 시간 > 「배터리 전환 감지 → 앱·서비스 정리 → OS 종료 완료」의 합. 서비스 중지 처리가 느리면 이 식이 성립하지 않습니다(6.2절)
  2. 감지: AC에서 배터리로의 전환과 잔량 저하는 PBT_APMPOWERSTATUSCHANGE 이벤트로 알림됩니다. 창이 있는 앱은 WM_POWERBROADCAST로 받고, 창이 없는 서비스는 SERVICE_ACCEPT_POWEREVENT를 선언하고 HandlerEx에서 SERVICE_CONTROL_POWEREVENT로 받습니다(WM_POWERBROADCAST는 서비스 제어 핸들러에 오지 않습니다). 받으면 GetSystemPowerStatus를 호출해 ACLineStatus(AC인지)와 BatteryLifePercent를 확인하고, 계측 중단·저장·종료 요청으로 이끕니다.15
UPS로 정전을 계획 종료로 바꾸는 흐름정전으로 UPS가 배터리로 바뀌면 PBT_APMPOWERSTATUSCHANGE가 알림되고, 전원 상태를 확인한 뒤 저장과 종료 요청으로 알림 없는 정전을 알림 있는 계획 종료로 바꾼다정전UPS가 배터리로PBT_APMPOWERSTATUSCHANGEGetSystemPowerStatus중단하고 저장종료를 요청통상의 알림 흐름(3–6)

그림 13: 정전으로 UPS가 배터리로 바뀌면 PBT_APMPOWERSTATUSCHANGE가 알림되고, 전원 상태를 확인한 뒤 저장과 종료 요청으로 알림 없는 정전을 알림 있는 계획 종료로 바꾼다.

전형적인 USB 연결 UPS는 Windows에 배터리로 보이므로, 이 표준 API로 감지할 수 있습니다. 벤더 관리 소프트웨어에 「잔량 N%에서 OS를 종료」기능이 있으면, 그 임계값이 앱의 정리 시간과 맞는지 함께 확인하십시오. 절전·최대 절전에서의 재개와 장시간 가동 문제는 별도의 축이며, 「슬립・최대 절전 모드・Modern Standby와 장시간 가동 앱」에서 다룹니다.

9. 검증하는 법 ── 종료를 안전하게 시험하기

종료 처리는 「썼지만 운영 상당 조건에서 한 번도 시험하지 않았다」가 되기 쉽습니다. 안전하게 검증하는 절차를 두십시오.

  • 테스트 기기나 VM에서 시험합니다: 운영 장치 PC에서 먼저 시험하지 마십시오. Hyper-V 검사점(스냅샷)이 있는 테스트 환경에서 종료, 다시 시작, 강제 정전(VM 전원 끄기)을 반복합니다. 다만 VM 「전원 끄기」는 「게스트 OS가 예고 없이 멈춘다」만 재현하며, 물리 디스크의 휘발 캐시 소실이나 컨트롤러 의존 깨짐은 재현하지 않습니다. 장치 PC로 출하한다면 최종 확인은 운영 상당 하드웨어에서의 실제 전원 차단 시험입니다
  • 로그아웃으로 빠른 확인: WM_QUERYENDSESSION → WM_ENDSESSION 경로는 로그아웃에서도 돕니다(차이는 lParam의 ENDSESSION_LOGOFF 비트뿐). 개발 기기에서 정리 코드 동작을 편리하게 확인할 수 있습니다.1
  • 완전 종료와 하이브리드를 따로 시험합니다: shutdown /s /t 0(완전), shutdown /s /hybrid /t 0(기본 동작), shutdown /r /t 0(다시 시작)을 각각 시험합니다.2
  • 정리가 얼마나 걸리는지 잽니다: 정리 함수의 시작과 끝에 타임스탬프를 로그에 쓰고, 5초(또는 서비스의 설정 유예)에 들어가는지 잽니다
검증할 조작과 각각이 확인할 수 있는 것로그아웃은 알림 경로의 편리한 확인이고, 종료 명령의 완전·하이브리드·다시 시작은 운영 알림 경로와 유예를 확인하며, VM 전원 끄기는 갑작스러운 정지 내성을 시험하고, 물리 전원 차단 시험은 물리 저장소를 포함한 최종 확인이다로그아웃QUERY → ENDSESSION 경로shutdown /s /hybrid /r운영 경로 + 유예VM 전원 끄기갑작스러운 게스트 정지물리 전원 차단저장소 포함(최종)

그림 14: 로그아웃은 알림 경로의 편리한 확인이고, 종료 명령의 완전·하이브리드·다시 시작은 운영 알림 경로와 유예를 확인하며, VM 전원 끄기는 갑작스러운 정지 내성을 시험하고, 물리 전원 차단 시험은 물리 저장소를 포함한 최종 확인이다.

사후 분리에는 이벤트 로그(System)가 유용합니다. 정상 종료나 다시 시작에서는 이벤트 ID 1074(어느 프로세스가, 누구를 위해, 어떤 이유로 종료를 시작했는지)가 기록됩니다. 갑작스러운 정전이나 크래시에는 1074가 없고, 다음 부팅에서 이벤트 ID 41(Kernel-Power)과 6008(이전 시스템 종료가 예기치 않았다)이 기록됩니다.14 「밤에 무슨 일이 있었는지」는 여기서 시작합니다.

# Check the recent history of shutdown-related events
Get-WinEvent -FilterHashtable @{ LogName = 'System'; Id = 1074, 6008, 41 } -MaxEvents 20 |
    Select-Object TimeCreated, Id, ProviderName, Message |
    Format-List

1074가 「Windows Update에 의한 다시 시작」을 보여 주고 앱 데이터가 깨져 있으면, 문제는 정리 코드입니다. 6008/41은 「예기치 않은 종료」만 보여 주며, 정전뿐 아니라 블루스크린(크래시)이나 강제 리셋에도 기록됩니다. 41의 BugcheckCode가 0이 아니면 크래시이고, 0이며 메모리 덤프도 없으면 정전일 가능성이 큽니다── 주변 정보로 원인을 분리하고, 정전임을 알면 8장의 쓰기 설계와 UPS가 다음 단계입니다.

10. 정리

  • 종료는 「조만간 오는 일상 이벤트」입니다. 알림 뒤의 유예는 원칙적으로 약 5초뿐이므로, 전제는 「종료 때 하는 일」이 최소가 되도록 빈번히 자동 저장하는 것입니다.
  • Windows 8 이후 클라이언트 OS에서 빠른 시작이 켜져 있으면 「종료」는 하이브리드 종료이며 커널은 최대 절전만 합니다. 완전 리셋은 「다시 시작」뿐입니다── 장애 절차에 「다시 시작」을 쓰십시오.
  • GUI 앱은 WM_QUERYENDSESSION에 즉시 TRUE를 돌려주고, 확정된 정리는 WM_ENDSESSION에서 합니다. WinForms/WPF의 FormClosing과 SessionEnding은 쿼리 단계에 대응하므로, 거기서 하는 일은 멱등한 스냅샷 저장이 한도입니다. 종료 중에 대화상자를 띄우지 마십시오.
  • 정말로 중단할 수 없는 작업은 ShutdownBlockReasonCreate로 이유를 표시해 보호합니다. 다만 막을 수 있다는 보장은 어디에도 없습니다.
  • 콘솔 앱은 SetConsoleCtrlHandler로, 서비스는 SERVICE_ACCEPT_PRESHUTDOWN/SHUTDOWN으로 알림을 받습니다. 현재 OS의 기본 PRESHUTDOWN 유예는 10초입니다. .NET에서는 ProcessExit에 의존을 그만두고 앱 모델의 정석 경로로 옮깁니다.
  • 다시 시작 뒤의 복구는 RegisterApplicationRestart(+ 복구 콜백)와 ARSO로 무인화할 수 있습니다.
  • 정전은 알림이 없습니다. 임시 파일 + ReplaceFile 교체(백업 + 로드 시 검증과 세트), 체크포인트의 플러시, 「정전을 계획 종료로 바꾸는」 UPS로 대비합니다.
종료 처리의 전체 그림알림이 있는 끝남에는 수 초 안에 가게를 닫는 정리로 응답하고 다시 시작 뒤의 자동 복구로 이끌며, 알림이 없는 정전에는 언제 잘려도 깨지지 않는 쓰기와 UPS로 대비하고 물리 하드웨어를 포함해 검증한다. 이 두 기둥이 글의 결론이다알림 있음알림 없음어떻게 끝나는가수 초의 정리(3–6)안전한 쓰기 + UPS(8)다시 시작 뒤 자동 복구(7)하드웨어에서 검증(9)

그림 15: 알림이 있는 끝남에는 수 초 안에 가게를 닫는 정리로 응답하고 다시 시작 뒤의 자동 복구로 이끌며, 알림이 없는 정전에는 언제 잘려도 깨지지 않는 쓰기와 UPS로 대비하고 물리 하드웨어를 포함해 검증한다. 이 두 기둥이 글의 결론이다.

  • VM과 로그아웃으로 안전하게 검증하고, 사후는 이벤트 ID 1074/41/6008로 분리합니다.

다음에 앱에 기능을 더할 때, 한 번 이렇게 물으십시오. 이 작업 한가운데에 WM_ENDSESSION이 오거나 전원이 뽑히면, 다음 기동에 무엇이 남는가? 그 답을 설계에 쓰는 것이, 아침마다 장치 PC 앞에서 머리를 싸매지 않는 가장 짧은 길입니다.

관련 기사

관련 상담 영역

합동회사 코무라소프트에서는 장치 PC·장시간 가동 앱의 종료·정전 대책 설계와 구현, Windows Update 다시 시작이나 로그아웃에서 시작하는 데이터 손상·「아침에는 멈춰 있었다」사고의 원인 조사, Windows 서비스 중지 처리와 자동 복구의 설계 리뷰를 다룹니다. 「종료할 때마다 무언가 깨지는 것 같은데, 어디서부터 손대야 할지 모르겠다」는 단계에서 시작해도 됩니다.

참고 링크

  1. Microsoft Learn, WM_QUERYENDSESSION message. WM_QUERYENDSESSION이 세션 종료 때 보내지며 앱은 TRUE를 돌려주고 사용자의 의도를 존중해야 한다는 점(DefWindowProc의 기본도 TRUE), 정리는 WM_ENDSESSION까지 미뤄야 한다는 점, 5초 뒤 시스템이 종료를 막는 앱용 UI를 표시하고 사용자가 강제 종료할 수 있다는 점, lParam의 ENDSESSION_LOGOFF/CLOSEAPP/CRITICAL 비트의 의미, 종료와 다시 시작을 구별할 수 없다는 점, 종료 때 저장할 것이 줄어들도록 데이터를 자주 저장해야 한다는 점에 대해.  2 3 4 5 6 7

  2. Microsoft Learn, Fast startup causes hibernation or shutdown to fail in Windows 10 or Windows 8.1. 빠른 시작에서 커널 세션이 닫히지 않고 최대 절전으로 취급되며 커널과 장치 드라이버 상태가 hiberfil.sys에 저장된다는 점, 「다시 시작」은 완전히 새로운 Windows 상태가 필요하므로 항상 완전 부팅을 한다는 점, 빠른 시작이 기본으로 켜져 있고 끄는 것은 권장되지 않는다는 점, Shutdown.exe의 기본은 완전 종료이며 /hybrid 옵션이 하이브리드 동작을 만든다는 점에 대해.  2 3 4 5

  3. Microsoft Learn, ShutdownBlockReasonCreate function (winuser.h). 중단할 수 없는 작업의 시작에서 호출해 이유 문자열을 등록하고 끝나면 ShutdownBlockReasonDestroy를 호출한다는 점, 창을 만든 스레드에서만 호출할 수 있다는 점, 사용자는 이유를 몇 초만 읽으므로 문자열이 짧고 분명해야 한다는 점에 대해.  2 3

  4. Microsoft Learn, Shutdown Changes for Windows Vista. WM_QUERYENDSESSION/WM_ENDSESSION 응답을 각각 5초 늦출 수 있고 이후 사용자가 계속 또는 취소를 고를 수 있다는 점, 콘솔 앱이나 보이는 창이 없는 앱은 종료를 중단할 수 없고 5초의 무응답 또는 FALSE 응답 뒤에 자동 종료된다는 점, 막기가 필요하면 ShutdownBlockReasonCreate로 이유를 등록해야 한다는 점, 앱이 종료를 막을 수 있다는 데 의존해서는 안 된다는 점에 대해.  2 3 4

  5. Microsoft Learn, HandlerRoutine callback function. SetConsoleCtrlHandler로 등록한 핸들러가 받는 CTRL_C/BREAK/CLOSE/LOGOFF/SHUTDOWN 이벤트, CTRL_CLOSE_EVENT의 기본 타임아웃이 약 5000밀리초이고 서비스 프로세스의 CTRL_SHUTDOWN_EVENT는 약 20000밀리초라는 점, 대화형 앱은 로그오프 때 종료되므로 CTRL_LOGOFF/SHUTDOWN_EVENT는 본질적으로 서비스만 받는다는 점, 핸들러가 별도 스레드에서 돈다는 점에 대해.  2 3

  6. Microsoft Learn, SetConsoleCtrlHandler function. gdi32.dll 또는 user32.dll을 로드한 프로세스는 Windows 앱으로 취급되어 CTRL_LOGOFF_EVENT/CTRL_SHUTDOWN_EVENT 핸들러가 호출되지 않는다는 점, 우회는 숨은 창을 만들어 WM_QUERYENDSESSION/WM_ENDSESSION을 처리하는 것이라는 점, 시그널 처리 중 콘솔 함수가 올바르게 동작하지 않을 수 있다는 점에 대해.  2

  7. Microsoft Learn, .NET runtime no longer provides default termination signal handlers. .NET 10부터 런타임이 Windows CTRL_SHUTDOWN_EVENT/CTRL_CLOSE_EVENT(Unix SIGTERM/SIGHUP 상당)의 기본 핸들러를 더 이상 제공하지 않는다는 점, OS 기본 처리가 앱을 즉시 종료하고 AppDomain.ProcessExit와 AssemblyLoadContext.Unloading이 더 이상 일어나지 않는다는 점, 앱 모델에 맞는 시그널 처리를 상위 라이브러리 또는 앱 코드에서 등록해야 한다는 점에 대해.  2

  8. Microsoft Learn, Service Control Handler Function. SERVICE_ACCEPT_PRESHUTDOWN을 선언한 서비스가 먼저 SERVICE_CONTROL_PRESHUTDOWN을 받고, 이어서 SERVICE_ACCEPT_SHUTDOWN 서비스가 SERVICE_CONTROL_SHUTDOWN을 받는다는 점, 종료 때 기본 유예가 약 20초이고 OS 다시 시작 때 상한이 WaitToKillServiceTimeout이라는 점, 이 값을 늘려서는 안 된다는 점, 제어 핸들러는 30초 안에 돌아와 STOP_PENDING과 wait hint를 보고하고 긴 작업은 다른 스레드에 맡겨야 한다는 점, UPS 운용을 염두에 두고 정리를 가능한 한 빨리 끝내야 한다는 점, 종료 때 SCM은 기본적으로 의존성을 고려하지 않는다는 점에 대해.  2 3 4 5

  9. Microsoft Learn, SERVICE_PRESHUTDOWN_INFO structure (winsvc.h). PRESHUTDOWN 알림 뒤 SCM이 서비스가 멈추거나 타임아웃까지 기다린다는 점, 기본 타임아웃이 Windows 10 Creators Update(빌드 15063) 이후 10초이고 그 이전은 3분이라는 점, ChangeServiceConfig2로 설정한다는 점, SERVICE_STOP_PENDING 중에 상태를 계속 갱신할 수 있다는 점에 대해.  2

  10. Microsoft Learn, RegisterApplicationRestart function (winbase.h). 크래시, 응답 없음, 업데이트, 업데이트를 동반한 컴퓨터 다시 시작에 다시 시작을 등록할 수 있다는 점, 다시 시작용 명령줄 인수를 지정할 수 있다는 점, 등록은 문제가 일어나기 전에 해야 하며 업데이트 시나리오에서 WM_QUERYENDSESSION 처리 중이 마지막 기회라는 점, 가동 60초 미만 프로세스는 다시 시작되지 않는다는 점, 크래시나 행 뒤의 다시 시작은 사용자 동의를 거친다는 점, OS 다시 시작을 넘으려면 EWX_RESTARTAPPS/SHUTDOWN_RESTARTAPPS로 종료해야 한다는 점에 대해.  2 3

  11. Microsoft Learn, Winlogon automatic restart sign-on (ARSO). Windows Update가 자동 다시 시작을 시작할 때 마지막 대화형 사용자의 자격 증명을 저장하고 Autologon을 구성한다는 점, 다시 시작 뒤에 사용자를 자동으로 로그인하고 세션을 잠근다는 점, 성공한 사인인 뒤에 저장된 자격 증명이 삭제된다는 점, 그룹 정책(DisableAutomaticRestartSignOn 등)으로 구성할 수 있다는 점에 대해.  2

  12. Microsoft Learn, ReplaceFileW function (winbase.h). ReplaceFile이 「새 파일에 저장, 원본을 임시로 이름 변경, 새 파일의 이름 변경, 원본 삭제」에 해당하는 여러 단계를 하나의 함수로 묶는다는 점, 원본 파일의 생성 시각, DACL, 암호화, 압축, 명명된 스트림 같은 특성을 보존한다는 점, 백업·교체 대상·교체 파일이 같은 볼륨에 있어야 한다는 점에 대해.  2

  13. Microsoft Learn, File Caching. 쓰기가 기본적으로 시스템 캐시에 올라가고 지연 쓰기로 디스크에 반영된다는 점, FILE_FLAG_WRITE_THROUGH가 즉시 디스크에 쓴다는 점, FlushFileBuffers로 명시적으로 플러시할 수 있다는 점, 파일 시스템 메타데이터는 항상 캐시되므로 메타데이터 확인에는 플러시 또는 write-through가 필요하다는 점에 대해.  2 3

  14. Microsoft Learn, Troubleshoot unexpected reboots using system event logs. 정상 다시 시작이 이벤트 ID 1074(어느 프로세스가, 누구를 위해, 어떤 이유로 종료를 시작했는지)를 기록한다는 점, 예기치 않은 다시 시작이 이벤트 ID 41(Kernel-Power)과 6008(이전 종료가 예기치 않았다)을 기록한다는 점, 이 ID로 다시 시작의 종류를 분리할 수 있다는 점에 대해.  2

  15. Microsoft Learn, PBT_APMPOWERSTATUSCHANGE event. 배터리와 AC 사이 전환이나 잔량 저하 때 이 이벤트가 WM_POWERBROADCAST로 알림된다는 점, 받으면 GetSystemPowerStatus를 호출해 ACLineStatus, BatteryFlag, BatteryLifePercent 같은 SYSTEM_POWER_STATUS 필드를 확인해야 한다는 점에 대해. 

같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.

이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.

이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.

자주 묻는 질문

이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.

「종료」로는 안 없어지던 문제가 「다시 시작」으로 없어졌습니다. 왜인가요?
Windows 8 이후 클라이언트 OS에서 빠른 시작이 켜져 있으면(최대 절전을 지원하는 대부분의 PC에서 기본값) 「종료」는 하이브리드 종료라는 메커니즘을 씁니다. 사용자는 로그아웃되지만, 커널과 드라이버 상태는 최대 절전 파일에 저장되어 다음 기동에서 그대로 복원됩니다. 즉 OS의 핵심은 리셋되지 않습니다. 반면 「다시 시작」은 항상 완전 부팅이므로 드라이버와 서비스 문제가 리셋됩니다. 분리 절차에는 「종료했다가 다시 켠다」가 아니라 「다시 시작」을 쓰십시오. 명령줄에서 완전 종료를 원하면 shutdown /s를 쓸 수 있습니다.
앱이 저장을 끝낼 때까지 종료를 멈출 수 있나요?
잠시 기다려 달라고 요청할 수는 있지만, 확실하게 멈출 수는 없습니다. 중단할 수 없는 작업 중에만 ShutdownBlockReasonCreate로 이유 문자열을 등록하면, 「이 앱이 종료를 막고 있습니다」화면에 그 이유가 나와 사용자가 계속할지 취소할지를 결정할 수 있습니다. 다만 사용자는 강제 계속을 고를 수 있고, 강제 종료나 업데이트에 의한 다시 시작은 전혀 기다리지 않을 수 있습니다. 따라서 정답은 「막기」가 아니라, 위험에 노출된 데이터를 줄이는 빈번한 자동 저장과, 종료 알림에서 수 초 안에 끝나는 정리 설계입니다.
Windows 서비스 중지가 오래 걸립니다. 종료 유예를 늘릴 수 있나요?
SERVICE_CONTROL_SHUTDOWN을 받는 기본 구성에서 유예는 대략 20초이며 WaitToKillServiceTimeout 레지스트리 값에 따릅니다. 앱 측에서 그 값을 고쳐 늘리는 것은 권장되지 않습니다. 더 긴 유예가 필요하면 SERVICE_ACCEPT_PRESHUTDOWN을 선언하고 SERVICE_CONTROL_PRESHUTDOWN을 받을 수 있습니다. 다른 쪽보다 먼저 알림이 오고, 타임아웃은 ChangeServiceConfig2로 설정할 수 있습니다(기본은 Windows 10 Creators Update 이후 10초, 그 이전은 3분). 다만 PRESHUTDOWN은 그 구간 동안 시스템 전체 종료를 붙잡으므로, 정말로 필요한 경우로 한정하고, 근본적으로는 중지 작업 자체를 수 초 안에 끝내도록 설계하십시오.
.NET의 AppDomain.ProcessExit에서 종료 정리를 해도 안전한가요?
의존하지 않기를 권합니다. 예전에는 런타임이 기본 시그널 핸들러를 등록해 CTRL_CLOSE_EVENT와 CTRL_SHUTDOWN_EVENT에서 ProcessExit가 일어났지만, .NET 10부터 런타임은 기본 종료 시그널 핸들러를 제공하지 않고, 그 경로에서 ProcessExit가 일어나지 않습니다. 정리는 앱 모델에 맞는 알림 경로에 구현합니다. GUI 앱은 FormClosing 또는 SessionEnding(쿼리 단계 알림이므로 멱등한 저장으로 한정하고, 세션이 확정된 뒤에만 할 수 있는 정리는 WM_ENDSESSION 훅에 둡니다). Generic Host / Worker Service는 IHostApplicationLifetime과 StopAsync. 콘솔 앱은 SetConsoleCtrlHandler 또는 PosixSignalRegistration입니다.
갑작스러운 정전으로 파일이 깨지지 않게 하려면 어떻게 하나요?
정전은 알림이 전혀 없으므로, 언제 전원이 잘려도 깨지지 않게 쓰는 수밖에 없습니다. 기준은 원본 파일을 제자리에서 덮어쓰지 않는 것입니다. 같은 볼륨의 임시 파일에 완전히 쓰고, 플러시하고, ReplaceFile(.NET의 File.Replace)로 바꿉니다. 보통 운용에서는 완전한 옛 파일 또는 완전한 새 파일을 읽을 수 있게 남지만, 정전을 넘는 ReplaceFile의 원자성은 사양으로 보장되지 않으므로, 백업(세 번째 인수)을 남기고 로드 때 주 파일을 검증해 깨져 있으면 백업으로 떨어지는 복구를 구현합니다. 또한 WriteFile의 성공은 데이터가 디스크에 도달했다는 뜻이 아니므로, 중요한 체크포인트에서는 FlushFileBuffers 또는 FILE_FLAG_WRITE_THROUGH로 쓰기를 확인합니다. 장치 PC에서는 이와 UPS를 조합하고, 배터리 전환을 감지해 안전 종료로 이끄는 구성이 표준입니다.

저자 프로필

기사 저자의 프로필 페이지입니다.

Go Komura

합동회사 코무라소프트 대표

Windows 소프트웨어 개발, 기술 상담, 장애 조사를 중심으로 재현이 어려운 장애 조사와 기존 자산이 남아 있는 프로젝트에 강점이 있습니다.

블로그 목록으로 돌아가기