Windows API PostMessage vs SendMessage 완벽 가이드 — 동기·비동기 메시지 전송
이 글의 핵심
Windows API의 PostMessage와 SendMessage 차이점 완벽 정리. 동기 vs 비동기, 메시지 큐 동작 원리, 데드락 방지, 실전 패턴
Windows API PostMessage vs SendMessage 완벽 가이드
이 글을 읽으면 Windows 프로그래밍에서 PostMessage와 SendMessage의 차이를 이해하고, 상황에 맞게 올바르게 사용하는 방법을 배울 수 있습니다.
Windows API에서 메시지 전송은 GUI 프로그래밍의 핵심입니다. 버튼 클릭, 키보드 입력, 타이머 등 모든 것이 메시지로 처리됩니다. 하지만 PostMessage와 SendMessage의 차이를 제대로 이해하지 못하면 UI가 멈추거나 데드락이 발생할 수 있습니다.
이 글에서는 두 함수의 동작 원리, 메시지 큐의 구조, 실전 사용 패턴을 다룹니다.
헷갈리기 쉬운 것: PostMessage는 “보내고 바로 리턴” (비동기), SendMessage는 “처리 완료될 때까지 대기” (동기)입니다. 잘못 사용하면 UI가 얼어붙을 수 있습니다.
1. 핵심 차이: 동기 vs 비동기
SendMessage (동기)
// SendMessage: 메시지를 보내고 처리가 완료될 때까지 대기
LRESULT result = SendMessage(
hWnd, // 대상 윈도우
WM_USER + 100, // 메시지 ID
wParam, // 매개변수 1
lParam // 매개변수 2
);
// ← 여기 도달하면 대상 윈도우가 처리 완료한 것
특징:
- ⏱️ 동기 처리: 대상 윈도우가 메시지를 처리할 때까지 블록
- ✅ 반환값 확인 가능: 처리 결과를
LRESULT로 받을 수 있음 - 🔄 즉시 실행: 메시지 큐를 거치지 않고 바로
WndProc호출 - ⚠️ 데드락 위험: 상호 SendMessage 시 무한 대기 가능
PostMessage (비동기)
// PostMessage: 메시지를 큐에 넣고 즉시 리턴
BOOL success = PostMessage(
hWnd, // 대상 윈도우
WM_USER + 100, // 메시지 ID
wParam, // 매개변수 1
lParam // 매개변수 2
);
// ← 즉시 도달 (메시지는 아직 처리되지 않음)
특징:
- ⚡ 비동기 처리: 메시지를 큐에 넣고 즉시 리턴
- ❌ 반환값 없음: 성공/실패만 확인 가능 (
BOOL) - 📬 큐 경유: 메시지 루프가
GetMessage로 꺼내서 처리 - ✅ 데드락 안전: 보내고 바로 리턴하므로 안전
2. 메시지 큐 동작 원리
메시지 흐름 비교
SendMessage 흐름:
[호출 스레드] → (직접 호출) → [대상 WndProc] → (결과 반환) → [호출 스레드]
└─ 즉시 실행, 큐 거치지 않음
PostMessage 흐름:
[호출 스레드] → [메시지 큐에 삽입] → (즉시 리턴)
↓
[메시지 루프]
↓
GetMessage/DispatchMessage
↓
[대상 WndProc]
메시지 루프의 역할
// 전형적인 메시지 루프
while (GetMessage(&msg, NULL, 0, 0)) {
TranslateMessage(&msg);
DispatchMessage(&msg); // ← 여기서 WndProc 호출
}
PostMessage로 보낸 메시지:
GetMessage가 큐에서 꺼냄DispatchMessage가WndProc호출- 큐가 비면
GetMessage는 대기 (CPU 사용 없음)
SendMessage는:
- 메시지 루프를 거치지 않음
- 대상 스레드의
WndProc를 직접 호출 - 대상이 다른 스레드면 내부적으로 동기화
3. 실전 사용 예제
예제 1: UI 업데이트 (같은 스레드)
// ✅ SendMessage: 즉시 업데이트 필요
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_COMMAND:
if (LOWORD(wp) == ID_BUTTON_CLICK) {
// 진행률 표시줄 즉시 업데이트
SendMessage(hProgressBar, PBM_SETPOS, 50, 0);
// 작업 수행
DoSomething();
// 완료 후 100%로
SendMessage(hProgressBar, PBM_SETPOS, 100, 0);
}
break;
}
return DefWindowProc(hWnd, msg, wp, lp);
}
예제 2: 스레드 간 통신
// ❌ 나쁜 예: UI 스레드에서 SendMessage로 작업 스레드에 전송
DWORD WINAPI WorkerThread(LPVOID param) {
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
if (msg.message == WM_USER_PROCESS) {
// 무거운 작업
Sleep(5000); // ← UI 스레드가 5초 멈춤!
}
}
return 0;
}
// UI 스레드에서
SendMessage(hWorkerWnd, WM_USER_PROCESS, 0, 0); // ❌ UI 얼어붙음!
// ✅ 좋은 예: PostMessage 사용
DWORD WINAPI WorkerThread(LPVOID param) {
MSG msg;
while (GetMessage(&msg, NULL, 0, 0)) {
if (msg.message == WM_USER_PROCESS) {
// 무거운 작업
Sleep(5000);
// 완료 알림
PostMessage(hMainWnd, WM_USER_COMPLETE, 0, 0);
}
}
return 0;
}
// UI 스레드에서
PostMessage(hWorkerWnd, WM_USER_PROCESS, 0, 0); // ✅ 즉시 리턴
예제 3: 사용자 정의 메시지
// 메시지 정의
#define WM_UPDATE_STATUS (WM_USER + 1)
#define WM_DOWNLOAD_DONE (WM_USER + 2)
// 다운로드 스레드
DWORD WINAPI DownloadThread(LPVOID param) {
HWND hMainWnd = (HWND)param;
for (int i = 0; i <= 100; i += 10) {
// 진행률 업데이트 (비동기)
PostMessage(hMainWnd, WM_UPDATE_STATUS, i, 0);
Sleep(500);
}
// 완료 알림
PostMessage(hMainWnd, WM_DOWNLOAD_DONE, 0, 0);
return 0;
}
// 메인 윈도우
LRESULT CALLBACK MainWndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
switch (msg) {
case WM_UPDATE_STATUS:
// 진행률 바 업데이트
SendMessage(hProgressBar, PBM_SETPOS, wp, 0);
break;
case WM_DOWNLOAD_DONE:
MessageBox(hWnd, L"Download Complete!", L"Info", MB_OK);
break;
}
return DefWindowProc(hWnd, msg, wp, lp);
}
4. 데드락 방지하기
데드락 발생 시나리오
// ❌ 데드락 예제
// 스레드 A의 윈도우
LRESULT CALLBACK WndProcA(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
if (msg == WM_USER_ACTION) {
// 스레드 B에게 SendMessage
SendMessage(hWndB, WM_USER_QUERY, 0, 0); // ← 대기
}
return 0;
}
// 스레드 B의 윈도우
LRESULT CALLBACK WndProcB(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
if (msg == WM_USER_QUERY) {
// 스레드 A에게 SendMessage
SendMessage(hWndA, WM_USER_REPLY, 0, 0); // ← 데드락!
}
return 0;
}
문제: A → B로 SendMessage, B → A로 SendMessage → 무한 대기
해결책 1: PostMessage 사용
// ✅ PostMessage로 변경
LRESULT CALLBACK WndProcB(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
if (msg == WM_USER_QUERY) {
// 비동기로 응답
PostMessage(hWndA, WM_USER_REPLY, 0, 0); // ✅ 즉시 리턴
}
return 0;
}
해결책 2: SendMessageTimeout
// ✅ 타임아웃 설정
DWORD_PTR result;
LRESULT lResult = SendMessageTimeout(
hWndB,
WM_USER_QUERY,
0, 0,
SMTO_NORMAL, // 플래그
5000, // 5초 타임아웃
&result // 결과
);
if (lResult == 0) {
// 타임아웃 또는 실패
DWORD error = GetLastError();
if (error == ERROR_TIMEOUT) {
// 타임아웃 처리
}
}
5. 성능 고려사항
SendMessage의 오버헤드
// ❌ 나쁜 예: 루프 안에서 SendMessage
for (int i = 0; i < 10000; i++) {
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)items[i]);
// 매번 동기 호출 → 느림!
}
// ✅ 좋은 예: 배치 처리
SendMessage(hListBox, WM_SETREDRAW, FALSE, 0); // 리드로우 중지
for (int i = 0; i < 10000; i++) {
SendMessage(hListBox, LB_ADDSTRING, 0, (LPARAM)items[i]);
}
SendMessage(hListBox, WM_SETREDRAW, TRUE, 0); // 리드로우 재개
InvalidateRect(hListBox, NULL, TRUE); // 한 번에 그리기
PostMessage 큐 오버플로우
// ⚠️ 주의: 너무 많은 PostMessage
for (int i = 0; i < 100000; i++) {
PostMessage(hWnd, WM_USER_DATA, i, 0);
// 큐가 가득 차면 실패 (약 10,000개 제한)
}
// ✅ 해결: 큐 상태 확인
for (int i = 0; i < 100000; i++) {
while (!PostMessage(hWnd, WM_USER_DATA, i, 0)) {
Sleep(10); // 큐가 비워질 때까지 대기
}
}
6. 데이터 전달 방법
WPARAM/LPARAM으로 전달
// ✅ 정수 전달
PostMessage(hWnd, WM_USER_PROGRESS, 75, 0); // 진행률 75%
// ✅ 포인터 전달 (같은 프로세스)
int* pData = new int(42);
PostMessage(hWnd, WM_USER_DATA, 0, (LPARAM)pData);
// 수신 측에서
case WM_USER_DATA:
int* pData = (int*)lParam;
// 사용 후 반드시 삭제
delete pData;
break;
WM_COPYDATA (크로스 프로세스)
// 다른 프로세스에 문자열 전송
const wchar_t* message = L"Hello from another process";
COPYDATASTRUCT cds;
cds.dwData = 1; // 사용자 정의 ID
cds.cbData = (wcslen(message) + 1) * sizeof(wchar_t);
cds.lpData = (void*)message;
SendMessage(hTargetWnd, WM_COPYDATA, (WPARAM)hWnd, (LPARAM)&cds);
// 수신 측
case WM_COPYDATA:
COPYDATASTRUCT* pcds = (COPYDATASTRUCT*)lParam;
wchar_t* receivedMsg = (wchar_t*)pcds->lpData;
// 사용
MessageBox(hWnd, receivedMsg, L"Received", MB_OK);
return TRUE;
공유 메모리 사용
// 송신 측: 공유 메모리 생성
HANDLE hMapFile = CreateFileMapping(
INVALID_HANDLE_VALUE,
NULL,
PAGE_READWRITE,
0,
1024,
L"MySharedMemory"
);
void* pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 1024);
memcpy(pBuf, largeData, dataSize);
// 메시지로 알림만 전송
PostMessage(hTargetWnd, WM_USER_SHARED_DATA, 0, 0);
// 수신 측
case WM_USER_SHARED_DATA:
HANDLE hMapFile = OpenFileMapping(
FILE_MAP_ALL_ACCESS,
FALSE,
L"MySharedMemory"
);
void* pBuf = MapViewOfFile(hMapFile, FILE_MAP_ALL_ACCESS, 0, 0, 1024);
// 데이터 사용
break;
7. 언제 무엇을 사용할까?
SendMessage를 사용해야 할 때
✅ 즉시 결과가 필요한 경우
// 컨트롤의 현재 값 얻기
int length = SendMessage(hEdit, WM_GETTEXTLENGTH, 0, 0);
✅ 같은 스레드 내 순차 처리
// 리스트 아이템 추가 후 선택
int index = SendMessage(hList, LB_ADDSTRING, 0, (LPARAM)L"Item");
SendMessage(hList, LB_SETCURSEL, index, 0);
✅ 표준 컨트롤 메시지
// 대부분의 컨트롤 메시지는 SendMessage
SendMessage(hButton, BM_SETCHECK, BST_CHECKED, 0);
SendMessage(hProgressBar, PBM_SETPOS, 50, 0);
PostMessage를 사용해야 할 때
✅ 스레드 간 통신
// 작업 스레드에 명령 전송
PostMessage(hWorkerWnd, WM_USER_PROCESS, taskId, 0);
✅ UI 응답성 유지
// 무거운 작업 후 UI 업데이트
PostMessage(hMainWnd, WM_USER_UPDATE_UI, 0, 0);
✅ 이벤트 알림
// 파일 다운로드 완료 알림
PostMessage(hMainWnd, WM_USER_DOWNLOAD_COMPLETE, fileId, 0);
✅ 프로세스 종료 요청
// 안전한 종료 요청
PostMessage(hWnd, WM_CLOSE, 0, 0);
8. 고급 패턴
패턴 1: 명령 큐 (Command Queue)
// 명령 구조체
struct Command {
int id;
void* data;
};
std::queue<Command> commandQueue;
CRITICAL_SECTION cs;
// 명령 추가 (스레드 안전)
void QueueCommand(int id, void* data) {
EnterCriticalSection(&cs);
commandQueue.push({id, data});
LeaveCriticalSection(&cs);
// 처리 요청
PostMessage(hProcessorWnd, WM_USER_PROCESS_QUEUE, 0, 0);
}
// 처리
case WM_USER_PROCESS_QUEUE:
EnterCriticalSection(&cs);
if (!commandQueue.empty()) {
Command cmd = commandQueue.front();
commandQueue.pop();
LeaveCriticalSection(&cs);
ProcessCommand(cmd);
} else {
LeaveCriticalSection(&cs);
}
break;
패턴 2: 응답 대기 (Request-Response)
// 요청 ID 생성
static UINT g_requestId = 0;
std::map<UINT, HANDLE> pendingRequests;
// 요청 전송
UINT SendAsyncRequest(HWND hTarget, void* data) {
UINT requestId = ++g_requestId;
HANDLE hEvent = CreateEvent(NULL, TRUE, FALSE, NULL);
pendingRequests[requestId] = hEvent;
PostMessage(hTarget, WM_USER_REQUEST, requestId, (LPARAM)data);
return requestId;
}
// 응답 대기
void WaitForResponse(UINT requestId, DWORD timeout) {
HANDLE hEvent = pendingRequests[requestId];
WaitForSingleObject(hEvent, timeout);
CloseHandle(hEvent);
pendingRequests.erase(requestId);
}
// 응답 전송 (처리 완료 후)
case WM_USER_REQUEST:
UINT requestId = wParam;
// 처리...
// 완료 알림
PostMessage(hRequestorWnd, WM_USER_RESPONSE, requestId, result);
break;
// 응답 수신
case WM_USER_RESPONSE:
UINT requestId = wParam;
SetEvent(pendingRequests[requestId]);
break;
패턴 3: 타이머 메시지 통합
// 여러 타이머를 하나로 통합
#define TIMER_ID_COMBINED 100
std::set<std::function<void()>> timerCallbacks;
// 타이머 시작
SetTimer(hWnd, TIMER_ID_COMBINED, 100, NULL);
case WM_TIMER:
if (wParam == TIMER_ID_COMBINED) {
for (auto& callback : timerCallbacks) {
callback();
}
}
break;
9. 디버깅 팁
메시지 로깅
// 메시지 이름 매핑
const char* GetMessageName(UINT msg) {
switch (msg) {
case WM_CREATE: return "WM_CREATE";
case WM_DESTROY: return "WM_DESTROY";
case WM_PAINT: return "WM_PAINT";
// ... 추가
default:
static char buf[32];
sprintf_s(buf, "0x%04X", msg);
return buf;
}
}
// WndProc에서 로깅
LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) {
OutputDebugStringA(GetMessageName(msg));
OutputDebugStringA("\n");
// 처리...
return DefWindowProc(hWnd, msg, wp, lp);
}
Spy++ 활용
# Visual Studio에 포함된 Spy++ 실행
%VSINSTALLDIR%\Common7\Tools\spyxx.exe
# 메시지 로그 필터링:
# - 대상 윈도우 선택
# - Messages 메뉴 → Log Messages
# - 원하는 메시지만 필터링
10. 베스트 프랙티스
✅ 해야 할 것
1. 스레드 간 통신은 PostMessage
// 작업 스레드 → UI 스레드
PostMessage(hMainWnd, WM_USER_UPDATE, 0, 0);
2. 타임아웃 설정
// 크로스 프로세스는 항상 타임아웃
SendMessageTimeout(hWnd, msg, wp, lp, SMTO_NORMAL, 5000, &result);
3. 메모리 정리
case WM_USER_DATA:
MyData* pData = (MyData*)lParam;
ProcessData(pData);
delete pData; // 반드시 삭제
break;
4. 에러 체크
if (!PostMessage(hWnd, msg, wp, lp)) {
DWORD error = GetLastError();
// 에러 처리
}
❌ 하지 말아야 할 것
1. UI 스레드에서 SendMessage로 무거운 작업
// ❌ UI 얼어붙음
SendMessage(hWorkerWnd, WM_HEAVY_TASK, 0, 0);
2. PostMessage에 스택 변수 포인터 전달
// ❌ 댕글링 포인터
int localVar = 42;
PostMessage(hWnd, WM_USER, 0, (LPARAM)&localVar); // 스택 해제됨!
3. 순환 SendMessage
// ❌ 데드락
// A → B SendMessage
// B → A SendMessage
4. 큐 오버플로우 무시
// ❌ 실패 체크 안 함
for (int i = 0; i < 100000; i++) {
PostMessage(hWnd, WM_USER, i, 0); // 일부 손실 가능
}
11. 비교표
| 특성 | SendMessage | PostMessage |
|---|---|---|
| 동작 방식 | 동기 (블록) | 비동기 (즉시 리턴) |
| 반환값 | LRESULT (처리 결과) | BOOL (성공/실패) |
| 메시지 큐 | 거치지 않음 | 큐에 삽입 |
| 실행 순서 | 즉시 실행 | 큐 순서대로 |
| 데드락 | 위험 있음 | 안전함 |
| 성능 | 빠름 (직접 호출) | 약간 느림 (큐 경유) |
| 크로스 스레드 | 가능 (내부 동기화) | 가능 |
| 크로스 프로세스 | 가능 (포인터 제한) | 가능 (포인터 제한) |
| 사용 예 | 컨트롤 조작, 즉시 응답 | 스레드 통신, 이벤트 알림 |
정리
핵심 요약
SendMessage:
- ⏱️ 동기: 처리 완료까지 대기
- ✅ 반환값 확인 가능
- ⚡ 즉시 실행 (큐 생략)
- ⚠️ 데드락 주의
PostMessage:
- ⚡ 비동기: 즉시 리턴
- ❌ 반환값 없음 (성공/실패만)
- 📬 메시지 큐 경유
- ✅ 데드락 안전
선택 가이드
┌─ 같은 스레드?
│ ├─ YES ─> 즉시 결과 필요? ─> YES ─> SendMessage
│ │ └─ NO ─> PostMessage
│ │
│ └─ NO (다른 스레드) ─> 무거운 작업? ─> YES ─> PostMessage
│ └─ NO ─> 둘 다 OK
│
└─ 크로스 프로세스? ─> PostMessage (+ SendMessageTimeout)
다음 단계
- Windows API 스레드 동기화 가이드 (작성 예정)
- Windows 메시지 루프 완전 정복 (작성 예정)
- 크로스 플랫폼 IPC 패턴 (작성 예정)
이 글이 도움이 되었나요? Windows 프로그래밍에서 겪은 메시지 처리 경험을 댓글로 공유해 주세요! 🚀