본문으로 건너뛰기

© 2026 Molayo

Dev.to헤드라인2026. 06. 06. 04:54

버그를 수정하려 했는데, 실수로 두 개의 기능을 만들어버렸습니다. PC Workman 1.7.8 - 고급 딥러닝 AI, 공급 모니터 및 헬스

요약

PC Workman v1.7.8 업데이트 과정에서 발생한 고스트 드라이버 탐지 기능 개발 사례를 다룹니다. WMI의 한계를 극복하기 위해 PnPUtil을 사용하여 실제 연결된 장치와 레지스트리 잔류 항목을 비교하는 로직을 구현했습니다.

핵심 포인트

  • WMI는 레지스트리 캐시 기반의 유령 장치를 반환할 수 있음
  • PnPUtil을 통해 커널의 라이브 장치 트리를 직접 쿼리하여 정확도 향상
  • 집합 차집합(Set diff) 방식을 활용한 고스트 드라이버 탐지 로직 구현
  • 원클릭 제거 기능을 통해 시스템 최적화 도구로서의 가치 제공

버그를 수정하려 했는데, 실수로 두 개의 기능을 만들어버렸습니다.

Friday Shipped & Scarred #11, "새로운 기능 없음"이 약 4시간 동안 지속되었던 그 에피소드입니다.

v1.7.7(한 주 동안 20개 이상의 패치를 배포함) 이후의 계획은 다음과 같았습니다: 유지보수만 수행할 것. 테스터 목록 수정하기. 테스트하기.
아무것도 새로 만들지 말 것.

수정 사항을 확인하기 위해 설정 및 드라이버(Setup & Drivers)를 열었습니다. 제 PC에는 두 개의 GPU 항목이 있었습니다.

RTX - 현재 기기에 장착된 것.
그리고 GT 1030 - 2년 전에 제거된 것.
드라이버 항목이 여전히 레지스트리(Registry)에 남아 공간을 차지하며 아무 일도 하지 않고 있었습니다. Windows는 죽은 하드웨어를 영원히 생명 유지 장치에 연결해 둡니다.

저는 생각했습니다: 내 기기에 이런 게 있다면, 모든 사람의 기기에도 이럴 것이다.

3시간 후, Ghost Driver Detection(고스트 드라이버 탐지) 기능이 코드베이스에 포함되었습니다.

Ghost Drivers: 왜 WMI는 당신에게 거짓말을 하는가

"어떤 장치가 실제로 연결되어 있는가"를 확인하기 위한 명백한 접근 방식은 WMI입니다. 하지만 그것은 틀렸습니다.

WMI의 Win32_PnPEntity는 유령 장치(Phantom devices)도 반환하며, 이것이 바로 제가 탐지하려고 했던 바로 그 문제입니다. 유령에게 자신이 유령인지 물어볼 수는 없는 노릇입니다.

실제로 작동하는 방식은 다음과 같습니다: pnputil /enum-devices --connected.
PnPUtil은 레지스트리 캐시(Registry cache)가 아니라 커널의 라이브 장치 트리(Live device tree)를 쿼리합니다.

