수정 이력(1건, 최종 수정 2026년 09월 03일)
이 글에 적용한 변경 사항의 기록입니다. 보관해 둔 수정 전 버전은 DOI가 부여된 고정 URL에서 읽을 수 있습니다.
- 한국어 전면 재작성에 맞춰 본문 표현을 바로잡았습니다. 기술적인 주장은 일본어판과 같습니다.
- 최초 공개
이 글을 인용하기(DOI(등록된 아카이브): 10.5281/zenodo.22176675)
아래 DOI는 이전에 등록된 아카이브를 가리키며 현재 본문과 다를 수 있습니다. 현재 본문을 참조할 때는 이 페이지의 URL을 사용하세요.
Go Komura (2026). 「「응답 없음」의 정체 ── Windows가 앱을 「멈췄다」고 판단하는 구조와, 멈추지 않는 설계」. 합동회사 코무라소프트. https://comcomponent.com/ko/blog/windows-app-not-responding-hang-mechanism/
- DOI(등록된 아카이브)
- 10.5281/zenodo.22176675
- DOI(마지막 등록 버전)
- 10.5281/zenodo.22176676
「처리 중에 앱이 하얘지며 (응답 없음)이 뜬다」「가끔 멈춘다는 문의가 오는데, 개발 PC에서는 재현되지 않는다」── Windows 업무 앱에서 가장 흔한 불만 중 하나가 이 「응답 없음」입니다. 그리고 의외로 잘 알려지지 않은 점은, 「응답 없음」 표시를 내는 쪽이 멈춘 앱 자신이 아니라 OS라는 사실입니다.
Windows는 앱이 「멈췄다」는 것을 어떻게 알까요. 하얗게 흐려진 그 창은 무엇일까요. 이 글에서는 Windows에서 업무 앱을 만드는 개발자와, 멈추는 앱 문의를 받는 IT 담당자를 대상으로, 「응답 없음」 판단의 구조를 메시지 루프의 기초부터 풀어 설명하고, 멈추는 전형적인 원인, 멈추지 않는 설계, 멈춘 순간의 조사 절차까지 1차 정보를 바탕으로 정리합니다.
1. 먼저 결론
- 「응답 없음」은 OS의 판단입니다. 입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 메시지 꺼내기(
PeekMessage)를 하지 않은 창(과 그 창을 소유한 GUI 스레드)을 OS는 응답 없음으로 봅니다. 판단은 프로세스 단위가 아닙니다.1 - 하얘진 창은 「고스트 창」입니다. OS가 원래 창을 숨기고, 같은 위치·크기·외관의 가짜로 교체합니다. 할 수 있는 것은 이동·최소화·닫기뿐이며, 내용은 동작하지 않습니다. 디버거가 연결된 동안에는 고스트 창이 만들어지지 않습니다.2
- 멈추는 원인은 거의 하나로 귀결됩니다. 메시지 루프를 돌려야 할 UI 스레드가 무거운 처리나 대기에 막혀 있는 것입니다. 동기 I/O·네트워크 호출·락 대기·스레드 간
SendMessage가 전형적입니다.3 - 대책의 원칙은 「UI 스레드에서 기다리지 말고, 계산하지 않는다」입니다. 무거운 처리는 워커 스레드(C#이라면
async/await+Task.Run)로 옮기고, UI 스레드는 그리기·진행·취소 수락에 전념하게 합니다.4 DoEvents나 메시지 펌프를 손으로 돌리는 방식은 재진입 버그의 온상입니다. 응답 없음 표시는 사라지지만, 처리 도중에 임의의 이벤트가 끼어드는 구조가 됩니다. 회피가 아니라 분리가 정석입니다.- 조사는 「멈춰 있는 순간」의 상태 확보가 최우선입니다. 덤프를 떠서 UI 스레드의 스택을 보면, 무엇을 기다리다 멈췄는지는 거의 특정할 수 있습니다.
2. 전제: Windows 앱은 메시지로 동작한다
「응답 없음」을 이해하려면, 먼저 Windows GUI 앱이 이벤트 구동이라는 점을 잡아 두어야 합니다. GUI 앱은 입력을 스스로 가져오지 않고, OS가 전달하는 메시지(마우스, 키보드, 다시 그리기 요청, 타이머 등)를 받아 동작합니다.3
창을 만든 각 스레드는 메시지 큐를 가지며, 다음과 같은 메시지 루프를 돌립니다.
MSG msg;
while (GetMessage(&msg, NULL, 0, 0) > 0) {
TranslateMessage(&msg);
DispatchMessage(&msg); // 윈도우 프로시저가 호출된다
}
GetMessage로 큐에서 메시지를 꺼내고, DispatchMessage가 그 창의 윈도우 프로시저(메시지 처리 함수)를 호출합니다. 버튼 클릭 처리도, 다시 그리기도, WinForms나 WPF의 이벤트 핸들러도, 따져 보면 모두 이 루프의 한 바퀴 안에서 실행됩니다.5
flowchart TB
accTitle: 메시지 루프의 기본 구조
accDescr: OS가 마우스나 키보드 등의 입력을 스레드의 메시지 큐에 넣고, UI 스레드의 루프가 GetMessage로 꺼내 DispatchMessage로 윈도우 프로시저를 호출하며, 처리가 끝나면 루프 처음으로 돌아간다
os["OS(입력·다시 그리기 요청·타이머)"] --> q["스레드의 메시지 큐"]
q --> gm["GetMessage로 꺼낸다"]
gm --> dm["DispatchMessage"]
dm --> wp["윈도우 프로시저에서 처리"]
wp --> gm
그림 1: GUI 앱의 핵심은 메시지 루프이며, 이벤트 처리는 모두 이 루프의 한 바퀴로 실행된다.
이 구조에서 중요한 결론이 하나 있습니다. 윈도우 프로시저(이벤트 핸들러) 안에서 시간이 걸리는 처리를 하면, 그동안 루프는 다음 메시지를 꺼낼 수 없다는 점입니다. 클릭에도 다시 그리기 요청에도 반응하지 못하게 됩니다. 이것이 「멈춤」의 정체입니다.
메시지가 전달되는 경로가 두 가지라는 점도 잡아 둡니다. PostMessage는 큐에 넣고 바로 돌아오며, 루프가 순서대로 꺼내 처리합니다. 반면 SendMessage는 윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출 쪽으로 돌아오지 않습니다.36 이 차이는 4장의 deadlock 이야기에 그대로 이어집니다.
flowchart TB
accTitle: 메시지의 두 가지 전달 경로
accDescr: PostMessage는 큐에 넣고 바로 돌아오며, 메시지 루프가 순서대로 꺼내 처리한다. SendMessage는 윈도우 프로시저를 직접 호출하고, 처리가 끝날 때까지 호출 쪽으로 돌아오지 않는다
pm["PostMessage"] --> q2["큐에 넣는다(바로 돌아온다)"]
q2 --> loop["루프가 순서대로 꺼내 처리"]
sm["SendMessage"] --> direct["프로시저를 직접 호출"]
direct --> w2["처리가 끝날 때까지 돌아오지 않는다"]
그림 2: 같은 「메시지를 보낸다」라도, 큐를 거치는 Post와 완료를 기다리는 Send는 성질이 전혀 다르다.
3. 「응답 없음」 판단의 구조 ── 5초 규칙과 고스트 창
그렇다면 OS는 「이 앱이 멈췄다」는 것을 어떻게 알까요. 판단 기준은 공식 문서에 나와 있습니다. 입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 PeekMessage(메시지 꺼내기)를 호출하지 않은 창을 OS는 응답 없음으로 봅니다.1 즉 OS는 「메시지 루프가 제대로 돌고 있는지」를 맥박처럼 보고, 5초 동안 맥박이 없으면 응답 없음으로 판단합니다(이 5초라는 값은 앞으로 바뀔 수 있다고 명시되어 있습니다). 판단 단위는 창과 그 창을 소유한 GUI 스레드이며, UI 스레드가 여러 개인 앱에서는 하나가 멈춰도 다른 스레드의 창은 살아 있을 수 있습니다. 덤프에서 봐야 할 것은 멈춘 창의 소유 스레드입니다.
판단된 최상위 창에 일어나는 일도 문서화되어 있습니다. OS는 원래 창을 숨기고, 같은 Z-order·위치·크기·외관을 가진 「고스트 창(ghost window)」으로 교체합니다. 사용자가 할 수 있는 것은 이동·크기 변경·(강제) 닫기뿐입니다. 안의 앱은 실제로 응답하지 않으므로 그 밖의 조작은 할 수 없습니다.2
flowchart TB
accTitle: 응답 없음 판단과 고스트 창 교체 흐름
accDescr: UI 스레드가 무거운 처리에 막혀 메시지 꺼내기가 5초 멈추면, OS가 응답 없음으로 판단해 원래 창을 숨기고 같은 모습의 고스트 창으로 교체하며, 사용자에게는 이동·최소화·닫기만 제공한다
busy["UI 스레드가 무거운 처리에 막힌다"] --> stop["메시지 꺼내기가 멈춘다"]
stop --> judge{"5초가 지났는가?"}
judge -->|"아니요"| stop
judge -->|"예"| ghost["고스트 창으로 교체"]
ghost --> u1["제목에 (응답 없음) 표시"]
ghost --> u2["하얗게 흐려짐·이동과 닫기만 가능"]
그림 3: 「응답 없음」 표시도 흰 화면도, 멈춘 앱이 아니라 OS가 교체한 고스트 창의 것이다.
제목 표시줄에 붙는 「(응답 없음)」 문자열도, Aero 테마에서 하얗게 흐려지는 모습도, 모두 이 고스트 창의 것입니다. 여기서 실무적으로 두 가지 결론이 나옵니다.
- 「응답 없음」이 표시된 시점에, 그 창의 소유 스레드는 적어도 5초 동안 메시지를 처리하지 않은 것입니다. 「표시가 너무 빨리 뜬다」가 아니라, UI 스레드가 분명히 막혀 있습니다.
- 디버거가 연결되어 있으면 고스트 창은 만들어지지 않습니다.2 「디버그 실행에서는 응답 없음이 되지 않는데, 릴리스에서는 나온다」처럼 보이는 경우, 멈춤 자체는 같고 표시만 다른 일이 있습니다.
참고로, 프로세스 단위로 이 교체를 끄는 DisableProcessWindowsGhosting API도 있습니다.7 키오스크 단말기처럼 「OS가 임의로 창을 조작 가능한 것처럼 보이게 하지 않았으면 하는」 특수한 장면을 위한 것이며, 호출하면 「응답 없음」 표시는 나오지 않지만 멈춰 있다는 사실은 바뀌지 않습니다. 일반 앱이 응답 없음 대책으로 쓸 것은 아니라고 이해하세요.
4. 왜 멈추는가 ── UI 스레드를 막는 전형적인 패턴
원인을 따지면 「UI 스레드가 메시지 루프로 돌아오지 않는다」는 한 점이지만, 실무에서 만나는 모습은 몇 가지 전형으로 나눌 수 있습니다.
flowchart TB
accTitle: UI 스레드를 막는 전형적 원인의 분류
accDescr: 동기 I/O와 네트워크 호출, 락 대기, 스레드 간 SendMessage, COM의 STA 관련이라는 4계통의 전형적 원인은, 모두 UI 스레드가 메시지 루프로 돌아오지 못한다는 같은 한 점으로 귀결된다
c1["동기 I/O·네트워크 호출"] --> core["UI 스레드가 루프로 돌아오지 못한다"]
c2["락 대기"] --> core
c3["스레드 간 SendMessage"] --> core
c4["COM의 STA 관련"] --> core
core --> ar["응답 없음 판단으로"]
그림 4: 겉보기 증상은 같아도, 막고 있는 원인은 4계통으로 나눌 수 있고 대책이 각각 다르다.
동기 I/O와 네트워크 호출. 가장 많습니다. 버튼 클릭 핸들러 안에서 큰 파일 읽기/쓰기, 데이터베이스 쿼리, Web API 호출, 네트워크 드라이브 위 파일 접근을 동기로 하는 패턴입니다. 개발 PC에서는 소수점 몇 초로 끝나 눈치채지 못하고, 운영 환경의 네트워크 지연이나 파일 서버 이상으로 수십 초를 기다리게 되어 「가끔 멈춘다」는 문의가 됩니다. 네트워크 드라이브는 연결이 끊겼을 때 타임아웃이 길어, 증상을 극적으로 악화시킵니다.
락 대기. UI 스레드가 워커 스레드와 공유하는 데이터의 락을 잡으려다, 그 락을 오래 쥐고 있는 워커를 기다려 버리는 패턴입니다. 락의 규율은 멀티스레드 실무 시리즈에서 자세히 다루었습니다.
스레드 간 SendMessage. SendMessage는 대상 창의 프로시저가 처리를 끝낼 때까지 돌아오지 않습니다.6 다른 스레드의 창으로 보낸 경우, 그 스레드가 메시지를 처리할 수 있는 상태가 될 때까지 송신 쪽이 기다리게 됩니다. 여기서 대상 스레드도 무언가를 기다리고 있으면, 서로 기다리는 메시지 deadlock이 성립합니다.3 특히 HWND_BROADCAST로의 송신은, 응답하지 않는 창이 하나라도 있으면 말려듭니다. 기다릴 수 없는 장면에서는 SendMessageTimeout이나, 응답을 기다리지 않는 PostMessage를 검토합니다.8
sequenceDiagram
accTitle: 스레드 간 SendMessage에 의한 deadlock
accDescr: UI 스레드가 워커 스레드의 결과를 기다리다 블록된 상태에서, 워커 스레드가 UI 스레드의 창으로 SendMessage를 보내면, 서로 상대의 완료를 기다려 deadlock이 된다
participant U as UI 스레드
participant W as 워커 스레드
U->>U: 워커의 완료를 대기(블록)
W->>U: SendMessage(처리가 끝날 때까지 돌아오지 않는다)
Note over U: 메시지를 처리할 수 없다(대기 중)
Note over W: SendMessage에서 돌아오지 못한다
Note over U,W: 서로 기다려 deadlock
그림 5: 「UI 스레드가 워커를 기다리고, 워커가 SendMessage로 UI 스레드를 기다린다」는 전형적인 deadlock이다.
COM apartment 관련. STA 객체로의 호출은 창 메시지로 전달되므로, UI 스레드(STA)가 막히면 다른 스레드에서의 COM 호출도 함께 블록됩니다. 이 구조는 COM STA/MTA 기사에서 설명합니다.
「잠깐이니까」의 누적. 한 번에 50ms인 동기 처리라도, 루프에서 100번 호출하면 5초입니다. 응답 없음의 임계값은 5초이지만, 체감의 「묵직함」은 100ms 정도부터 시작됩니다. 설계의 기준은 「UI 스레드를 막아도 되는 것은 밀리초 단위까지」입니다.
5. 멈추지 않는 설계 ── 무거운 처리를 UI 스레드에서 빼낸다
대책의 원칙은 하나, 시간이 걸리는 일을 UI 스레드에서 빼내는 것입니다. C#(WinForms/WPF)라면 async/await가 가장 짧은 정석입니다.
private async void RunButton_Click(object sender, EventArgs e)
{
runButton.Enabled = false;
try
{
// CPU 부하가 높은 처리·동기 API만 있는 경우는 Task.Run으로 워커에
var result = await Task.Run(() => HeavyCalculation(input));
// I/O는 네이티브로 async인 API를 사용한다(스레드도 소비하지 않는다)
var data = await httpClient.GetStringAsync(url);
// await 뒤에는 UI 스레드로 돌아와 있으므로, 컨트롤을 직접 건드릴 수 있다
resultLabel.Text = result + data.Length.ToString();
}
catch (Exception ex)
{
// async void 핸들러에서 예외를 흘리면 앱이 죽는다. 여기서 받는다
MessageBox.Show($"처리에 실패했습니다: {ex.Message}");
}
finally
{
runButton.Enabled = true;
}
}
포인트는 세 가지입니다. 첫째, await로 기다리는 동안 UI 스레드는 메시지 루프로 돌아가 있으므로 응답 없음이 되지 않습니다. 둘째, await continuation은 UI 스레드로 돌아오므로, 이후에서 컨트롤을 그대로 건드릴 수 있습니다(워커 스레드에서 컨트롤을 직접 건드리는 것은 금지이며, 필요하면 Control.Invoke / Dispatcher.InvokeAsync를 사용합니다).4 셋째, 실행 중에는 버튼을 비활성화하는 등으로 재진입을 설계로 막는 것입니다.
Win32 네이티브의 경우도 구도는 같고, 워커 스레드에 일을 넘기고, 완료를 PostMessage로 자체 메시지로서 UI 스레드에 알리며, 윈도우 프로시저에서 화면을 갱신합니다. PostMessage는 큐에 넣기만 하고 바로 돌아오므로, 워커 쪽이 블록되지도 않습니다.6 워커 스레드의 대기 자체는 조건 변수 기사에서 다룬 규율로 작성합니다.
flowchart TB
accTitle: 멈추지 않는 앱의 역할 분담
accDescr: UI 스레드는 입력 수락·진행 표시·취소 수락만 담당하고, 무거운 처리는 워커 스레드가 실행한 뒤, 완료를 PostMessage나 await continuation으로 UI 스레드에 돌려준다
ui["UI 스레드: 입력·진행·취소"] -->|"일을 넘긴다"| w["워커 스레드: 무거운 처리"]
w -->|"PostMessage / await continuation"| ui
ui -.-> ng["UI 스레드에서의 동기 I/O·긴 계산은 금지"]
그림 6: UI 스레드는 「접수 담당」에 전념하고, 무거운 일은 반드시 워커에 넘겨 완료 알림만 받는다.
피해야 할 것은, 무거운 처리 사이에 Application.DoEvents()나 PeekMessage 루프를 끼워 넣어 표시만 살리는 기법입니다. 응답 없음은 피할 수 있지만, 처리 도중에 임의의 이벤트 핸들러가 재진입합니다. 버튼 두 번 누르기, 처리 중 폼 닫기, 타이머 실행 ── 모두 처리 중인 데이터를 깨뜨릴 수 있고, 타이밍에 따라 재현하기 어려운 버그가 됩니다. 메시지 펌프를 손으로 돌리는 것은 모달 진행 대화 상자 같은 한정된 구조 안에만 머물게 하고, 원칙은 분리로 해결하세요.
sequenceDiagram
accTitle: DoEvents에 의한 재진입 버그의 시계열
accDescr: 무거운 처리 도중에 DoEvents를 호출하면 쌓여 있던 클릭의 이벤트 핸들러가 끼어들어 실행되고, 처리 중인 데이터를 덮어쓴 뒤에 원래 처리가 재개되므로, 타이밍에 의존하는 데이터 파괴가 일어난다
participant U as UI 스레드
U->>U: 무거운 처리를 시작(데이터 가공 중)
U->>U: DoEvents(쌓인 메시지를 처리)
Note over U: 버튼 재클릭 핸들러가 끼어든다
U->>U: 끼어든 처리가 데이터를 덮어쓴다
U->>U: 원래 처리가 재개(데이터는 이미 불일치)
그림 7: DoEvents는 「응답 없음」을 없애는 대신, 처리 한가운데로 임의의 이벤트를 불러들인다.
장시간 처리에는 진행 표시와 취소도 설계에 넣습니다. IProgress<T>로 진행을 UI에 보내고, CancellationToken으로 중단을 전하는 형태로 두면, 사용자는 「움직이고 있다」는 것을 알 수 있어, 강제 종료(그것은 종종 데이터 손상의 원인이 됩니다)에 손을 뻗지 않게 됩니다.
flowchart TB
accTitle: 장시간 처리의 진행과 취소 흐름
accDescr: 워커 스레드는 IProgress를 통해 진행을 UI 스레드에 보내고, UI의 취소 조작은 CancellationToken을 통해 워커에 전달되며, 워커는 끊기 좋은 지점에서 중단하고 뒷정리를 한다
w3["워커: 장시간 처리"] -->|"IProgress로 진행"| ui2["UI: 진행 표시와 중지 버튼"]
ui2 -->|"CancellationToken"| w3
w3 --> stop2["끊는 지점에서 중단하고 뒷정리"]
그림 8: 진행은 「워커→UI」, 취소는 「UI→워커」. 양방향의 가는 연락 경로를 처음부터 설계에 넣는다.
6. 멈춘 순간의 조사 방법
「가끔 멈춘다」는 조사에서 가장 가치 있는 것은, 멈춰 있는 바로 그 순간의 스레드 상태입니다. 다시 시작해 버리면 증거는 사라집니다.
덤프를 뜬다. 작업 관리자의 세부 정보 탭에서 대상 프로세스를 마우스 오른쪽 단추로 클릭 → 「덤프 파일 만들기」. 이것만으로 모든 스레드의 스택이 들어간 전체 덤프를 뜰 수 있습니다. 문의를 받는 IT 담당 쪽에 「멈추면 닫지 말고 이것을 떠 달라」고 전해 두는 것만으로, 조사 성공률은 크게 달라집니다. 수집 체계를 만드는 방법은 크래시 덤프 수집 기사를 참고하세요.
UI 스레드의 스택을 본다. WinDbg로 덤프를 열고, 메시지 루프를 돌리고 있는 스레드(보통 스레드 0)의 스택을 봅니다. 동기 I/O라면 ReadFile이나 네트워크 API, 락 대기라면 WaitFor… 계열, 스레드 간 SendMessage라면 SendMessage 내부에서 기다리는 모습이 그대로 찍혀 있습니다. 읽는 법은 WinDbg 입문 기사에서 설명합니다.
살아 있는 채로 본다. Process Explorer라면 스레드 목록과 스택을 그 자리에서 확인할 수 있습니다. 상시 묵직한 경우에는 WPR로 트레이스를 떠서 UI 스레드의 대기를 시계열로 분석합니다(WPR/WPA 실전).
flowchart TB
accTitle: 응답 없음 조사의 기본 절차
accDescr: 멈춰 있는 순간에 덤프를 뜨고, UI 스레드의 스택을 보고, 동기 I/O·락 대기·스레드 간 SendMessage 중 어디에서 멈췄는지를 특정한 뒤, 해당하는 설계 수정으로 연결한다
hang["멈춰 있는 순간"] --> dump["덤프를 뜬다(닫기 전에)"]
dump --> stack["UI 스레드의 스택을 본다"]
stack --> io["동기 I/O·네트워크 대기"]
stack --> lock["락 대기"]
stack --> sm["스레드 간 SendMessage"]
io -.-> fix["해당 지점을 워커로 분리"]
lock -.-> fix
sm -.-> fix
그림 9: 조사의 주역은 「멈춰 있는 순간의 덤프」이며, UI 스레드의 스택이 그대로 원인의 분류가 된다.
증상에서 감을 잡는 방법도 전형화할 수 있습니다. 특정 조작에서 반드시 멈추면, 그 핸들러 안의 동기 I/O를 먼저 의심합니다. 드물게 멈추고 조작과 상관이 없으면, 락 순서나 스레드 간 SendMessage의 deadlock을 의심하고, 덤프에서 양쪽 스레드의 대기 대상을 맞춰 봅니다. 특정 환경에서만 멈추면, 네트워크 드라이브·프록시·백신 등 환경 요인의 타임아웃을 의심하는 흐름입니다.
7. 정리
- 「응답 없음」은, 5초 동안 메시지를 꺼내지 않는 앱을 OS가 판단해 고스트 창으로 교체하는 구조입니다. 표시를 내는 것은 앱이 아니라 OS입니다.
- 멈추는 원인은 「UI 스레드가 메시지 루프로 돌아오지 못한다」는 한 점입니다. 동기 I/O·네트워크·락 대기·스레드 간 SendMessage가 전형적입니다.
- 대책은 UI 스레드에서 무거운 처리를 빼내는 것입니다. C#이라면
async/await+Task.Run, Win32라면 워커 스레드 +PostMessage. 실행 중 재진입은 버튼 비활성화 등의 설계로 막습니다. DoEvents로 피하는 것은 재진입 버그와 맞바꾸는 일입니다.DisableProcessWindowsGhosting은 표시만 없앱니다. 둘 다 근본 대책이 아닙니다.- 조사는 「멈춰 있는 순간」의 덤프가 가장 중요합니다. 원인은 UI 스레드의 스택에 거의 그대로 찍혀 있습니다.
「응답 없음」은 사용자에게는 「고장」이지만, 구조를 알면 「UI 스레드가 5초 동안 돌아오지 않았다」는 정확한 한 문장으로 옮길 수 있습니다. 그 한 문장에서 거꾸로 가면, 원인 후보도, 고치는 방법도, 조사 절차도 자연스럽게 정해집니다.
관련 기사
- 스퓨리어스 웨이크업 ── 조건 변수가 「알림 없이 깨어나는」 이유와 Windows에서 올바르게 기다리는 방법
- 멀티스레드의 실무 베스트 프랙티스 .NET 편
- COM STA/MTA의 기초 지식 - 스레드 모델과 hang을 피하는 생각법
- WinDbg + SOS로 크래시 덤프 읽기 ── 수집한 뒤의 실무 분석 입문
- Process Explorer / Handle / VMMap 실전 ── hang·누수·「파일이 사용 중」을 지금 이 순간의 상태에서 추적한다
- 앱에서 본 Windows 종료 ── 종료 알림·다시 시작·전원 차단을 올바르게 견딘다
관련 상담 영역
합동회사 코무라소프트에서는 「가끔 멈춘다」, 「응답 없음이 된다」는 업무 앱의 원인 조사(덤프 분석·트레이스 분석), 동기 처리로 가득한 레거시 UI 코드의 async/await화·워커 스레드 분리 개수, 멈추지 않는 UI 설계 리뷰를 다루고 있습니다. 재현 절차가 아직 없는 단계에서도, 증거를 잡는 방법의 설계부터 도울 수 있습니다.
참고 링크
-
Microsoft Learn, IsHungAppWindow function (winuser.h). 앱이 「입력을 기다리지 않고, 시작 처리 중도 아니며, 내부 타임아웃인 5초 동안 PeekMessage를 호출하지 않은」 경우 응답 없음으로 본다는 판단 기준, 이 5초 기준은 바뀔 수 있다는 점, 고스트 창에 대해서는 항상 TRUE가 반환된다는 점에 대해. ↩ ↩2
-
Microsoft Learn, GetMessage function (winuser.h). 최상위 창이 수 초 동안 메시지에 응답하지 않게 되면 시스템이 그 창을 응답 없음으로 보고, 같은 Z-order·위치·크기·외관을 가진 고스트 창으로 교체한다는 점, 사용자는 이동·크기 변경·닫기만 할 수 있다는 점, 디버거가 연결된 동안에는 고스트 창이 만들어지지 않는다는 점에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, About Messages and Message Queues. Windows 앱이 이벤트 구동이며 윈도우 프로시저가 메시지를 처리하는 구조, 큐를 거치는 메시지와 직접 보내지는 메시지의 구별, 응답 없는 창의 고스트 창 교체, 스레드 간에 메시지를 주고받아 생기는 deadlock 절에 대해. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, How to make thread-safe calls to controls (Windows Forms). WinForms 컨트롤이 만들어진 스레드 이외에서는 안전하게 건드릴 수 없다는 점, 다른 스레드에서의 갱신에는 Invoke/BeginInvoke를 사용한다는 점, async/await나 BackgroundWorker를 사용한 안전한 비동기 패턴에 대해. ↩ ↩2
-
Microsoft Learn, Using Messages and Message Queues. GetMessage·TranslateMessage·DispatchMessage에 의한 표준 메시지 루프 구현 예와, 메시지 큐를 조사하는 방법에 대해. ↩
-
Microsoft Learn, SendMessage function (winuser.h). SendMessage가 지정 창의 윈도우 프로시저를 호출하고 처리가 끝날 때까지 돌아오지 않는다는 점, 다른 스레드의 창으로 보내면 상대 스레드가 메시지를 처리할 때까지 기다리게 된다는 점, 응답을 기다리지 않고 큐에 넣는 PostMessage와의 차이에 대해. ↩ ↩2 ↩3
-
Microsoft Learn, DisableProcessWindowsGhosting function (winuser.h). 호출한 GUI 프로세스에 대해, 응답 없는 창을 최소화·이동·닫을 수 있게 하는 고스트 창 기능을 끌 수 있다는 점, 해제는 프로세스 수명 동안 유지된다는 점에 대해. ↩
-
Microsoft Learn, SendMessageTimeout function (winuser.h). 타임아웃을 두고 메시지를 보낼 수 있다는 점, 응답하지 않는 창(hang으로 판단된 창)에 대해 기다리지 않고 돌아오는 플래그(SMTO_ABORTIFHUNG)가 마련되어 있다는 점에 대해. ↩
관련 기사
같은 태그를 공유하는 최신 기사입니다. 더 가까운 주제로 지식을 넓힐 수 있습니다.
Windows 앱 외주·수탁 개발을 의뢰하기 전에 정리할 점
Windows 앱 외주·수탁 개발을 의뢰하기 전에, 기존 소프트웨어 수정, 장치 연동, COM/ActiveX, 배포·업데이트, 유지보수를 정리하는 포인트를 설명합니다.
Windows 프린터 드라이버 제공 종료 ── 업무 앱의 장표·라벨 인쇄는 어떻게 대비할 것인가
Microsoft는 v3/v4 프린터 드라이버의 제공 종료를 단계적으로 진행하고 있으며, 2026년 7월부터는 IPP 클래스 드라이버가 우선됩니다. Windows protected print mode에서 무엇이 사라지는지, 업무 앱의 장표·라벨 ...
Win32 스레드 풀 API ── CreateThreadpoolWork로 「스레드를 만들지 않는」 병렬 처리
네이티브 코드에서 CreateThread를 마구 늘리고 있지는 않습니까. Vista에서 새로 설계된 Win32 스레드 풀 API의 work, timer, wait, io 네 가지 객체와 클린업 그룹, 콜백에서 해서는 안 되는 일까지 1차 자료를 ...
절전에서 재개하면 깨지는 앱 ── 전원 이벤트의 구조와 재개에 강한 업무 앱을 만드는 방법
노트북을 열었더니 업무 앱의 통신이 끊어져 있었다――원인은 절전을 전제하지 않은 설계입니다. WM_POWERBROADCAST에 의한 알림의 흐름, Modern Standby의 동작, 끊김·재연결 설계, 절전 억제와 조사 명령까지를 1차 정보로 설...
DllMain과 로더 락 ── 「DLL 초기화에서는 아무것도 하지 말라」는 말의 진짜 이유
DllMain에서 LoadLibrary나 스레드 동기화를 해서는 안 되는 이유는 무엇인가. 모든 DLL 알림을 직렬화하는 로더 락의 구조부터, 데드락이 성립하는 전형적인 시나리오, 지연 초기화 같은 올바른 설계, hang 조사 절차까지를 1차 정...
관련 토픽
이 기사와 가까운 토픽 페이지입니다. 기사를 출발점 삼아 관련 서비스와 다른 기사로 이어집니다.
Windows 기술 토픽
Windows 개발, 장애 조사, 기존 자산 활용에 관한 KomuraSoft LLC 기사를 모은 토픽 허브입니다.
UI 스레드 & 타이머
WPF / WinForms UI 스레드, async 흐름, Dispatcher 사용, 타이머 판단을 정리한 토픽 페이지입니다.
장애 조사 & 장기 가동 장애
간헐적 장애, 통신 진단, 장기 가동 크래시, 실패 경로 테스트 기반을 정리한 토픽 페이지입니다.
이 주제와 연결되는 서비스
이 기사는 다음 서비스 페이지로 이어집니다. 가까운 입구부터 확인해 주세요.
Windows 앱 개발
상주 처리, 장비 연동, 운영 로그, 유지 보수 가능한 구조가 필요한 Windows 데스크톱 애플리케이션을 지원합니다.
장애 조사 & 원인 분석
간헐적 장애, 장기 가동 중 크래시, 누수, 통신 중단 등 까다로운 프로덕션 이슈를 조사합니다.
자주 묻는 질문
이 기사 주제에 대해 상담 시 자주 나오는 질문을 모았습니다.
- 「응답 없음」은 어떤 조건에서 표시되나요?
- OS는 창이 있는 앱이 「입력을 기다리지 않고, 시작 처리 중도 아니며, 5초 동안 메시지 꺼내기(PeekMessage)를 하지 않은」 경우 그 창을 응답 없음으로 판단합니다. 판단된 최상위 창은 숨겨지고, 같은 위치·크기·외관의 「고스트 창」으로 교체됩니다. 제목 표시줄의 「(응답 없음)」과 하얗게 흐려진 모습은 이 고스트 창의 것이며, 이동·최소화·닫기만 할 수 있습니다. 즉 「응답 없음」은 앱 자신의 표시가 아니라 OS가 대신 보여주는 화면입니다.
- 처리 중에 「응답 없음」이 나오지 않게 하는 설정이 있나요?
- DisableProcessWindowsGhosting을 호출하면 그 프로세스에 대한 고스트 창 교체를 끌 수 있습니다. 다만 이는 「멈춰 있다는 사실을 사용자에게 덜 보이게 할」 뿐이며, 창이 조작에 반응하지 않는다는 사실은 그대로입니다. 사용자에게는 이동도 닫기도 할 수 없는 완전한 멈춤으로 보입니다. 근본 대책은 표시를 막는 것이 아니라, UI 스레드를 5초는커녕 0.1초 단위로도 막지 않도록 무거운 처리를 워커 스레드로 옮기는 것입니다. 참고로 디버거가 연결된 동안에는 OS가 고스트 창을 만들지 않으므로, 「디버그 중에는 응답 없음이 되지 않는」 것처럼 보일 수 있습니다.
- DoEvents(메시지 펌프를 수동으로 돌리기)로 응답 없음을 피해도 되나요?
- 권장하지 않습니다. 무거운 처리 도중에 DoEvents나 PeekMessage 루프를 돌리면 응답 없음 판단은 피할 수 있지만, 처리 도중에 버튼 재클릭·창 닫기·타이머 등 임의의 이벤트 핸들러가 재진입합니다. 처리 중인 데이터를 다른 핸들러가 덮어쓰거나, 닫힌 폼을 건드려 예외가 나는 식의 재진입 버그는 타이밍에 따라 재현하기 어렵고, 응답 없음보다 다루기 어렵습니다. 정석은 처리 자체를 Task.Run 등으로 워커 스레드에 옮기고, UI 스레드는 진행 표시와 취소 수락만 맡기는 것입니다.
- 워커 스레드에서 화면(컨트롤)을 갱신하려면 어떻게 하나요?
- WinForms 컨트롤이나 WPF 요소는 만든 스레드(보통 UI 스레드)에서만 건드릴 수 있습니다. 워커 스레드에서 직접 건드리면 예외나 비결정적 동작이 됩니다. C#에서는 async/await가 가장 간단합니다. await continuation은 호출한 UI 스레드로 돌아오므로, await 뒤에서 컨트롤을 그대로 갱신할 수 있습니다. 명시적으로 전환할 때는 WinForms에서는 Control.Invoke/BeginInvoke, WPF에서는 Dispatcher.InvokeAsync를 사용합니다. Win32 네이티브에서는 워커 스레드에서 PostMessage로 자체 완료 메시지를 UI 스레드에 보내고, 윈도우 프로시저 쪽에서 화면을 갱신하는 것이 정석입니다.
- 「응답 없음」 상태인 앱의 원인은 어떻게 조사하나요?
- 멈춰 있는 「바로 그 순간」의 상태를 확보하는 것이 중요합니다. 먼저 작업 관리자의 세부 정보 탭에서 「덤프 파일 만들기」로 전체 덤프를 뜬 뒤, WinDbg에서 UI 스레드(메시지 루프를 도는 스레드)의 스택을 봅니다. 동기 I/O·네트워크 대기·락 대기·SendMessage로 다른 스레드를 기다리는지 여부는 스택에 그대로 나타납니다. 살아 있는 상태에서 보려면 Process Explorer의 스레드 목록과 스택 표시가, 시계열로 따라가려면 WPR 트레이스 수집이 유효합니다. 이 사이트의 WinDbg 입문·Process Explorer 실전·WPR/WPA 실전 기사도 참고하세요.