윈도우 10 스피커 전환 유틸리티 개발기: Python 프로토타입부터 C++ 1.5MB 최적화까지

윈도우 10에서 단축키로 스피커를 전환하는 앱 개발기입니다. AI를 활용해 파이썬 프로토타입 검증부터 C++ Win32 API 포팅, 저수준 훅 간섭 해결까지의 전 과정을 정리했습니다.
윈도우 10 오디오 스위처 개발 아키텍처 및 C++ 최적화 다이어그램

윈도우 10 환경에서는 기본적으로 단축키를 이용해 스피커나 헤드셋 등 오디오 출력 장치를 즉각적으로 전환하는 기능을 제공하지 않습니다. 작업 표시줄의 사운드 아이콘을 일일이 클릭하거나 복잡한 제어판 설정을 거쳐야만 장치를 바꿀 수 있는 구조입니다. 이를 해결하고자 몇 가지 서드파티 유틸리티를 찾아 사용해 보았으나, 백그라운드 상주 중 예기치 않게 종료되거나 장치 전환 딜레이가 발생하는 등 불안정한 경우가 많았습니다.

더 이상 인터넷 검색으로 시간을 소모하기보다는, AI에게 명확한 요구사항을 제시하여 가볍고 확실하게 동작하는 맞춤형 유틸리티를 직접 제작하는 것이 빠르고 효율적이라고 판단했습니다. 이번 글에서는 초기 아이디어를 Python으로 신속하게 프로토타이핑하고, 이후 메모리 점유율과 반응 속도 한계를 극복하기 위해 C++ Win32 Native API로 전면 재작성(1.5MB 메모리 최적화)하기까지의 전체 개발 과정을 시간 순서대로 가감 없이 공유하고자 합니다.

버전 단계 주요 기술 스택 해결한 핵심 문제 및 특징
v1.0 ~ v2.0 Python, pystray, comtypes, keyboard 초기 기획 검증, 트레이 UI 노출 및 키보드 글로벌 훅 결함 확인
v3.0 ~ v6.0 Python, pynput, WinReg, Tkinter pynput 교체로 먹통 해결, 시작프로그램 등록, 단축키 캡처 상태 머신 구현
v7.0 Python (Threading, Subprocess) 동시성 락(is_switching) 및 UI 분리, 파이썬 런타임 상주 메모리(23MB) 한계 도달
v8.0 ~ v8.1 C++ (Win32 API, Core Audio COM) C++ 완전 포팅(RAM 1.5MB 달성), WH_KEYBOARD_LL 저수준 훅으로 OS 단축키 간섭 차단

1. 파이썬 기반 초기 기획 및 프로토타이핑 (v1.0 ~ v2.0)

첫 단계의 목표는 Windows Core Audio API를 제어하여 실제로 출력 장치가 즉시 전환되는지 최소 기능 제품(PoC)을 빠르게 검증하는 것이었습니다.

v1.0 — 최초 요구사항 지시와 설계

AI에게 다음과 같이 명확한 요구사항을 전달하며 개발을 시작했습니다.

💬 사용자 지시 (v1.0): "Windows 환경에서 핫키 입력을 통해 기본 오디오 출력 장치를 즉시 전환하는 백그라운드 유틸리티를 구현하십시오. 언어는 우선 Python을 사용하며, 기본 단축키는 Alt + Ctrl + Shift + Win + S 조합으로 설정하고 시스템 트레이에 상주하도록 설계하십시오."

AI는 Windows Core Audio의 미문서화 COM 인터페이스인 IPolicyConfig를 comtypes 모듈로 래핑하고, pystray(시스템 트레이)와 keyboard(단축키 캡처)를 결합한 스크립트를 작성하여 배포용 실행 파일(dist/main.exe)을 빌드했습니다.

flowchart TD
    User(["사용자 / 매크로 키보드"]) -->|"Alt+Ctrl+Shift+Win+S"| Hotkey["keyboard 모듈"]
    Hotkey -->|"스위칭 트리거"| Main["main.py 컨트롤러"]
    Main -->|"설정 조회"| Config[("config.json<br/>(즐겨찾기 목록)")]
    Main -->|"장치 순환 요청"| AudioMgr["audio_manager.py"]
    AudioMgr -->|"COM 인터페이스"| CoreAudio["Windows Core Audio API<br/>(IPolicyConfig / IMMDevice)"]
    CoreAudio -->|"기본 엔드포인트 변경"| Speaker["스피커 / 헤드셋 출력"]
    Main -->|"상태 피드백"| Tray["pystray 시스템 트레이"]
    Tray -->|"토스트 알림"| OSNotification["Windows 툴팁 알림"]

