“감시 앱을 밤새 돌려 두었을 텐데 아침에 보니 그래프가 새벽 1시에서 멈춰 있었다”, “데스크톱 PC에서는 몇 달이나 안정적이었던 수집 도구가 노트북으로 바꾸자마자 기록이 듬성듬성해졌다” ── 장시간 계속 동작하는 업무용 앱 상담에서 이 증상은 단골 중의 단골입니다. 로그를 보면 예외도 크래시도 없이, 단지 몇 시간 분의 기록이 통째로 빠져 있습니다. 범인은 앱의 버그가 아니라 Windows의 전원 관리인 경우가 매우 많습니다.
성가신 점은 “슬립”이라고 한마디로 말해도 그 내용이 한 가지가 아니라는 것입니다. 기존의 S3 슬립, 최대 절전 모드(S4), 그리고 최근 노트북에서 주류인 Modern Standby(S0 low power idle)에서는 앱 입장에서 보이는 멈추는 방식도, 대책이 통하는 방식도 다릅니다. 특히 Modern Standby는 “슬립 중에도 시스템은 동작하고 있다”는 홍보 문구 때문에 오해받기 쉽지만, 데스크톱 앱은 오히려 적극적으로 멈춰집니다.
이 글에서는 슬립의 종류와 슬립 중 앱의 동작을 최소한만 짚은 뒤, “슬립시키지 않는다”, “슬립을 전제로 한다”, “정해진 시각에 깨운다”는 세 가지 설계를 어떻게 구분해 사용할지를, 구현 예시와 판단표와 함께 정리합니다.
1. 먼저 결론
결론이 많으므로 전제(1.1)・슬립을 “시키지 않는” 설계(1.2)・슬립에 “따르는” 설계(1.3)의 세 가지로 나눠 놓습니다. 1.1을 읽은 후 자신의 앱이 1.2 쪽인지 1.3 쪽인지를 정하고 본문으로 넘어가는 것이 지름길입니다(판단표는 7장).
1.1. 전제 ── 슬립의 종류와, 멈춰 있는 동안 일어나는 일
- Windows의 슬립에는 기존의 S3 슬립・최대 절전 모드(S4)・Modern Standby(S0 low power idle)가 있으며, Modern Standby 지원 기기는 S1〜S3를 지원하지 않습니다. 자신의 PC가 어느 것인지는
powercfg /a로 확인할 수 있습니다.123 - Modern Standby 중에도 데스크톱 앱은 계속 동작하지 않습니다. DAM(Desktop Activity Moderator)이 데스크톱 프로세스의 스레드를 서스펜드합니다(세션 0의 서비스는 스로틀링). “S0니까 동작할 것”이라는 전제로 설계하지 마십시오.4
- 슬립 중에는 스레드가 실행되지 않으며, 타이머 기한의 계산 방식도 API 세대마다 다릅니다. Windows 8 이후, 상대 지정 타이머나 대기(
SetWaitableTimer의 상대 지정,SleepEx등)는 슬립 중 카운트가 진행되지 않고, 남은 시간을 복귀 후로 이월합니다.56 .NET의 타이머는 .NET 10까지는 슬립 시간을 세는 구현이었으나(슬립 중에 기한이 지나 있으면 복귀 직후 발화), .NET 11부터는 세지 않는 방식으로 바뀔 예정입니다(.NET 11은 본 기사 작성 시점에서 아직 출시되지 않았습니다. 3.1의 단서를 참조).6 경과 시간 API도, 슬립 시간을 포함하는 것(GetTickCount, QueryPerformanceCounter=Stopwatch)과 포함하지 않는 것(QueryUnbiasedInterruptTime)이 혼재합니다.78 - 슬립 중 유휴 상태가 된 TCP 연결은, NAT・방화벽・로드밸런서 등의 중간 장비의 유휴 타임아웃으로 조용히 폐기되어 있는 경우가 있습니다(예: Azure Load Balancer의 기본값은 4분 만에 통지 없이 드롭). 복귀 후에는 재연결을 전제로 설계합니다.94
1.2. 슬립을 “시키지 않는” 설계(4장)
- 슬립을 억제하는 정공법은
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)입니다. 다만 효과가 있는 것은 무조작 타임아웃에서 비롯된 자동 슬립뿐이며, 사용자가 전원 버튼이나 덮개 닫기로 지시한 슬립은 막을 수 없습니다. 필요한 구간에만 설정하고, 끝나면 반드시 해제합니다.1011 - 억제가 적용되고 있는지는
powercfg /requests로 확인할 수 있습니다.PowerCreateRequest계열 API라면 요청에 이유 문자열을 붙일 수 있어 이 목록에 표시되므로 운영 조사에 강해집니다.121314 - 24시간 계속 가동하는 장치용 PC처럼 “애초에 슬립시키지 않는다”가 요건이라면, 앱의 억제 API가 아니라 전원 설정에서 비활성화하는 것이 본래의 방법입니다(구체적인 설정 위치는 7.1).
1.3. 슬립에 “따르는” 설계(5장・6장)
- 슬립을 전제로 한다면, .NET에서는
SystemEvents.PowerModeChanged, Win32에서는WM_POWERBROADCAST(PBT_APMSUSPEND / PBT_APMRESUMEAUTOMATIC)로 감지합니다. 서스펜드 통지의 유예는 앱 하나당 약 2초밖에 없으며, 배터리가 부족할 때는 통지 없이 슬립하는 경우도 있습니다.15161718 - 창을 갖지 않는 콘솔・서비스형 앱은,
PowerRegisterSuspendResumeNotification으로 HWND 없이 동일한 통지를 받을 수 있습니다(최소 예제는 5.1).19 - 정해진 시각에 확실히 동작시키려면, 작업 스케줄러의 “작업 실행 시 절전 모드 해제”(WakeToRun)가 첫 번째 후보입니다. 작업이 완료될 때까지 시스템을 깨어 있는 상태로 유지해 줍니다. 다만 전원 옵션의 웨이크 타이머 허용에 의존하므로, 실제 기기에서의 기동 확인이 필수입니다.2021
2. Windows의 슬립은 한 가지가 아니다
먼저, 대책을 생각하기 전 전제가 되는 세 가지 상태만 짚습니다.
| 상태 | 통칭 | 내용 | 앱 입장에서 보면 |
|---|---|---|---|
| S3 | 기존 슬립 | CPU 정지, RAM만 통전하여 상태 유지 | 계산 처리는 일절 돌지 않음1 |
| S4 | 최대 절전 모드 | 메모리 내용을 최대 절전 모드 파일에 써서 전원 차단 | 위와 동일. 복귀는 파일에서의 복원1 |
| S0 low power idle | Modern Standby | 시스템은 저전력으로 부분적으로 계속 가동, 즉시 복귀 | 데스크톱 앱은 DAM에 의해 멈춤24 |
S3와 S4는 “시스템이 계산 작업을 아무것도 실행하지 않는, 겉보기에는 꺼진 상태”입니다.1 한편 Modern Standby는 스마트폰의 전원 모델에 가까운 방식으로, 화면이 꺼져 있는 동안에도 네트워크 연결을 유지한 채 저전력으로 대기하며, 전원 버튼에서 1초 미만으로 복귀합니다. Modern Standby를 지원하는 기종은 S1〜S3를 사용하지 않습니다.222
여기서 중요한 것이 Modern Standby 지원 기기에 탑재된 DAM(Desktop Activity Moderator)입니다. DAM은 스탠바이 진입 시 데스크톱 앱의 실행을 S3 슬립과 동등하게 억누르는 구조로, 대화형 세션의 프로세스는 모든 스레드가 서스펜드되고, 세션 0의 서비스는 스로틀링(대부분의 시간 동안 서스펜드되고, 단속적으로만 실행)됩니다.4 즉 “Modern Standby라면 슬립 중에도 앱이 동작하지 않을까”라는 기대는 정반대이며, 앱 입장에서는 S3와 마찬가지로 멈추고, 게다가 S3와 달리 “시스템 자체는 동작하는데 앱만 멈춘다”는 상태이므로, 시각 어긋남이나 타이머 동작의 불일치로 나타난다고 이해해 두는 것이 안전합니다.4
자신의 PC(고객 현장의 PC)가 어느 방식인지는, 관리자 권한 없이 다음 명령으로 확인할 수 있습니다.3
> powercfg /a
다음 절전 상태를 이 시스템에서 사용할 수 있습니다:
대기 모드(S0 저전력 유휴 상태) 네트워크에 연결됨
최대 절전 모드
...
대기 모드 (S3)라고 나오면 S3 기기, S0 저전력 유휴 상태라고 나오면 Modern Standby 기기입니다. “노트북으로 바꿨더니 기록이 빠지게 되었다”는 상담에서는 우선 이것을 확인합니다.
3. 슬립 중・복귀 후 앱은 어떻게 되는가
3.1. 스레드와 타이머
슬립 중(S3/S4, 그리고 DAM 서스펜드 중)에는 스레드가 실행되지 않습니다.14 간과되기 쉬운 것은 타이머나 대기의 “기한”이 슬립 시간을 어떻게 세는가이며, 이는 API 계층에 따라 다르게 취급됩니다. 다음 장의 경과 시간 API 표와 같은 형태로 나열하면 다음과 같습니다.
| 타이머・대기의 종류 | 슬립 중의 취급 | 복귀 후의 동작 |
|---|---|---|
Win32의 상대 타이머(SetWaitableTimer / SetWaitableTimerEx의 상대 지정) |
Windows 8 이후는 카운트가 진행되지 않음(Windows 7 이전에는 저전력 상태의 시간도 포함해 진행되었음)5 | 남은 시간만큼 기다린 후 발화5 |
타임아웃 지정 대기 API(SleepEx / WaitForMultipleObjectsEx 등) |
Windows 8 이후는 비가동 시간을 세지 않음6 | 남은 시간을 이월하여 대기를 계속함6 |
.NET의 관리형 타이머(System.Threading.Timer 등) |
.NET 10까지는 셈(GetTickCount64 기반), .NET 11부터는 세지 않을(QueryUnbiasedInterruptTime 기반) 예정6 | .NET 10까지는 기한이 지나 있으면 복귀 직후 발화, .NET 11부터는 남은 시간을 기다린 후 발화할 예정6 |
웨이크 가능 타이머(SetWaitableTimer의 fResume을 TRUE로) |
시스템 자체를 기동시킴23 | 기동하여 발화하지만, 무인 유휴 타이머(최소 2분) 동안 “사용 중”을 선언하지 않으면 재슬립함(6장)23 |
3번째 행의 변경 문서는 Environment.TickCount64의 기반이 바뀌는 것에 따른 파괴적 변경(breaking change)으로 설명되며, 문서 자체가 “복귀 직후 타이머가 발화하지 않게 되는 코드가 있을 수 있다”고 주의를 주고 있습니다.6
.NET 11에 관한 서술에 대해
.NET 11은 본 기사 작성 시점(2026년 7월)에서 아직 출시되지 않았습니다. .NET은 매년 11월에 메이저 릴리스되는 연간 주기이며, 가장 최근인 .NET 10은 2025년 11월 11일 공개되었습니다.24 본 기사의 .NET 11 동작은 Microsoft가 공개한 파괴적 변경 문서(프리뷰 단계의 정보)에 근거한 예정이며, GA(정식 출시) 후에는 반드시 최신 문서로 확인하시기 바랍니다.6
즉 “10초 간격의 System.Threading.Timer로 계측하던 앱이 8시간 슬립했다”는 경우, 8시간분이 한꺼번에 발화하는 일은 어느 쪽이든 없지만, 복귀 직후 1회 즉시 발화하는지, 남은 시간을 기다린 후 발화하는지는 API 계층과 런타임 버전에 달려 있습니다. 어느 쪽이든 슬립 중의 샘플은 결측이 됩니다. “복귀 직후의 발화”에 의존해 재정비 처리를 짜기보다, 5장의 복귀 이벤트에서 명시적으로 재스케줄링하는 것이 안전합니다.
3.2. 경과 시간 측정이 어긋난다
경과 시간 API에는, 슬립 시간을 포함하는 것과 포함하지 않는 것이 혼재합니다.
| API | 슬립 시간 |
|---|---|
| GetTickCount / GetTickCount64 | 포함7 |
| QueryPerformanceCounter(= .NET의 Stopwatch의 기반) | 포함(standby・hibernate・connected standby)8 |
| QueryUnbiasedInterruptTime | 포함하지 않음(working state의 시간만)257 |
| Environment.TickCount / TickCount64 | .NET 10까지 포함(GetTickCount64 기반), .NET 11부터 포함하지 않음(QueryUnbiasedInterruptTime 기반)으로 변경6 |
“Stopwatch로 10초 지나면 다음 샘플”이라는 코드는, 슬립을 걸치면 “8시간과 10초 지났다”는 것이 되고, 반대로 TickCount 기반의 경과 판정은 .NET 11 이행에 따라 동작이 바뀝니다. “처리에 걸린 시간”은 Stopwatch, “다음에 동작해야 할 벽시계 시각”은 DateTime/DateTimeOffset으로 관리하고, 복귀 이벤트(5장)에서 기준을 다시 잡는 역할 분담을 해 두면, 슬립에도 런타임 업데이트에도 흔들리지 않습니다. 짧은 주기 타이머 설계 자체에 관한 이야기는 “Windows에서 타이머 대기보다 이벤트 대기를 우선하는 이유“에서 다루고 있습니다.
3.3. TCP 연결은 “조용히” 죽어 있다
슬립 중에는 앱이 통신할 수 없으므로 연결은 유휴 상태가 됩니다. 문제는 경로상의 장비입니다. NAT나 방화벽, 로드밸런서 같은 중간 장비는 유휴 흐름을 타임아웃으로 폐기하는데, 많은 구성에서는 폐기를 양쪽에 통지하지 않고 조용히 끊어버립니다. 예를 들어 Azure Load Balancer의 기본 동작은 “유휴 타임아웃(기본 4분)에 도달한 흐름을 조용히 드롭”입니다.9 사내 라우터나 장치 쪽의 내장 TCP 스택에도 같은 종류의 타임아웃이 있습니다.
그 결과, 복귀 후 앱의 소켓은 겉보기에는 정상인 채로, 다음 송신에서 오류가 나거나, 응답 대기 상태로 타임아웃까지 멈춰 버립니다. DAM 문서에서도 프로세스 서스펜드가 연결의 수명이나 핸드셰이크에 미치는 영향을 고려하도록 명시하고 있습니다.4 게다가 Modern Standby 기기에서는, 배터리 구동 시 기본적으로 슬립 중의 네트워크 활동 자체가 정지됩니다(Adaptive Connected Standby).26 복귀 이벤트를 받으면 연결은 의심하고 폐기・재연결하는 것이 정석입니다. 연결이 “살아 있는 것처럼 보이지만 죽어 있는” 문제의 구분은 “TCP 재전송으로 산업용 카메라 통신이 수 초 멈출 때 - RFC1323 타임스탬프와 재전송 대기의 구분“도 참고하시기 바랍니다.
4. 슬립을 “시키지 않는다” ── SetThreadExecutionState와 전원 요청
계측 중인 몇 시간만은 슬립되면 곤란하다는 경우의 정공법이 SetThreadExecutionState입니다. C#에서는 P/Invoke로 호출합니다.
using System.Runtime.InteropServices;
internal static class PowerGuard
{
[Flags]
private enum EXECUTION_STATE : uint
{
ES_CONTINUOUS = 0x80000000,
ES_SYSTEM_REQUIRED = 0x00000001,
ES_DISPLAY_REQUIRED = 0x00000002,
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern EXECUTION_STATE SetThreadExecutionState(EXECUTION_STATE esFlags);
/// <summary>계측 시작 시 호출: 자동 슬립을 억제한다(End와 같은 스레드에서 호출할 것)</summary>
public static void Begin() =>
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS |
EXECUTION_STATE.ES_SYSTEM_REQUIRED);
/// <summary>계측 종료 시 반드시 호출: 억제를 해제한다(Begin과 같은 스레드에서 호출할 것)</summary>
public static void End() =>
SetThreadExecutionState(EXECUTION_STATE.ES_CONTINUOUS);
}
파악해야 할 사양은 다음과 같습니다.
ES_CONTINUOUS를 붙이면, 다음에ES_CONTINUOUS를 붙여 다시 호출할 때까지 효과가 지속됩니다. 붙이지 않으면 유휴 타이머를 1회 재설정할 뿐이므로, 지속시키고 싶다면 정기적으로 계속 호출해야 합니다.10ES_SYSTEM_REQUIRED는 시스템의 슬립을,ES_DISPLAY_REQUIRED는 디스플레이 꺼짐을 억제합니다. 백그라운드 계측이라면 전자만으로 충분합니다. 화면은 꺼져도 상관없는데ES_DISPLAY_REQUIRED까지 세우는 것은 전력 낭비입니다.10- 사용자가 전원 버튼을 누르거나 덮개를 닫아서 발생하는 슬립은 막을 수 없습니다. 이 함수가 효과가 있는 것은 무조작 타임아웃에서 비롯된 자동 슬립뿐입니다. 사용자의 명시적인 조작은 존중해야 한다고 공식 문서에도 명시되어 있습니다.10
- 시스템은
SetThreadExecutionState를 호출한 스레드 수를 세고 있으며, 카운트가 0이 되고 사용자 입력도 없으면 슬립에 들어갑니다.11 프로세스가 비정상 종료되면 억제도 사라지므로, “억제한 채로 해제를 잊어버려 영원히 슬립하지 않는 PC”가 될 걱정은 재시작하면 해소되지만, 반대로 말하면 크래시 내성을 담보해 주지는 않습니다. - 이름 그대로, 이 함수가 설정하는 것은 호출한 스레드의 실행 상태입니다.10 설정과 해제는 같은 스레드에서 수행하십시오.
async/await의 연속(continuation)은 다른 스레드 풀 스레드에서 실행되는 경우가 있으므로,await를 사이에 두고Begin()과End()를 호출하는 구현에서는 해제가 슬립 억제를 세운 것과는 다른 스레드에서 공회전하여, 원래 스레드가 살아 있는 동안 계속 억제가 남아 있게 됩니다. UI 스레드처럼 동일성이 보장되는 스레드에 고정하여 호출하는 것이 안전하며, 스레드를 넘나드는 설계가 필요하다면 뒤에서 다루는 핸들 기반의 전원 요청 API를 사용하십시오.
Windows 7 이후에는, 같은 목적의 더 새로운 API로 전원 요청(PowerCreateRequest / PowerSetRequest / PowerClearRequest)도 있습니다. 이쪽은 요청 생성 시 REASON_CONTEXT로 이유 문자열을 전달할 수 있는 것이 실무상 이점이며, 요청 종류에는 PowerRequestSystemRequired 외에, Modern Standby 기기에서의 프로세스 서스펜드를 억제하는 PowerRequestExecutionRequired가 있습니다.1413 공식 모범 사례는 “시나리오 직전에 Set, 끝나면 즉시 Clear, 프로세스 종료 전에 핸들을 정리”입니다.13
다만 Modern Standby 기기에는 중요한 제약이 있습니다. 배터리 구동의 Modern Standby 시스템에서는, SystemRequired/ExecutionRequired 요청은 슬립 타임아웃 초과 5분 후에 강제 종료됩니다. 또한 전원 방식과 무관하게, 사용자 조작(전원 버튼・덮개 닫기・시작 메뉴의 슬립)에 의한 슬립 진입 시에는 요청이 종료됩니다.13 즉 “노트북에서, 덮개가 닫혀도, 배터리로도 계속 동작한다”는 앱의 노력만으로는 실현할 수 없습니다. 그런 요건은 전원 설정과 운영(덮개를 닫아도 슬립하지 않는 설정, AC 급전)으로 담보해야 하는 것입니다.
억제가 실제로 효과가 있는지는, 관리자 권한의 명령 프롬프트에서 확인할 수 있습니다.
> powercfg /requests
SYSTEM:
[PROCESS] \Device\HarddiskVolume3\Apps\SensorLogger.exe
powercfg /requests는 슬립・화면 꺼짐을 방해하고 있는 전원 요청을 나열하는 명령으로, “왜인지 슬립하지 않는 PC”의 조사에도, 자사 앱의 억제 확인에도 사용할 수 있습니다.12 출력을 읽는 방법은 다음과 같습니다.
- 출력은 요청 종류별 제목으로 구분됩니다.
DISPLAY(화면 꺼짐 억제),SYSTEM(슬립 억제),AWAYMODE가 기본이며,PowerRequestExecutionRequired에 대응하는EXECUTION구분도 나옵니다. 위 예에서SYSTEM:아래에 자신의 앱이 나와 있다면, 시스템의 슬립을 억제할 수 있는 상태입니다.1213 - 제목 아래 나열되는 행 앞의
[PROCESS]/[SERVICE]/[DRIVER]가 요청 주체의 종류입니다. 이 종류와 이름은, 그대로powercfg /requestsoverride에 전달하는 인수가 됩니다.12 - 해당하는 요청이 없는 종류에는, 요청이 없다는 취지의 행(영어 환경에서는
None.)이 나옵니다. 자사 앱의 억제를 확인할 때는,SYSTEM아래에 자신의 프로세스가 나와 있는지만 보면 충분합니다. PowerCreateRequest계열 API로 이유 문자열을 붙여 두면, 그 문자열이 이 목록에 나오므로, “어떤 처리를 위해 억제하고 있는가”를 운영 측에서도 알 수 있습니다.1413
반대로, 관리자가 powercfg /requestsoverride로 특정 프로세스의 요청을 무시하는 설정을 넣을 수 있다는 것도 알아 두십시오.12 억제 API는 “부탁”이지 절대적인 것이 아니라는 것이 설계의 전제입니다.
마지막으로 매너에 관한 이야기입니다. 앱이 실행되는 동안 계속 ES_CONTINUOUS | ES_SYSTEM_REQUIRED를 세워 두는 구현은, 사용자가 설정한 전원 플랜을 상주 앱이 영구적으로 죽여 버리는 것을 의미합니다. 노트북이라면 배터리를 소모시키고, 공유 PC라면 다른 용도에도 영향을 줍니다. 억제는 “슬립되면 곤란한 처리가 실제로 돌고 있는 구간”에만 한정하는 것이 원칙입니다(공식 문서의 예시도 “녹화 시작에서 설정, 녹화 완료에서 해제”입니다10). 참고로 사용자 쪽의 임시 회피책으로는 PowerToys Awake가 있으며, 이것도 내부적으로는 같은 구조(실행 상태를 요청하는 스레드)로 동작합니다.27 앱에 억제를 구현할지, 운영 도구에 맡길지의 경계선으로도 참고할 수 있습니다.
5. 슬립을 “전제로 한다” ── 감지・재연결・결측 기록
상주 감시나 수집 앱의 대부분은, 슬립을 금지하기보다 슬립과 공존하는 쪽이 나은 설계가 됩니다. 필요한 것은 “들어가기 전에 안다”, “복귀를 안다”, “복귀 후 다시 세운다”의 3가지입니다.
.NET에서는 Microsoft.Win32.SystemEvents.PowerModeChanged가 입구입니다.15
using Microsoft.Win32;
SystemEvents.PowerModeChanged += OnPowerModeChanged;
private void OnPowerModeChanged(object sender, PowerModeChangedEventArgs e)
{
switch (e.Mode)
{
case PowerModes.Suspend:
// 유예는 짧다: 버퍼를 플러시하고 계측 정지 시각을 기록하는 것에 국한한다
_logger.Info("suspend at {0:O}", DateTimeOffset.Now);
_collector.Pause();
break;
case PowerModes.Resume:
// 1) 결측 구간을 데이터로 남긴다
_logger.Info("resume at {0:O}", DateTimeOffset.Now);
// 2) 연결은 죽어 있다는 전제로 폐기하고 재연결한다
_connection.Reset();
// 3) 벽시계 기준으로 스케줄을 재계산하여 타이머를 다시 건다
_scheduler.Rebase(DateTimeOffset.Now);
// 4) 재정비가 끝난 후 수집을 명시적으로 재개한다(Pause한 채로 두지 않는다)
_collector.Resume();
break;
}
}
이 이벤트에는 공식적으로 명시된 2가지 주의점이 있습니다. 메시지 펌프가 돌고 있지 않으면 발생하지 않는다는 점(Windows 서비스에서는 숨김 폼 등이 필요), 그리고 static 이벤트이므로 구독 해제를 게을리하면 누수된다는 점입니다.15 GUI 앱이라면 그대로 사용할 수 있지만, 콘솔/서비스형 수집 앱에서는, Win32의 WM_POWERBROADCAST를 받는 메시지 창을 직접 준비하거나, HWND 없이 콜백을 받을 수 있는 PowerRegisterSuspendResumeNotification을 사용합니다(DAM 환경에서의 통지도 이 경로입니다).416
5.1. 창을 갖지 않는 앱에서 복귀를 받기
수집 도구나 Windows 서비스처럼 창을 갖지 않는 앱에서는, 메시지 창을 직접 만드는 것보다 PowerRegisterSuspendResumeNotification이 수고가 적습니다. 이 함수는 Flags에 DEVICE_NOTIFY_CALLBACK을 지정하고, Recipient에 콜백과 컨텍스트를 담은 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS에 대한 포인터를 전달하는 형태로, 성공 시 ERROR_SUCCESS(0)를 반환합니다. Windows 8 / Windows Server 2012 이후에서 사용할 수 있습니다.1928
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
// 창을 갖지 않는 콘솔/서비스에서 서스펜드・복귀를 받는 최소 예제
// 메시지 펌프는 불필요. Windows 8 / Windows Server 2012 이후
internal sealed class SuspendResumeNotifier : IDisposable
{
private const uint DEVICE_NOTIFY_CALLBACK = 2; // powrprof.h
private const uint PBT_APMSUSPEND = 0x0004; // winuser.h
private const uint PBT_APMRESUMEAUTOMATIC = 0x0012; // winuser.h
private const uint ERROR_SUCCESS = 0;
// ULONG DeviceNotifyCallbackRoutine(PVOID Context, ULONG Type, PVOID Setting)
private delegate uint DeviceNotifyCallbackRoutine(IntPtr context, uint type, IntPtr setting);
[StructLayout(LayoutKind.Sequential)]
private struct DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
{
public IntPtr Callback;
public IntPtr Context;
}
[DllImport("powrprof.dll")]
private static extern uint PowerRegisterSuspendResumeNotification(
uint flags,
ref DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS recipient,
out IntPtr registrationHandle);
[DllImport("powrprof.dll")]
private static extern uint PowerUnregisterSuspendResumeNotification(IntPtr registrationHandle);
// 델리게이트는 필드로 보관한다(로컬 변수인 채로 두면 GC에 회수되어 통지 시 죽는다)
private readonly DeviceNotifyCallbackRoutine _callback;
private IntPtr _registration;
// 해제에 실패한 등록은 OS 쪽에 남는다. 그 델리게이트만은 프로세스가
// 끝날 때까지 살려 두는 보관 장소(아래의 Dispose 참조)
private static readonly List<DeviceNotifyCallbackRoutine> AbandonedCallbacks = new();
public SuspendResumeNotifier()
{
_callback = OnPowerNotification;
var parameters = new DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS
{
Callback = Marshal.GetFunctionPointerForDelegate(_callback),
Context = IntPtr.Zero,
};
uint result = PowerRegisterSuspendResumeNotification(
DEVICE_NOTIFY_CALLBACK, ref parameters, out _registration);
if (result != ERROR_SUCCESS)
{
throw new InvalidOperationException(
$"PowerRegisterSuspendResumeNotification failed with {result}.");
}
}
private uint OnPowerNotification(IntPtr context, uint type, IntPtr setting)
{
// 이 메서드는 OS(네이티브)에서 직접 호출됩니다. managed 예외를
// 여기서 밖으로 내보내면, 받아줄 호출자가 managed 쪽에 없기 때문에
// 프로세스째로 죽습니다. 로그 기록 대상이 가득 참, collector가
// 이미 폐기됨과 같은 "일어날 수 있는" 실패로 슬립할 때마다
// 앱이 죽게 되므로, 안에서 완결시키고 반드시 코드를 반환합니다
try
{
switch (type)
{
case PBT_APMSUSPEND:
// 유예는 약 2초. 버퍼 플러시와 정지 시각 기록에만 국한한다
_logger.Info("suspend at {0:O}", DateTimeOffset.Now);
_collector.Pause();
break;
case PBT_APMRESUMEAUTOMATIC:
// 무인 복귀에서도 반드시 도착한다. 무거운 재정비는 콜백 밖으로 넘긴다
_logger.Info("resume at {0:O}", DateTimeOffset.Now);
_recovery.RequestRebuild();
break;
}
}
catch (Exception ex)
{
// 로그 출력 자체가 실패하고 있을 수도 있으므로, 여기도 보호한다
try { _logger.Error("power notification failed: {0}", ex); }
catch { /* 여기서 다시 던지면 위 try의 의미가 없어진다 */ }
}
return ERROR_SUCCESS; // Windows의 오류 코드를 반환한다
}
public void Dispose()
{
if (_registration == IntPtr.Zero)
{
return;
}
uint result = PowerUnregisterSuspendResumeNotification(_registration);
if (result != ERROR_SUCCESS)
{
// 해제되지 않았는데 핸들을 버리면, 등록은 OS 쪽에 살아 있는 채로
// 추적할 수 없게 된다. 그대로 이 인스턴스가 회수되면 _callback도
// 사라지고, OS는 해제된 함수 포인터를 호출하게 된다. 다음 서스펜드에서
// 프로세스째로 죽으므로, 델리게이트만은 살려 둔다
lock (AbandonedCallbacks)
{
AbandonedCallbacks.Add(_callback);
}
_logger.Error("PowerUnregisterSuspendResumeNotification failed with {0}.", result);
return; // 해제되지 않은 이상, 핸들은 버리지 않는다
}
_registration = IntPtr.Zero;
}
}
구현상의 주의점은 3가지입니다.
- 예외를 콜백 밖으로 내보내지 말 것. 이 메서드는 OS에서 직접 호출되므로, managed 예외가 경계를 넘으면, 받아줄 호출자가 managed 쪽에 없어 프로세스째로 죽습니다. 로그 출력 실패나 이미 폐기된 객체에 대한 접근처럼 실제로 일어날 수 있는 실패로 슬립할 때마다 앱이 죽게 됩니다. 본체 전체를
try/catch로 감싸고, 반드시 코드를 반환하십시오. - 콜백은 시스템 쪽 스레드에서 호출됩니다.
PBT_APMSUSPEND의 유예가 약 2초라는 것은 창을 경유하는 경우와 동일하므로, 여기서 재연결이나 네트워크를 거치는 마무리 작업을 해서는 안 됩니다.1718 복귀 처리도, 플래그를 세우고 별도 스레드에 맡기는 편이 안전합니다. - 등록 해제를 잊지 말 것.
PowerUnregisterSuspendResumeNotification에 등록 핸들을 전달합니다.29 반환값도 확인하십시오. 해제되지 않았는데 등록 핸들을 버리면, OS 쪽에는 등록이 남은 채로 추적 수단만 사라집니다. 그 상태에서 인스턴스가 회수되면 콜백의 델리게이트도 사라져, OS는 해제된 함수 포인터를 호출하게 됩니다. - 창 핸들로 받고 싶은 경우에는, user32.dll의
RegisterSuspendResumeNotification에DEVICE_NOTIFY_WINDOW_HANDLE을 전달하는 방법도 있습니다(이쪽은 평소대로WM_POWERBROADCAST가 날아옵니다).30
Windows 서비스로 만드는 경우에는, ServiceBase의 CanHandlePowerEvent를 true로 하고 OnPowerEvent(PowerBroadcastStatus)를 오버라이드하는 방법도 있습니다. 서비스 제어 관리자를 경유해 동일한 전원 이벤트가 도착하므로, P/Invoke 없이 작성할 수 있습니다.31
5.2. Win32 수준 이벤트의 의미
창을 경유하든 콜백을 경유하든, 도착하는 이벤트의 의미는 공통입니다. 여기는 짚어 두겠습니다.
- PBT_APMSUSPEND: 슬립 직전의 통지. 앱 하나당 약 2초밖에 유예가 없으며, 초과하면 시스템에 의해 중단될 수 있습니다. 여기서 할 일은 플러시와 시각 기록 정도로 국한하고, 네트워크를 거치는 마무리 작업처럼 시간이 걸리는 처리를 작성해서는 안 됩니다.1718
- PBT_APMRESUMEAUTOMATIC: 복귀할 때마다 반드시 도착하는 통지. 재연결・재스케줄링은 여기에 작성합니다.32
- PBT_APMRESUMESUSPEND: 사용자의 조작으로(또는 사용자가 돌아와서) 복귀한 경우에, PBT_APMRESUMEAUTOMATIC 뒤에 도착합니다. 원격 웨이크 등의 자동 복귀에서는 도착하지 않으므로, “복귀 처리”를 이쪽에만 작성하면 무인 복귀 시 재정비가 실행되지 않습니다.33
- 나아가, 배터리 부족 등의 긴급한 슬립 진입에서는 사전 통지 자체가 오지 않습니다.18 Suspend 이벤트를 받지 못한 경우에도 복귀 처리가 성립하도록, Resume 쪽을 멱등하게 작성해 둡니다.
5.3. 결측을 결측으로 기록한다
하나 더, 복귀 대응과 마찬가지로 중요한 것이 결측을 결측으로 기록하는 것입니다. 슬립을 걸친 수집 데이터는 “값이 없는” 것이 아니라 “시스템이 멈춰 있어서 측정하지 않은” 것이며, 그 구간을 suspend/resume 시각과 함께 로그와 데이터 양쪽에 남겨 두면, 나중에 그래프의 공백을 본 사람이 장애와 혼동하지 않게 됩니다. 장기 가동 앱의 로그에 무엇을 남겨야 하는가라는 관점은 “산업용 카메라 제어 앱이 1개월 뒤에 갑자기 떨어질 때(전편) - 핸들 누수의 발견 방법과 장기 가동용 로그 설계“에서도 다루고 있습니다.
또한, 슬립과 비슷한 “어느새 느려져 있었다” 계열의 문제로, Windows 11의 효율 모드(EcoQoS)에 의한 억제도 있습니다. 이쪽은 “Windows의 효율 모드란 무엇인가 - 녹색 잎 아이콘의 의미와 끄는 방법“을 참조하십시오.
6. 정해진 시각에 확실히 동작시킨다 ── 작업 스케줄러의 기동 실행
“새벽 2시에 집계해서 전송한다”와 같은 시각 기준 처리를, 상주 앱의 타이머+슬립 억제로 실현하는 것은 방향이 좋지 않습니다. 밤새 슬립을 죽여 두게 되기 때문입니다. 이 용도는 작업 스케줄러의 “작업 실행 시 절전 모드 해제”(WakeToRun)가 본명입니다.
WakeToRun을 활성화한 작업은, 실행 시각에 슬립・최대 절전 모드에서 컴퓨터를 기동시키고, 작업이 완료될 때까지 시스템을 깨어 있는 상태로 유지합니다(이미 깨어 있던 경우에도 완료까지 유지를 요구합니다). 기동 시 화면은 꺼진 채로 있는 경우가 있지만, 그것으로 정상입니다.20
다만, 기동이 성립하려면 조건이 있습니다. 공식 문제 해결 문서에도 언급되어 있듯이, 전원 옵션의 “절전 모드 해제 타이머 허용”(Allow Wake Timer)이 활성화되어 있을 것, BIOS 쪽의 웨이크 설정이 활성화되어 있을 것이 전제이며, 최근 노트북은 절전 설계상 웨이크를 허용하지 않는 구성도 드물지 않습니다.21 powercfg /waketimers로 현재 활성화된 웨이크 타이머를 나열할 수 있으므로12, 도입처의 실제 기기에서 “정말로 깨어나는지”를 반드시 검증하십시오. 자체 앱으로 기동시키고 싶은 경우에는, SetWaitableTimer의 fResume에 TRUE를 전달하는 웨이크 가능 타이머라는 수단도 있습니다. 이 경우 시스템은 자동 기동 후, 무인 유휴 타이머(최소 2분) 동안만 깨어 있으며, 앱이 SetThreadExecutionState로 “사용 중”을 선언하지 않으면 곧바로 재슬립합니다. 기동 후 처리가 길 경우에는 4장의 억제와 조합하는 것이 올바른 조합 기술입니다.23
작업 자체의 실행 계정・0x1로 끝나는 문제・다중 기동 방지 같은 운영 설계는 “작업 스케줄러 작업이 실행되지 않는다・0x1로 끝난다 ── 원인 분리와 안전한 운영 설계“에 정리해 두었습니다.
7. 판단표 ── 억제인가, 복귀 대응인가, 기동인가
세 가지 설계는 배타적이지 않으며, 앱 종류마다 주・보조를 정해 조합하는 것입니다.
| 앱 종류 | 첫 번째 후보 | 조합・보충 |
|---|---|---|
| 계측・데이터 수집(수 시간〜수일의 연속 측정) | 측정 구간만 ES_SYSTEM_REQUIRED로 억제 |
복귀 대응도 반드시 구현(사용자 조작의 슬립은 막을 수 없음10). 배터리 구동의 Modern Standby 기기는 5분 강제 종료가 있으므로 AC 급전을 요건으로 |
| 야간 배치・정시 전송 | 작업 스케줄러 + WakeToRun20 | 상주+억제보다 절전이면서 확실. 웨이크 타이머 허용 확인이 전제21 |
| 상주 감시・알림 에이전트 | 슬립 전제(PowerModeChanged로 감지→재연결・재스케줄링・결측 기록)15 | 감시가 주목적인 전용 기기라면, 앱이 아니라 전원 플랜 쪽에서 슬립을 비활성화 |
| 데스크톱 조작 도구(프레젠테이션・대시보드 표시 등) | 표시 중에만 ES_DISPLAY_REQUIRED를 ES_SYSTEM_REQUIRED와 병용10 |
표시 종료 시 반드시 해제. 상시 표시 단말이라면 전원 설정으로 대응 |
| 24/7 장치 제어・라인 PC | 전원 설정에서 슬립 자체를 비활성화(운영으로 담보) | 앱의 억제 API를 보험으로 삼지 않는다. powercfg /requests를 정기 점검에 사용12 |
판단의 축은 두 가지입니다. “멈춰도 되는 시간대가 있는가”(있다면 작업 스케줄러, 없다면 전원 설정)와, “누구의 PC에서 동작하는가”(사용자의 개인 소유・공용 노트북에서 동작하는 앱일수록, 억제보다 복귀 대응 쪽에 무게를 둔다)입니다.
7.1. “전원 설정에서 슬립 자체를 비활성화”의 내용
표에서 몇 번인가 나온 “전원 설정에서 비활성화”는, 구체적으로는 다음 조작입니다. 장치 제어 PC처럼 앱이 아니라 운영으로 슬립을 막는 경우는, 여기까지 해야 비로소 담보됩니다.
| 할 일 | 위치・명령 |
|---|---|
| 슬립까지의 시간을 “없음”으로 설정 | 설정 > 시스템 > 전원(노트북에서는 “전원 및 배터리”) > 화면 및 절전. 또는 제어판 > 하드웨어 및 소리 > 전원 옵션 > 플랜 설정 변경. 명령이라면 powercfg /change standby-timeout-ac 0(0은 슬립하지 않음을 의미)1234 |
| 화면 꺼짐까지의 시간도 재검토 | 같은 화면의 “디스플레이 끄기”. 명령이라면 powercfg /change monitor-timeout-ac 012 |
| 덮개 닫기・전원 버튼의 동작 변경 | 전원 옵션 > 전원 단추 작동 설정 > “아무 작업 안 함”을 선택. SetThreadExecutionState로는 막을 수 없는 것이 이 경로10 |
| 기종의 방식 확인 | powercfg /a. S3 기기인지 Modern Standby 기기인지에 따라 다음 주의점이 달라짐3 |
Modern Standby 기기에서는 한 단계 더 주의가 필요합니다. Modern Standby 지원 기기는, 화면이 꺼졌을 때 스탠바이에 들어갑니다. 공식 문서는 그 계기로 전원 버튼・덮개 닫기・시작 메뉴의 슬립에 더해 “시스템이 유휴 아웃했을 때”를 들고 있습니다.35 즉 S3 기기의 감각으로 “슬립의 타임아웃만 꺼 두면 괜찮다”고 생각하면, 화면 꺼짐에서 스탠바이로 들어가는 경로가 남습니다. 슬립과 화면 꺼짐 양쪽의 타임아웃을 재검토하고, powercfg /a로 방식을 확인한 후, 실제 기기에서 하룻밤 방치하여 멈추지 않는지 확인하는 단계까지 절차에 포함시키십시오.
배터리 구동 시의 취급도 잊기 쉽습니다. standby-timeout-ac는 AC 급전 시의 설정이며, 배터리 시에는 standby-timeout-dc가 사용됩니다. 장치용 PC가 노트북이라면, AC 급전을 전제 조건으로 명기해 두는 것이 안전합니다(4장에서 설명한 대로, 배터리 구동의 Modern Standby 기기에서는 전원 요청 자체가 5분 만에 강제 종료됩니다).13
8. 정리
- 슬립에는 S3・최대 절전 모드(S4)・Modern Standby(S0 low power idle)가 있으며,
powercfg /a로 판별할 수 있습니다. Modern Standby 기기에서도 데스크톱 앱은 DAM에 의해 서스펜드되므로, “슬립 중에도 동작한다”는 전제는 성립하지 않습니다. - 슬립 중에는 스레드도 타이머도 진행되지 않습니다. Windows 8 이후, 상대 타이머・대기는 슬립 시간을 세지 않고 남은 시간을 이월하며, .NET의 타이머도 .NET 11부터 같은 동작이 됩니다(.NET 10까지는 복귀 직후 발화할 수 있습니다). 경과 시간 API는 슬립 시간을 포함/포함하지 않음이 혼재합니다. 경과는 Stopwatch, 벽시계는 DateTime으로 갖고, 복귀 시 기준을 다시 잡으십시오.
- TCP 연결은 중간 장비의 유휴 타임아웃으로 조용히 죽습니다. 복귀 이벤트에서 폐기・재연결이 정석입니다.
- 억제는
SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)를 필요 구간에만. 사용자 조작의 슬립은 막을 수 없으며, Modern Standby 기기의 배터리 구동에서는 5분 만에 강제 종료됩니다. 확인은powercfg /requests. - 복귀 대응은
SystemEvents.PowerModeChanged/WM_POWERBROADCAST로. 서스펜드 통지의 유예는 약 2초, 통지 없는 슬립도 있으므로, Resume 쪽을 멱등하게 작성하고, 결측 구간을 기록합니다. - 정시 실행은 작업 스케줄러의 WakeToRun이 본명입니다. 웨이크 타이머의 전원 설정과 실제 기기에서의 기동 확인까지 포함해 설계하십시오.
관련 글
- 작업 스케줄러 작업이 실행되지 않는다・0x1로 끝난다 ── 원인 분리와 안전한 운영 설계
- Windows에서 타이머 대기보다 이벤트 대기를 우선하는 이유 - 약 15.6ms 입자의 폴링을 피한다
- TCP 재전송으로 산업용 카메라 통신이 수 초 멈출 때 - RFC1323 타임스탬프와 재전송 대기의 구분
- 산업용 카메라 제어 앱이 1개월 뒤에 갑자기 떨어질 때(전편) - 핸들 누수의 발견 방법과 장기 가동용 로그 설계
- Windows의 효율 모드란 무엇인가 - Windows 11의 녹색 잎 아이콘의 의미와 끄는 방법
관련 상담 영역
합동회사 코무라소프트에서는 계측・감시・데이터 수집 등 장시간 가동하는 Windows 앱의 설계와 구현, “한밤중에 멈춰 있었다”, “기록이 듬성듬성해진다” 같은 장기 가동 특유의 불량 조사를 다루고 있습니다. 슬립・전원 관리가 얽힌 재현하기 어려운 증상의 구분도 상담해 주십시오.
참고 링크
-
Microsoft Learn, System Sleeping States. S1〜S4가 슬립 상태이며 계산 작업을 실행하지 않는다는 점, S3는 메모리만 유지・S4는 최대 절전 모드 파일에 저장한다는 점, powercfg /a로 사용 가능한 슬립 상태를 나열할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, System power states. S0 low power idle(Modern Standby)에서는 저전력인 채로 시스템이 부분적으로 계속 동작한다는 점, Modern Standby 지원 SoC 시스템은 S1〜S3를 사용하지 않는다는 점, 긴급한 전환에서는 통지가 이루어지지 않는다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Overview of Modern Standby Testing and Diagnostics. powercfg /a로 Modern Standby 지원(Standby (S0 Low Power Idle) 표시)을 판별할 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Desktop Activity Moderator. DAM이 데스크톱 앱의 실행을 S3와 동등하게 억누른다는 점, 대화형 세션의 프로세스는 모든 스레드가 서스펜드되고 세션 0은 스로틀링된다는 점, 서스펜드 전의 WM_POWERBROADCAST 통지, 타이머 동작이나 가동 시간과 벽시계의 불일치, 연결 수명에 대한 고려가 필요하다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8
-
Microsoft Learn, SetWaitableTimerEx function. 상대 시간 지정 타이머가, Windows 7 이전에는 저전력 상태의 시간을 포함해(슬립 중에도 카운트다운이 진행되어) 있었던 것에 비해, Windows 8 이후에는 포함하지 않는다는(슬립 중에는 카운트다운이 진행되지 않는다) 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Environment.TickCount made consistent with Windows timeout behavior. Environment.TickCount/TickCount64가 .NET 10까지는 GetTickCount64 기반(슬립 시간을 포함)이며 .NET 11부터 QueryUnbiasedInterruptTime 기반(포함하지 않음)으로 변경되는 파괴적 변경, Windows 8 이후 타임아웃을 지정하는 대기 API(SleepEx/WaitForMultipleObjectsEx)가 비가동 시간을 세지 않게 되어 있었다는 점, 이 변경으로 복귀 직후 타이머가 발화하지 않게 되는 코드가 있을 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, Windows Time. GetTickCount/GetTickCount64의 경과 시간이 슬립・최대 절전 모드의 시간을 포함한다는 점, QueryUnbiasedInterruptTime은 working state의 시간만 포함한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Acquiring high-resolution time stamps. 관리형 코드의 System.Diagnostics.Stopwatch가 QPC를 시각 기반으로 한다는 점, QueryPerformanceCounter가 standby・hibernate・connected standby 중의 시간을 포함하는 틱 수를 반환한다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Load Balancer TCP Reset and Idle Timeout. 로드밸런서의 기본 동작이 유휴 타임아웃(기본 4분) 도달 시 흐름을 조용히 드롭하는 것이라는 점, TCP 리셋 전송은 명시적으로 활성화해야 하는 기능이라는 점, 대책으로 TCP keep-alive가 사용된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, SetThreadExecutionState function. ES_CONTINUOUS/ES_SYSTEM_REQUIRED/ES_DISPLAY_REQUIRED의 의미, ES_CONTINUOUS 없이는 유휴 타이머 재설정에 그친다는 점, 사용자에 의한 슬립 조작은 막을 수 없다는 점, 필요한 처리 동안만 설정하고 완료 후 해제하는 사용 예시에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, System Sleep Criteria. 시스템이 SetThreadExecutionState를 호출한 앱/스레드를 카운트하며, 카운트가 0이고 사용자 입력이 없으면 슬립에 들어간다는 점에 대해. ↩ ↩2
-
Microsoft Learn, Powercfg command-line options. /requests가 슬립이나 화면 꺼짐을 방해하는 전원 요청을 나열한다는 점, /requestsoverride로 특정 프로세스의 요청을 무시할 수 있다는 점, /waketimers로 활성화된 웨이크 타이머를 나열할 수 있다는 점에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9
-
Microsoft Learn, PowerSetRequest function. PowerRequestSystemRequired/PowerRequestExecutionRequired 등의 요청 종류, Modern Standby의 DC 전원에서는 요청이 슬립 타임아웃 초과 5분 후에 강제 종료된다는 점, 사용자 기인의 슬립 진입으로 요청이 종료된다는 점, 이유 문자열을 붙이고 직전에 Set・직후에 Clear하는 모범 사례에 대해. ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7
-
Microsoft Learn, PowerCreateRequest function. REASON_CONTEXT를 지정해 전원 요청 객체를 생성하고, 불필요해지면 CloseHandle로 해제한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, SystemEvents.PowerModeChanged Event. 서스펜드/레지움 시에 발생하는 이벤트라는 점, 메시지 펌프가 동작하지 않으면 발생하지 않는다는 점(서비스에서는 숨김 폼 등이 필요), static 이벤트이므로 구독 해제하지 않으면 누수된다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, WM_POWERBROADCAST message. 전원 관리 이벤트가 WM_POWERBROADCAST 메시지(PBT_APMSUSPEND/PBT_APMRESUMEAUTOMATIC/PBT_APMRESUMESUSPEND 등)로 창에 통지된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, PBT_APMSUSPEND event. 서스펜드 직전에 통지된다는 점, 처리 유예가 약 2초이며 초과하면 시스템에 의해 중단될 수 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, System Power Management Events. 크리티컬 배터리 등에 의한 긴급 서스펜드에서는 사전 통지가 이루어지지 않는다는 점, 서스펜드 통지 처리는 앱마다 최대 2초로 타임아웃된다는 점에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, PowerRegisterSuspendResumeNotification function. Flags에 DEVICE_NOTIFY_CALLBACK을 지정하고, Recipient에 DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS에 대한 포인터를 전달한다는 점, 콜백의 Type에 PBT_APMSUSPEND / PBT_APMRESUMESUSPEND / PBT_APMRESUMEAUTOMATIC이 들어온다는 점, 성공 시 ERROR_SUCCESS를 반환한다는 점, Windows 8 / Windows Server 2012 이후 Powrprof.dll에 포함된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, ITaskSettings::get_WakeToRun method. WakeToRun이 작업 실행 시 컴퓨터를 기동시키고 작업 완료까지 깨어 있는 상태로 유지한다는 점, 기동 시 화면이 꺼진 채로 있는 경우가 있다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, Automatic maintenance. 정시 기동이 동작하지 않는 경우의 확인 항목으로 BIOS의 웨이크 설정, 전원 옵션의 “Allow Wake Timer”, 작업의 WakeToRun 설정이 거론된다는 점, 최근 노트북에서는 S3 기동을 허용하지 않는 구성이 일반적이라는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, What is Modern Standby. Modern Standby가 S0 저전력 유휴 모델로 네트워크 연결을 유지한 채 즉시 온/오프를 실현한다는 점, 복귀(전원 버튼에서 화면 점등)가 1초 미만이라는 점에 대해. ↩
-
Microsoft Learn, System Wake-up Events. SetWaitableTimer의 fResume을 TRUE로 하면 타이머로 시스템을 기동할 수 있다는 점, 자동 기동 후에는 최소 2분의 무인 유휴 타이머가 설정되며 SetThreadExecutionState로 사용 중임을 나타내지 않으면 재슬립한다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft, .NET and .NET Core Support Policy. .NET의 메이저 릴리스가 매년 11월이라는 점, 최근인 .NET 10이 2025년 11월 11일 릴리스된 LTS 버전이라는 점에 대해. ↩
-
Microsoft Learn, QueryUnbiasedInterruptTime function. unbiased interrupt time이 working state의 시간만을 세며, 슬립・최대 절전 모드의 시간을 포함하지 않는다는 점에 대해. ↩
-
Microsoft Learn, Modern standby network connectivity. Adaptive Connected Standby에 의해, 배터리 구동 시에는 필요로 하는 시나리오가 없는 한 슬립 중의 네트워크 활동이 정지된다는 점에 대해. ↩
-
Microsoft Learn, PowerToys Awake utility. 전원 플랜을 변경하지 않고 PC를 깨어 있는 상태로 유지하는 유틸리티이며, 머신 상태를 요청하는 백그라운드 스레드를 생성하는 구조로 동작한다는 점, 종료하면 통상의 전원 플랜 동작으로 돌아간다는 점에 대해. ↩
-
Microsoft Learn, DEVICE_NOTIFY_SUBSCRIBE_PARAMETERS structure. 구조체가 Callback과 Context 2개의 멤버를 갖는다는 점, 콜백 DeviceNotifyCallbackRoutine이 ULONG(PVOID Context, ULONG Type, PVOID Setting) 형태로 Windows 오류 코드를 반환한다는 점에 대해. ↩
-
Microsoft Learn, PowerUnregisterSuspendResumeNotification function. 등록 핸들을 전달해 통지 등록을 해제한다는 점, 성공 시 ERROR_SUCCESS를 반환한다는 점에 대해. ↩
-
Microsoft Learn, RegisterSuspendResumeNotification function. Flags에 DEVICE_NOTIFY_WINDOW_HANDLE을 지정하면 창에 이벤트가 전달되고, DEVICE_NOTIFY_CALLBACK을 지정하면 콜백으로 받을 수 있다는 점에 대해. ↩
-
Microsoft Learn, ServiceBase.OnPowerEvent(PowerBroadcastStatus) Method. Windows 서비스가 컴퓨터의 전원 상태 변화를 받는 메서드이며, CanHandlePowerEvent 속성이 true일 때 오버라이드하는 것이 전제라는 점에 대해. ↩
-
Microsoft Learn, PBT_APMRESUMEAUTOMATIC event. 복귀할 때마다 반드시 전달되는 이벤트이며, 사용자의 존재를 나타내는 것은 아니라는 점, 사용자 활동을 감지하면 그 후에 PBT_APMRESUMESUSPEND가 전달된다는 점에 대해. ↩
-
Microsoft Learn, PBT_APMRESUMESUSPEND event. 사용자 기인・사용자 감지 시의 복귀에서 PBT_APMRESUMEAUTOMATIC 뒤에 전달된다는 점, 원격 웨이크에서는 PBT_APMRESUMEAUTOMATIC만 전달된다는 점에 대해. ↩
-
Microsoft Learn, Sleep idle timeout. 자동으로 슬립에 들어가기까지의 무조작 시간을 지정하는 설정이며, 최솟값 0이 “슬립에 들어가지 않음”을 의미한다는 점에 대해. ↩
-
Microsoft Learn, Prepare software for modern standby. Modern Standby에 들어가는 것은 화면이 꺼졌을 때이며, 그 계기가 전원 버튼・덮개 닫기・설정에서의 슬립 선택・시스템의 유휴 아웃이라는 점, DAM 단계 이후에는 데스크톱 앱의 실행이 멈춰지고 세션 0의 서비스는 스로틀링된다는 점, 전원 요청이 AC 급전에서는 무기한, DC 급전에서는 최대 5분 이 단계를 연장한다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
MAX_PATH와 Windows 경로·파일 이름의 함정 ── 260자 제한, 예약어, 끝 점, 대소문자
'파일을 찾을 수 없습니다'의 단골 원인인 경로·파일 이름 제한을 정리합니다. MAX_PATH=260자의 내역, LongPathsEnabled를 통한 긴 경로 활성화, CON 등의 예약어, 끝 점의 정규화, Path.Combine의 함정까지 설명...
네트워크 드라이브와 UNC 경로의 함정 ── 업무 앱에서 파일 서버(공유 폴더)를 다루는 실무
업무 앱에서 공유 폴더로 출력・감시할 때 자주 발생하는 문제를 정리합니다. 드라이브 문자(Z:)가 서비스에서 보이지 않는 이유, 실행 계정별로 필요한 권한, 에러 1219, FileSystemWatcher의 주의점까지 해설합니다.
Windows 앱의 작업 트레이 상주와 토스트 알림 ── NotifyIcon의 함정과 AppNotification 선택 기준
업무용 Windows 앱을 작업 트레이(알림 영역)에 상주시키고 토스트 알림으로 사용자에게 알리는 구현을 정리합니다. NotifyIcon의 올바른 사용법과 「닫으면 트레이로」설계, 탐색기 재시작 시 재등록, 토스트 API 3종(Windows Ap...
자사 개발 Windows 앱이 바이러스로 오탐되었다면 ── Microsoft Defender 오탐 대응과 성능 영향에 대처하는 법
자사 개발 Windows 앱이 Microsoft Defender에 오탐되었을 때의 정규 대응 방법을 정리합니다. 현대 백신 소프트웨어의 구조, Microsoft에 대한 오탐 신고, 격리로부터의 복원, 제외 설정을 올바르게 넣는 방법과 그 위험까지...
Arm판 Windows에서 업무 앱은 동작하는가 ── x64 에뮬레이션(Prism)과 네이티브 DLL·COM의 현실
「Arm판 Windows에서 업무 앱은 동작하는가」에 개발자·정보시스템 담당자를 위해 답합니다. x64 에뮬레이션(Prism)의 구조, 드라이버 등 동작하지 않는 계층, .NET의 AnyCPU와 P/Invoke 문제, Arm 대응 체크리스트까지 ...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
장애 조사 & 장기 가동 장애
간헐적 장애, 통신 진단, 장기 가동 크래시, 실패 경로 테스트 기반을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
기술 상담 & 설계 리뷰
설계 방향, 아키텍처 경계, 수명 관리, 기존 Windows 자산 처리 방법을 정리하는 데 도움을 드립니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 앱이 실행 중일 때만 PC를 슬립시키지 않으려면 어떻게 해야 하나요?
- 처리 시작 시 SetThreadExecutionState(ES_CONTINUOUS | ES_SYSTEM_REQUIRED)를 호출하고, 종료 시 SetThreadExecutionState(ES_CONTINUOUS)로 해제하는 것이 기본입니다. 화면도 켜진 상태로 유지하고 싶을 때만 ES_DISPLAY_REQUIRED를 추가합니다. 효과가 있는 것은 무조작 타임아웃에 의한 자동 슬립뿐이며, 사용자가 전원 버튼을 누르거나 덮개를 닫아 지시한 슬립은 막을 수 없습니다. 억제가 적용되고 있는지는 powercfg /requests로 확인할 수 있습니다.
- Modern Standby란 무엇인가요? 기존 슬립과 무엇이 다른가요?
- S0 low power idle이라고도 불리는 새로운 슬립 방식으로, 시스템은 저전력이지만 부분적으로 계속 동작하며 즉시 복귀할 수 있는 것이 특징입니다. Modern Standby 지원 기기는 기존의 S3 슬립을 지원하지 않습니다. 다만 '계속 동작하는' 것은 OS가 허용한 활동뿐이며, 데스크톱 앱은 DAM(Desktop Activity Moderator)에 의해 스레드 단위로 서스펜드되기 때문에, 앱 입장에서는 S3와 마찬가지로 멈춥니다. 자신의 PC가 어느 방식인지는 powercfg /a로 확인할 수 있습니다.
- 슬립에서 복귀한 후 TCP 연결을 사용할 수 없게 되는 이유는 무엇인가요?
- 슬립 중에는 앱이 통신할 수 없어 연결이 유휴 상태가 되고, 경로상의 NAT・방화벽・로드밸런서 등의 중간 장비가 유휴 타임아웃으로 흐름을 폐기하기 때문입니다. 대부분의 장비는 폐기를 통지하지 않고 조용히 끊어버리므로, 앱 쪽 소켓은 겉보기에는 정상인 채로, 복귀 후 첫 송수신에서 비로소 오류가 나거나 타임아웃까지 멈춰 버립니다. 복귀 이벤트를 받으면 연결을 의심하고 다시 만드는 것이 정석입니다.
- 야간 배치를 확실히 동작시키려면 슬립 억제와 작업 스케줄러 중 어느 쪽이 좋은가요?
- 작업 스케줄러의 '작업 실행 시 절전 모드 해제'(WakeToRun)가 첫 번째 후보입니다. 실행 시각에 PC를 깨우고 작업이 완료될 때까지 깨어 있는 상태를 유지해 주므로, 밤새 슬립을 죽여 둘 필요가 없습니다. 다만 웨이크 타이머가 전원 옵션에서 비활성화되어 있으면 깨어나지 않으므로, '절전 모드 해제 타이머 허용' 설정과 실제 기기에서의 기동 확인이 필수입니다. 상시 슬립 억제는 그동안의 전력을 낭비할 뿐 아니라 사용자의 전원 설정을 덮어써 버리는 무례한 설계가 됩니다.