# core/startup_watcher.py — _get_pnp_connected_names()
result = subprocess.run(
    ["pnputil", "/enum-devices", "--connected", "/class", guid],
...

탐지 로직은 집합 차집합(Set diff) 방식입니다:

  1. 레지스트리 드라이버(Registry drivers) -> 장치 클래스에 대해 설치된 모든 항목
  2. 연결됨 (pnputil) -> 현재 실제로 꽂혀 있는 것
  3. 차이(Gap) -> 고스트(Ghost). 연결된 집합에 포함되지 않은 20개월보다 오래된 모든 드라이버 항목은 플래그(Flag)가 지정됩니다. 빨간색 배지. 연령 정보. pnputil /remove-device를 통한 원클릭 제거.

드라이버 탭을 위한 레지스트리 읽기 인프라가 이미 존재했기 때문에 이 기능은 빠르게 출시되었습니다.
저는 차이점(diff) 로직과 Optimization Center에 새로운 카드를 추가하기만 하면 되었습니다.

백그라운드 앱 휴면 (Background App Hibernation):

두 가지 모드, 서로 다른 트레이드오프 (Tradeoffs)
Optimization Center를 위해 계획된 16개 기능 중 14번째 기능입니다. 유휴 프로세스(idle process) 탐지에 대한 버그 조사로 시작되었으나, 완전한 기능으로 발전했습니다.

오탐(False Positives) 없는 "유휴(idle)" 상태 탐지

단순한 버전: CPU 사용량이 1% 미만인 프로세스를 15분 동안 유지하면 플래그를 지정합니다. 문제점: 여기에는 백그라운드 서비스, 컴파일러 데몬(compiler daemons), git 프로세스 등 정당하게 유휴 상태인 모든 것이 포함됩니다. 사용자는 이러한 것들을 보아서는 안 됩니다.

app_activity_tracker.py에 적용된 실제 필터:

# 이번 세션 동안 포그라운드(foreground)에 있었던 앱만 표시합니다.
# 포그라운드에 있었던 적이 없다면 서비스이므로 건너뜁니다.
if pid not in known_fg_pids:
...

포그라운드 창 추적은 ctypes를 통한 GetForegroundWindow 및 GetWindowThreadProcessId를 사용하며, 15초마다 폴링(polled)됩니다. 외부 의존성 없이 순수 Windows API만을 사용합니다.

@staticmethod
def _foreground_pid() -> int:
    hwnd = ctypes.windll.user32.GetForegroundWindow()
...

CPU 사용량 제로. RAM에 상주. 깨어날 때 즉각적인 proc.resume(). 하루 종일 아무것도 하지 않고 대기하는 업데이트 프로세스 같은 항목에 적합합니다.

터보(Turbo) 통합
동작 방식은 실행 파일(exe) 이름별로 hibernation_prefs.json에 저장됩니다. TURBO 모드가 활성화되면(v1.7.7의 기존 전원 계획 + 서비스 중지 시스템), 휴면 동작이 자동으로 적용됩니다:

# optimization_services.py — _tpp_run()
ok = _pp_set(_TPP["turbo_guid"])
if ok:
...

그리고 터보가 비활성화되면 원래대로 되돌립니다:

def _tpp_restore():
    ok = _pp_set(original_guid)
    if ok:
...

CPU 사용량이나 유휴 시간과 관계없이 휴면 후보로 절대 나타나지 않는 30개 이상의 시스템 프로세스 이름 보호 목록 (svchost.exe, MsMpEng.exe, dwm.exe 등).

폴란드어 단어를 손상시킨 파서(Parser) 버그

parser.py에는 악센트 정규화(accent normalization) 단계가 있었습니다. 이는 분음 기호(diacritics)를 입력하지 않는 사용자를 위해 불완전한 폴란드어 파편을 전체 형태로 매핑하는 과정이었습니다.

_ACCENT_MAP = [
    ("temperatur",   "temperatura"),  # 수정: "jak jest temperatur" → "temperatura"
    ("wydajnosc",    "wydajność"),
...

수정 사항: 단어 경계 매칭(word-boundary matching).

def _normalize_accents(self, text: str) -> str:
    for stripped, accented in self._ACCENT_MAP:
        text = re.sub(r'\b' + re.escape(stripped) + r'\b', accented, text)
...

\b는 "temperatur"를 독립된 단어로만 매칭합니다. "temperatura"는 수정되지 않고 그대로 통과합니다.

이 문제는 감사(audit) 후 실시한 20개 질문 의도 라우팅(intent routing) 테스트 중에 발견되었습니다.
"temperatura cpu"가 hw_cpu를 33%의 신뢰도로 점수화하고 있었습니다.
추적 결과, "temperaturaa cpu"가 아무것도 매칭하지 못하는 문제였습니다.

매 메시지마다 재구축되던 캐시

같은 파일입니다. 다른 문제입니다. (악센트 무시 매칭에 사용되는) 폴드 패턴 캐시(folded pattern cache)가 parse() 함수 내부에서 생성되고 있었습니다.

# 기존 방식 — 모든 메시지마다 실행됨
def parse(self, text):
    folded_patterns_cache = {
...

INTENT_PATTERNS에는 82개의 의도(intent)가 있으며, 일부는 각각 30개 이상의 패턴을 가지고 있습니다. 사용자의 모든 메시지마다 이 전체 딕셔너리(dict)를 처음부터 다시 구축했습니다. 호출당 O(n×m)의 비용이 발생했습니다.

수정 사항: 클래스 수준의 지연(lazy) 캐시 도입.

# 새로운 방식 - 한 번 구축되어 영구적으로 재사용됨
class IntentParser:
    _folded_cache: Dict[str, List[str]] = {}
...

82/82 - 완전한 의도 커버리지(Full Intent Coverage)의 실제 의미

hck_GPT AI 엔진은 하이브리드 시스템을 사용하여 메시지를 라우팅합니다:
키워드 점수 산정(keyword scoring)과 나이브 베이즈(Naive Bayes)를 혼합하여 신뢰도 점수(confidence score)를 산출하며,
점수가 0.65 이상이면 규칙 엔진(rule engine)을, 그 미만이면 Ollama LLM을 사용합니다.

각 의도는 hybrid_engine.py에 두 개의 설정 항목을 가집니다:

_CONTEXT_WINDOWS — AI가 해당 의도 유형을 찾기 위해 얼마나 과거까지 탐색하는지:

_CONTEXT_WINDOWS = {
...

이번 주 이전 상황: 82개 중 34개에만 컨텍스트 윈도우(context windows)가 있었습니다. 82개 중 54개에는 힌트(hints)가 있었습니다. 등록되지 않은 모든 의도는 30분의 기본 윈도우와 빈 가이드라인으로 떨어졌습니다.

이제 82개 모두 두 가지를 모두 갖추고 있습니다. "temperatur cpu"에 대한 쿼리는 10분의 윈도우(관련된 최근 데이터)와 LLM이 정확히 무엇을 확인해야 하는지 알려주는 힌트(hint)를 받습니다.
"is my cpu hot more than average"(temp_comparison)와 같은 트렌드 쿼리는 7일간의 데이터와 적절한 비교 가이드를 받습니다.

파서(parser) 버그를 수정하고 맵(maps)을 채운 후의 의도 탐지(intent detection) 전/후 비교:

Query   Before  After   Context
temperatur cpu  hw_cpu (broken) temperature ✓ 10m
is my pc more warm than normal? hw_all RULE 5m  temp_comparison ✓ 10080m
...

hck_GPT 패널 시작 버그
한 줄 요약 증상: 새로 실행 시 패널이 창의 맨 상단에 나타납니다. '최대 보기 모드(Max View Mode)'를 한 번 토글하면 올바른 하단 위치로 즉시 이동합니다.

근본 원인:

parent.after(350, lambda: self._animate(0, self.collapsed_h, duration_ms=700))

애니메이션은 패널 생성 350ms 후에 실행됩니다. 이 과정에서 y = parent_h - current_h를 계산하기 위해 parent.winfo_height()를 읽습니다.

하지만 첫 실행 시 350ms는 충분하지 않습니다. 창의 레이아웃(layout)이 아직 잡히지 않았기 때문에 winfo_height()가 0을 반환합니다.
그러면 y = 0 - 34 = -34가 되고, 이는 0으로 제한(clamped)됩니다. 결과적으로 패널이 상단에 위치하게 됩니다.

최대 보기 모드(Max View Mode)가 기하학적 구조 변경(geometry change)을 트리거하여 강제로 재배치(re-layout)를 수행하게 했고, 이로 인해 올바른 winfo_height() 값을 얻을 수 있었습니다.

수정 사항:

def _animate(self, start_h, end_h, duration_ms=200, on_end=None):
    parent_h = self.parent.winfo_height()
    if parent_h < 50:
...

수치 데이터

수치 데이터

2개의 계획되지 않은 기능 출시 (Ghost Drivers, Background App Hibernation)
Optimization Center 기능 15/16개 활성화
컨텍스트 윈도우 (Context Windows) + LLM 힌트가 포함된 의도(Intents) 82/82개 (기존 34/54)
14개의 어휘 패턴 수정 또는 추가
단어 손상을 일으키는 파서 (Parser) 버그 1개
hck_GPT 패널의 시작 위치 버그 1개
자동화 테스트 21/21개 통과
나는 버그를 수정하기로 되어 있었습니다. 버그를 수정했습니다. 그리고 기능 두 개를 더 출시해 버렸습니다. 설명할 방법이 없네요.

Stack & Links

PC Workman은 임베디드 AI 어시스턴트 (hck_GPT)가 포함된 실시간 Windows 시스템 모니터입니다. 오픈 소스이며, Python 3.9+ / Tkinter, Windows 10+ 환경을 지원합니다.

GitHub: github.com/HuckleR2003/PC_Workman_HCK
Medium (내러티브 버전): link
Support: Coffee 또는 Github :)
Marcin Firmuga - HCK_Labs - 1인 개발자, 폴란드.
11개월째. 여전히 업데이트 중.

About the Author

저는 Marcin Firmuga입니다. 1인 개발자이자 HCK_Labs의 설립자입니다.

저는 네덜란드의 창고 교대 근무 시간 동안 수명이 다해가는 하드웨어를 사용하여 밑바닥부터 완전히 구축한, 시간이 지남에 따라 사용자의 자연스러운 PC 동작(스파이크, 전압, 온도, 사용량)을 학습하는 오픈 소스 AI 기반 PC 리소스 모니터인 PC Workman을 만들었습니다.

이전에는 게임 번역, PC 기술 인턴십, 여러 국가에서의 창고 운영, 그리고 결코 끝내지 못한 수많은 실패한 프로젝트들을 경험했습니다.

하지만 이것은요? 이것은 계속 이어지고 있습니다.
1,000시간 이상의 코딩. 4번의 완전한 UI 재구축. 16,000줄의 코드 삭제.
새벽 3시의 밤샘 작업. 에너지 드링크와 토스트.

그리고 PC Workman에게 두뇌와 심장을 부여하기 위해 수개월 동안 구축해 온 저만의 hck_GPT AI가 있습니다.

그리고 마침내, 5초 만에 닫아버리지 않을 앱이 완성되었습니다. 이것이 바로 단순히 만드는 것(building)과 출시하는 것(shipping)의 차이입니다.

AI 자동 생성 콘텐츠

본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0