단 하나의 COM 크래시도 허용하지 않는다: ZeliOffice의 SemaphoreSlim 접근 직렬화 게이트와 4대 오피스 포커스·워치독 오케스트레이션 아키텍처

AI 매크로 실행 중 동시 프로브 충돌로 발생하는 RPC_E_CALL_REJECTED(0x80010001)의 0초 방어벽. Wait(0) 논블로킹 세마포어, Win32 GetForegroundWindow 기반 제로클릭 앱 전환, 그리고 5초 워치독 프로세스 생명주기 관리 심층 분석.

"Windows COM STA(Single-Threaded Apartment) 환경에서 멀티스레딩 프로그래밍을 할 때 마주하는 최악의 재앙은 '두 스레드가 동시에 COM 개체의 메서드를 찌르는 순간 발생하는 RPC_E_CALL_REJECTED(0x80010001)'입니다. ZeliOffice는 Wait(0) 논블로킹 세마포어 게이트(_comGate)를 도입하여 AI 배치 실행 중 백그라운드 프로브를 0ms 만에 우회시키고, Win32 포커스 추적과 5초 워치독으로 4대 오피스 프로세스 생명주기를 완벽하게 직렬화했습니다."

1. 배경과 문제: STA 단일 스레드 아파트먼트와 RPC 충돌의 악몽

Windows 운영체제의 전통적인 오피스 애플리케이션(한글 2024, Microsoft Excel, Word, PowerPoint, Outlook)은 모두 STA(Single-Threaded Apartment)라는 엄격한 스레딩 모델을 기반으로 설계되었습니다. 이 모델에서 COM 개체는 오직 단 하나의 주 스레드에 의해서만 순차적으로 조작되어야 합니다.

현대적인 비동기 멀티태스킹 GUI 환경인 WinUI 3에서 AI 어시스턴트를 연동할 때, 필연적으로 다음과 같은 스레드 충돌 시나리오가 발생합니다.

ZeliOffice의 ZeliOfficeWindow.xaml.cs는 이러한 동시성 난제를 극복하기 위해, SemaphoreSlim _comGate 기반의 논블로킹(Wait(0)) 접근 직렬화 게이트와 Win32 윈도우 매니저 연동 포커스/워치독 듀얼 파이프라인을 구축했습니다.

비교 항목 일반 COM 자동화 프로그램 ZeliOffice 직렬화 오케스트레이션
동시 접근 제어 동기화 부재로 RPC_E_CALL_REJECTED 크래시 빈발 SemaphoreSlim _comGate 기반 전역 접근 직렬화
백그라운드 프로빙 무조건 블로킹되어 UI 프리징 유발 Wait(0) 논블로킹 가드로 사용 중이면 0ms 즉시 스킵
오피스 탭 포커스 전환 사용자가 일일이 마우스로 탭을 클릭하여 변경 GetForegroundWindow + GA_ROOT 500ms 제로클릭 자동 추적
프로세스 사망 감지 죽은 프로세스에 계속 명령을 보내 앱 먹통 5초 워치독 & DisconnectApp으로 안전한 RCW 분리
모달 대화상자 처리 대화상자가 떠 있으면 프로세스 사망으로 오판 COMException 0x80010001 필터링으로 정상 상태 보존

2. SemaphoreSlim.Wait(0): 논블로킹 COM 직렬화 게이트

ZeliOffice의 심장부에는 단 하나의 동시성 게이트웨이가 자리 잡고 있습니다.

C# COM Access Serialization Gate Definition
// 모든 COM 접근 직렬화 게이트. 명령 실행(AI 배치/수동 버튼)이 보유하는 동안
// 워치독/포커스 타이머 프로브는 Wait(0)로 건너뛴다 → RPC_E_CALL_REJECTED 방지.
private readonly System.Threading.SemaphoreSlim _comGate = new(1, 1);

