본문으로 건너뛰기
Previous
Next
Windows API PostMessage vs SendMessage 완벽 가이드 — 동기·비동기 메시...

Windows API PostMessage vs SendMessage 완벽 가이드 — 동기·비동기 메시지 전송

Windows API PostMessage vs SendMessage 완벽 가이드 — 동기·비동기 메시지 전송

이 글의 핵심

Windows API의 PostMessage와 SendMessage 차이점 완벽 정리. 동기 vs 비동기, 메시지 큐 동작 원리, 데드락 방지, 실전 패턴

Windows API PostMessage vs SendMessage 완벽 가이드

이 글을 읽으면 Windows 프로그래밍에서 PostMessage와 SendMessage의 차이를 이해하고, 상황에 맞게 올바르게 사용하는 방법을 배울 수 있습니다.

Windows API에서 메시지 전송은 GUI 프로그래밍의 핵심입니다. 버튼 클릭, 키보드 입력, 타이머 등 모든 것이 메시지로 처리됩니다. 하지만 PostMessageSendMessage의 차이를 제대로 이해하지 못하면 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가 큐에서 꺼냄
  • DispatchMessageWndProc 호출
  • 큐가 비면 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. 비교표

특성SendMessagePostMessage
동작 방식동기 (블록)비동기 (즉시 리턴)
반환값LRESULT (처리 결과)BOOL (성공/실패)
메시지 큐거치지 않음큐에 삽입
실행 순서즉시 실행큐 순서대로
데드락위험 있음안전함
성능빠름 (직접 호출)약간 느림 (큐 경유)
크로스 스레드가능 (내부 동기화)가능
크로스 프로세스가능 (포인터 제한)가능 (포인터 제한)
사용 예컨트롤 조작, 즉시 응답스레드 통신, 이벤트 알림

정리

핵심 요약

SendMessage:

  • ⏱️ 동기: 처리 완료까지 대기
  • ✅ 반환값 확인 가능
  • ⚡ 즉시 실행 (큐 생략)
  • ⚠️ 데드락 주의

PostMessage:

  • ⚡ 비동기: 즉시 리턴
  • ❌ 반환값 없음 (성공/실패만)
  • 📬 메시지 큐 경유
  • ✅ 데드락 안전

선택 가이드

┌─ 같은 스레드?
│  ├─ YES ─> 즉시 결과 필요? ─> YES ─> SendMessage
│  │                           └─ NO ─> PostMessage
│  │
│  └─ NO (다른 스레드) ─> 무거운 작업? ─> YES ─> PostMessage
│                                      └─ NO ─> 둘 다 OK

└─ 크로스 프로세스? ─> PostMessage (+ SendMessageTimeout)

다음 단계


이 글이 도움이 되었나요? Windows 프로그래밍에서 겪은 메시지 처리 경험을 댓글로 공유해 주세요! 🚀