그러나 첫 번째 빌드를 실행했을 때, 백그라운드 프로세스는 구동되었으나 작업 표시줄 시스템 트레이에 아이콘이 나타나지 않아 상태를 제어할 수 없는 현상이 발생했습니다.

v2.0 — 트레이 UI 노출과 키보드 마비 결함

트레이 아이콘 노출과 GUI 기반 단축키 변경 기능을 요청하며 다음 피드백을 전달했습니다.

💬 사용자 지시 (v2.0): "빌드된 바이너리 실행 시 시스템 트레이에 아이콘이 렌더링되지 않습니다. 또한 단축키를 사용자가 직접 변경할 수 있는 설정 인터페이스를 추가하고, 단축키 입력 직후 시스템 전체 키보드 이벤트가 블로킹되는 결함을 수정하십시오."

AI는 pystray.Icon.run_detached() 방식으로 스레드를 분리하여 트레이 아이콘을 정상 렌더링시켰고, Tkinter 기반의 설정 대화상자를 추가했습니다. 하지만 실사용 테스트 과정에서 심각한 결함이 드러났습니다.

⚠️ 증상 및 원인 분석: keyboard 라이브러리가 윈도우 OS의 저수준 키보드 훅을 등록하고 해제하는 과정에서 스레드 락이 비정상적으로 걸려, 단축키를 누른 직후 운영체제 전체의 키보드 입력이 완전히 마비되는 치명적인 문제가 발생했습니다.

2. 키보드 엔진 교체 및 UX 고도화 (v3.0 ~ v6.0)

글로벌 훅 마비 현상을 해결하고, 실사용에 적합하도록 단축키 캡처 알고리즘을 대대적으로 개편했습니다.

💬 사용자 지시 (v3.0 ~ v6.0): "단축키 설정 UI 호출 실패 및 입력 후 키보드 락업 현상이 지속되고 있습니다. Windows 시작 시 자동 실행 레지스트리 등록 기능을 추가하고, 수식어 키의 좌우 접미사(L/R) 오인식 문제와 일반 키의 가상 키코드(VK) 미매핑 오류를 해결하십시오."

이 단계에서 적용된 핵심 개선 사항은 다음과 같습니다.

  1. 단축키 엔진 교체: 불안정한 keyboard 라이브러리를 완전히 걷어내고, 시스템 이벤트 안정성이 검증된 pynput.keyboard 모듈로 전환했습니다.
  2. 자동 실행 등록: winreg API를 통해 Windows 레지스트리(HKCU\Software\Microsoft\Windows\CurrentVersion\Run)에 앱 실행 경로를 등록/해제하는 기능을 트레이 메뉴에 연동했습니다.
  3. 가상 키코드 디코딩 패치: vk=83처럼 문자가 아닌 가상 키코드로 들어오는 입력을 win32api.MapVirtualKey를 통해 실제 영문 알파벳('S')으로 정확하게 역변환했습니다.
  4. 키 릴리즈 기반 상태 머신: 키를 누르는 중간에 단축키가 불완전하게 저장되는 문제를 방지하기 위해, 수식어 키를 누르고 일반 키를 누른 뒤 모든 키에서 손을 완전히 떼었을 때(Key Release) 입력을 확정하는 상태 머신을 구현했습니다.
윈도우 작업 표시줄 시스템 트레이 우클릭 컨텍스트 메뉴 UI

3. 동시성 락 적용과 파이썬 런타임의 한계 (v7.0)

기능적 안정성을 확보한 뒤 전체 코드 최적화 및 리소스 검토를 진행했습니다.

💬 사용자 지시 (v7.0): "전체 코드베이스의 동시성 제어 및 리소스 점유 상태를 검토하십시오. 백그라운드 상주 유틸리티임에도 메모리 점유율이 23MB에 달하는 것은 비효율적입니다. 리소스 최적화 방안을 제시하고 적용하십시오."

스레드 경합 제어 및 프로세스 분리