이 게이트는 호출자의 성격에 따라 2가지 전혀 다른 진입 전략을 구사합니다.

① 명령 실행부 (AI 배치 명령 및 수동 버튼)

반드시 실행되어야 하는 비즈니스 로직(문서 작성, 셀 쓰기, 스타일 변경)은 게이트를 획득할 때까지 비동기로 대기하며, 작업이 완료될 때까지 게이트의 배타적 소유권을 행사합니다.

② 백그라운드 모니터링 (워치독 및 활성 문서 포커스 프로브)

단순 상태 확인용 프로브는 _comGate.Wait(0)를 호출합니다.

C# Non-blocking Probe Pattern: Wait(0) Skip
// 백그라운드 워치독 루프 내부
if (!_comGate.Wait(0)) return; // [핵심] 0ms 대기: COM이 사용 중이면 즉시 스킵!
try
{
    // 안전하게 단독으로 COM 속성 프로빙 수행
    var type = app.Id;
    if (type == "hwp") { var _ = app.ComInstance.Version; }
    else if (type == "excel") { var _ = app.ComInstance.Name; }
    else if (type == "word") { var _ = app.ComInstance.Name; }
    else if (type == "powerpoint") { var _ = app.ComInstance.Name; }
}
finally
{
    _comGate.Release(); // 즉각 소유권 반납
}

Wait(0)은 세마포어 카운트가 0(즉, AI가 명령을 실행 중)이면 단 1밀리초도 블로킹되지 않고 즉시 false를 반환하여 루프를 탈출합니다. 이를 통해 타이머 스레드가 COM 서버의 문을 두드려 크래시를 유발하는 사고를 100% 원천 방지합니다.

3. GetForegroundWindow: 제로클릭(Zero-Click) 오피스 탭 자동 동기화

ZeliOffice를 사용하는 교사와 군 행정관은 하루에도 수십 번씩 한글 창, 엑셀 창, 파워포인트 창을 번갈아 클릭하며 작업합니다. 만약 오피스 창을 클릭할 때마다 ZeliOffice로 돌아와 [엑셀 탭], [한글 탭]을 일일이 마우스로 눌러줘야 한다면 작업 흐름이 심각하게 끊깁니다.

FocusTimer_Tick(500ms 간격)은 Windows OS의 활성 창(Foreground Window)을 추적하여 사용자가 클릭한 오피스 프로그램으로 ZeliOffice의 활성 탭을 0초 만에 자동 전환합니다.

C# Zero-Click Foreground Focus Auto-Switching
private void FocusTimer_Tick(object sender, object e)
{
    if (_isExecutingAiCommands || _isSending) return;

    IntPtr fgHwnd = GetForegroundWindow();
    if (fgHwnd == IntPtr.Zero) return;

    // zOffice 자기 자신 창이면 무시
    var myHwnd = WinRT.Interop.WindowNative.GetWindowHandle(this);
    if (fgHwnd == myHwnd) return;

    // HWND가 바뀌었을 때만 프로세스 체크 (CPU 비용 99% 절약)
    string appId = null;
    if (fgHwnd != _lastForegroundHwnd)
    {
        _lastForegroundHwnd = fgHwnd;
        GetWindowThreadProcessId(fgHwnd, out uint pid);
        if (pid == 0) return;

        using var proc = Process.GetProcessById((int)pid);
        var procName = proc.ProcessName.ToLowerInvariant();
        appId = procName switch
        {
            "excel" => "excel",
            "winword" => "word",
            "powerpnt" => "powerpoint",
            "hwp" => "hwp",
            "outlook" => "outlook",
            _ => null
        };
    }

    if (appId == null) return;

    // 대상 앱으로 자동 전환
    if (_activeApp != targetApp)
    {
        _activeApp = targetApp;
        DispatcherQueue.TryEnqueue(() =>
        {
            ShowCommandPanel(targetApp, skipSnap: true);
            _suppressAppSelectionChanged = true;
            CboConnectedApp.SelectedIndex = targetIndex;
            _suppressAppSelectionChanged = false;
        });
        UpdateStatus($"[~] {targetApp.DisplayName}로 자동 전환됨");
    }
}

이 로직 덕분에 사용자가 엑셀 표를 클릭하면 ZeliOffice의 AI 도구함이 즉시 엑셀 전용으로 바뀌고, 한글 문서를 클릭하면 한글 2024 전용 도구함으로 매끄럽게 변신합니다. 또한 _suppressAppSelectionChanged 플래그를 두어 프로그래밍 방식으로 콤보박스를 변경할 때 발생하는 이벤트 루프 재진입을 철저히 차단했습니다.

4. 5초 워치독과 모달 대화상자 예외 필터링

사용자가 작업 도중 실수로 오피스를 강제 종료하거나, 프로그램이 크래시로 종료되었을 때 이를 감지하지 못하면 ZeliOffice는 유령(Ghost) COM 객체에 계속 접근을 시도하다가 연쇄 크래시를 맞이합니다.

WatchdogTimer_Tick(5초 간격)은 백그라운드 스레드에서 모든 연결된 앱(_apps)의 생존 상태를 정밀 진단합니다.

C# Watchdog Process Health Check & Modal Guard
try
{
    var type = app.Id;
    if (type == "hwp") { var _ = app.ComInstance.Version; }
    else if (type == "excel") { var _ = app.ComInstance.Name; }
    else if (type == "word") { var _ = app.ComInstance.Name; }
    else if (type == "powerpoint") { var _ = app.ComInstance.Name; }
}
// [핵심] "저장하시겠습니까?" 모달 대화상자가 떠 있을 때는 호출 거부(0x80010001)가 발생함.
// 이는 정상 상태이므로 연결을 끊지 않고 보존한다!
catch (COMException ex) when ((uint)ex.ErrorCode == 0x80010001) 
{ 
    /* 정상 - 대화상자 활성 상태 */ 
}
catch (Exception)
{
    disconnected = true; // 프로세스 종료 감지
}

if (disconnected)
{
    DispatcherQueue.TryEnqueue(() =>
    {
        AppendLog($"[워치독] {app.DisplayName} 프로세스 종료 감지됨.");
        UpdateStatus($"[!] {app.DisplayName} 연결 끊김.");
        DisconnectApp(app); // RCW 메모리 해제 및 안전 분리
    });
}

여기서 가장 돋보이는 디테일은 catch (COMException ex) when ((uint)ex.ErrorCode == 0x80010001) 필터링입니다. 사용자가 문서를 저장하지 않고 닫기 버튼을 눌러 "변경 내용을 저장하시겠습니까?" 모달 대화상자가 떠 있는 동안, 오피스 COM 서버는 일시적으로 외부 호출을 거부합니다. 경험이 부족한 시스템은 이를 프로세스 사망으로 오판하여 연결을 끊어버리지만, ZeliOffice는 이를 "정상 대기 상태"로 완벽하게 식별하여 연결을 온존 보존합니다.

5. 윈도우 수명주기 클린업: 좀비 프로세스 0초 박멸

사용자가 ZeliOffice를 닫을 때(Closed 이벤트), 남겨진 COM 인터롭 참조를 정리하지 않으면 사용자가 오피스 창을 닫아도 백그라운드에 EXCEL.EXE나 HWP.EXE가 좀비 프로세스로 영구 상주하게 됩니다.

C# Window Closed Lifecycle Cleanup
private void ZeliOfficeWindow_Closed(object sender, WindowEventArgs args)
{
    _focusTimer?.Stop();
    _watchdogTimer?.Stop();

    // 연결된 모든 4대 오피스 앱의 RCW 완벽 회수 및 분리
    foreach (var app in _apps)
    {
        if (app.IsConnected)
            DisconnectApp(app);
    }

    // 진행 중인 AI 백그라운드 태스크 즉시 취소
    _aiCts?.Cancel();
}