단축키를 빠르게 연타했을 때 COM 인터페이스가 중복 호출되어 충돌하는 문제를 방지하기 위해 플래그 기반 동시성 락(is_switching)을 도입했습니다.

sequenceDiagram
    autonumber
    actor User as 사용자
    participant HK as HotkeyManager
    participant App as Main App
    participant Worker as Worker Thread
    participant Audio as AudioManager
    participant Tray as TrayApp

    User->>HK: 단축키 입력
    HK->>App: switch_device()
    alt is_switching == True
        App-->>HK: 요청 무시 (연타 방어)
    else is_switching == False
        App->>App: is_switching = True
        App->>Worker: 스레드 생성 (daemon)
        activate Worker
        Worker->>Worker: CoInitialize()
        Worker->>Audio: 기본 스피커 조회
        Worker->>Audio: SetDefaultEndpoint()
        Worker->>Tray: 변경 완료 알림 발송
        Worker->>App: is_switching = False
        deactivate Worker
    end

또한 Tkinter 설정 팝업이 트레이 아이콘의 메인 이벤트 루프를 방해하지 않도록 subprocess.run([exe, "--dialog"]) 형태로 독립 프로세스를 생성하여 실행하도록 격리했습니다.

sequenceDiagram
    autonumber
    actor User as 사용자
    participant Tray as TrayApp
    participant Sub as Subprocess (--dialog)
    participant Tk as Tkinter UI (Listener)
    participant Main as Main App

    User->>Tray: [단축키 설정] 클릭
    Tray->>Main: change_hotkey() 호출
    Main->>Sub: subprocess.run([exe, "--dialog"])
    Sub->>Tk: 설정 창 생성 & 리스너 활성화
    User->>Tk: 모든 키에서 손을 뗌 (Release)
    Tk->>Sub: 임시 파일에 단축키 기록 후 종료
    Main->>Main: config.json 저장 & 핫키 갱신

파이썬 런타임 메모리의 한계와 부작용

하지만 리소스 측면에서는 파이썬 런타임의 한계가 명확했습니다. 단순 백그라운드 트레이 유틸리티임에도 상주 메모리가 23MB에 달했고, PyInstaller 패키징 결과물 크기도 32MB가 넘었습니다.

이를 줄이고자 Windows API의 SetProcessWorkingSetSize를 호출해 메모리를 강제로 페이징 아웃(Trimming)시키는 트릭을 적용해 보았으나, pystray의 메시지 펌프가 멈춰버려 트레이가 다시 먹통이 되는 치명적인 부작용이 발생했습니다. 억지스러운 메모리 트레이닝 대신 기반 언어를 교체해야 할 시점이었습니다.

4. C++ Win32 Native API로의 전면 재작성 (v8.0)

파이썬으로 모든 로직과 예외 케이스를 이미 검증했으므로, AI에게 C++ Win32 Native API로의 전면 전환을 지시했습니다.

💬 사용자 지시 (v8.0): "상주 메모리 한계가 명확한 Python 런타임을 고수할 이유가 없습니다. C++ 또는 C# 기반으로 재작성 시 기대되는 메모리 절감 수치를 분석하고, 최소한의 풋프린트를 위해 C++ Win32 Native API로 전면 재작성하십시오."

AI를 활용한 언어 마이그레이션은 놀라울 정도로 빠르고 매끄럽게 진행되었습니다. 이미 확립된 상태 머신과 COM 제어 로직이 존재했기 때문에, C++ 표준 라이브러리와 순수 Win32 API로 구조를 옮겨 담는 작업이 즉각 완료되었습니다.

스피커 및 헤드셋 오디오 출력 장치 즐겨찾기 선택 서브메뉴
graph TD
    subgraph AppSub ["Win32 Native C++ Application (439 KB)"]
        MainProc["main.cpp (Message Pump)"]
        MainProc --> TrayUI["tray_app.cpp (Tray Icon)"]
        MainProc --> AudioCore["audio_manager.cpp (COM API)"]
        MainProc --> HotkeyProc["hotkey_manager.cpp (Hotkey)"]
        MainProc --> ConfigCore["config_manager.cpp (JSON)"]
        MainProc --> AutoCore["autostart.cpp (AutoRun)"]
    end
    subgraph OSSub ["Windows 10 / 11 OS Subsystem (RAM 1.5 MB)"]
        Taskbar["시스템 트레이"]
        CoreAudioAPI["Core Audio Engine"]
        KeyboardSubsys["Raw Input Manager"]
        Registry["HKCU 시작프로그램"]
    end
    TrayUI <--> Taskbar
    AudioCore <--> CoreAudioAPI
    HotkeyProc <--> KeyboardSubsys
    AutoCore <--> Registry