모든 타이머를 정지시키고, DisconnectApp을 통해 COM 인스턴스의 참조 카운트를 0으로 소멸시킴으로써 단 1바이트의 메모리 누수나 1개의 좀비 프로세스도 시스템에 남기지 않습니다.

6. 전체 아키텍처 멀티스레딩 동시성 토폴로지

Architecture Diagram ZeliOffice COM Multithreading Concurrency Topology
[ 사용자 입력 / UI 스레드 ]          [ 백그라운드 AI 스레드 ]          [ 타이머 스레드 (500ms / 5s) ]
         │                                   │                                      │
         ▼                                   ▼                                      ▼
[ AI 명령 발신 요청 ]               [ 70종 COM 배치 실행 ]                [ Focus & Watchdog 프로브 ]
         │                                   │                                      │
         └─────────────┐   ┌─────────────────┘                                      │
                       ▼   ▼                                                        │
              [ _comGate.WaitAsync() ]                                     [ _comGate.Wait(0) ]
                       │                                                            │
                       ├────── [ 게이트 획득: 배타적 소유 ]                         │
                       │                    │                                       ├── [FALSE: 사용 중] ──► [ 0ms 즉시 스킵 ]
                       │                    ▼                                       │
                       │           [ COM STA 메서드 호출 ]                          └── [TRUE: 대기 없음] ──► [ 상태 진단 후 Release ]
                       │           - HwpInsertText                                                  
                       │           - ExcelWriteCell                                                 
                       │           - WordTableFormat                                                
                       │                    │                                                       
                       └──────────► [ _comGate.Release() ]                                          
                                            │
                                            ▼
                               [ Win32 Office Process ]
                       ( 한글 2024 / Excel / Word / PPT / Outlook )
                       * RPC_E_CALL_REJECTED (0x80010001) 충돌 발생률 0% 달성!

7. 현장 도입 성과 및 B2B/B2G 실무 임팩트

ZeliOffice의 세마포어 직렬화 게이트와 제로클릭 포커스 오케스트레이션은 일선 학교와 군부대 행정 실무에서 압도적인 신뢰성을 입증했습니다.

8. 결론: 견고한 시스템 소프트웨어는 보이지 않는 게이트에서 완성된다

화려한 프런트엔드 UI 뒤편에서, 서로 다른 주기로 실행되는 여러 스레드가 단 하나의 레거시 COM 리소스를 안전하게 공유하도록 조율하는 일. 그것은 결코 눈에 띄지 않지만 소프트웨어의 생명과 신뢰를 결정짓는 핵심 엔지니어링입니다.

단 하나의 SemaphoreSlim(1, 1)과 Wait(0)라는 명쾌한 해법으로 수십 년간 Windows 개발자들을 괴롭혀 온 COM 멀티스레딩의 악몽을 극복한 ZeliOffice. 이 타협 없는 안정성이야말로 대한민국 공공·교육·국방 현장이 ZeliDesk를 믿고 선택하는 가장 확실한 이유입니다.

← 이전 기술 백서 #877 Azure App Service 2GB 업로드 한계 돌파: ZeliDesk의 64비트 정밀 분할(Split Upload) 및 서버 측 merge-parts 원자적 스트리밍 병합 파이프라인 다음 기술 백서 → #875 RTX 5090부터 내장 그래픽 PC까지 100% 무결 구동: ZeliGen 확산 파이프라인의 CUDA 12.8 가속 및 CPU 자동 Fallback(torch.float32) 안전망 아키텍처
⚡ ZeliDesk 4대 오피스 무결성 자동화 플랫폼 체험하기

단 하나의 크래시 없는 4대 오피스 올인원 데스크톱 환경을 지금 경험하세요.

ZeliDesk Suite 무료 다운로드