파이썬 vs C++ 성능 측정 결과

C++ 네이티브 빌드로 전환한 결과, 백그라운드 유틸리티에 걸맞은 극적인 성능 개선을 달성할 수 있었습니다.

  • 상주 메모리(RAM): 23.0 MB → 1.5 MB (약 93.5% 절감)
  • 실행 파일 크기: 32.4 MB → 439 KB (약 1/73 수준 축소)
  • 최초 실행 딜레이: 1.5초(인터프리터 로딩) → 0.0초(즉각 반응)

5. 저수준 키보드 훅(WH_KEYBOARD_LL)과 최종 검증 (v8.1)

C++ 전환으로 모든 성능 문제가 해결되었으나, 마지막 실사용 테스트에서 운영체제와의 단축키 충돌이 발견되었습니다.

💬 사용자 지시 (v8.1): "단축키 캡처 대화상자 활성화 시 OS 수준의 단축키 간섭이 발생하고 있습니다. Win+S 입력 시 Windows 검색창이 함께 호출되는 등의 시스템 이벤트 전파를 차단하도록 저수준 키보드 훅을 적용하십시오."

WH_KEYBOARD_LL을 통한 이벤트 격리

기존 RegisterHotKey 방식은 단축키 설정 창이 떠 있는 동안 사용자가 입력하는 조합키를 OS가 먼저 낚아채는 문제가 있었습니다. 이를 차단하기 위해 저수준 키보드 훅(WH_KEYBOARD_LL)을 설정 창 라이프사이클에만 임시로 부착하는 설계를 적용했습니다.

sequenceDiagram
    autonumber
    actor User as 사용자
    participant Dlg as 단축키 설정 창 (Win32)
    participant Hook as LowLevelKeyboardProc
    participant OS as Windows OS / 타 앱
    participant Main as Main (HotkeyManager)

    User->>Dlg: 단축키 설정 대화상자 오픈
    Dlg->>Hook: SetWindowsHookExW 설치
    rect rgba(56, 189, 248, 0.12)
        note over User, OS: 키 입력 가로채기 & 시스템 전파 차단
        User->>Hook: 복합키 입력 (Win + S 등)
        Hook-->>OS: return 1 (OS 전파 차단 / 검색창 방지)
    end
    User->>Hook: 모든 키에서 손을 뗌 (Release)
    Hook->>Dlg: PostMessage(WM_CLOSE)
    Dlg->>Main: 새 단축키 등록
💡 최종 실환경 검증 결과: 저수준 키보드 훅을 사용함에도 Windows Defender 등 주요 안티바이러스 프로그램에서 오탐지(False Positive)가 전혀 발생하지 않았으며, 전체 화면 3D 게임 구동 중에도 입력 지연이나 단축키 충돌 없이 백그라운드에서 매끄럽게 동작함을 확인하였습니다.

마무리하며

이번 윈도우 스피커 전환 유틸리티 개발은 단순한 기능 구현을 넘어, AI를 활용한 현대적인 소프트웨어 개발 워크플로우의 장점을 체감할 수 있었던 프로젝트였습니다.

Python을 통해 복잡한 Windows Core Audio COM 인터페이스의 동작을 단시간에 검증하고, 여기서 도출된 안정화된 로직과 예외 처리 구조를 바탕으로 C++ Win32 Native 코드로 단번에 포팅함으로써 1.5MB의 상주 메모리와 제로 딜레이를 갖춘 완성형 유틸리티를 손쉽게 완성할 수 있었습니다.

여러분도 일상적인 PC 사용 환경에서 겪는 사소하지만 성가신 불편함이 있다면, AI와 함께 가벼운 프로토타입부터 네이티브 최적화까지 직접 구축해 보시는 것을 추천합니다. 혹시 C++ Win32 환경에서 백그라운드 훅이나 오디오 엔드포인트 제어를 다루며 겪으셨던 또 다른 트러블슈팅 경험이 있다면 댓글로 자유롭게 의견을 나누어 주시면 감사하겠습니다.