POOOLING FOREST
맥북이 조용히 느려질 때: Thermal Throttling 감지 앱 개발기 - M2 맥북 에어의 팬리스 구조에서 발생하는 스로틀링 현상을 감지하기 위해, 루트 권한 없이 시스템 열 압력
Engineering & Tech

맥북이 조용히 느려질 때: Thermal Throttling 감지 앱 개발기

M2 맥북 에어의 팬리스 구조에서 발생하는 스로틀링 현상을 감지하기 위해, 루트 권한 없이 시스템 열 압력 상태를 구독하는 메뉴바 앱을 개발한 과정을 담았습니다.

김영태

테크리드

안녕하세요. 풀링포레스트 테크리드 김영태입니다.

개발자에게 장비 욕심은 끝이 없죠. 저도 최근 M2 MacBook Air를 사용하면서 그 가벼움과 배터리 성능에 감탄하고 있었습니다. 팬이 없어서 도서관에서도 눈치 보지 않고 코딩할 수 있다는 점이 정말 매력적이더군요. 하지만 4K 120Hz 같은 고해상도 모니터를 연결하고 무거운 빌드를 돌릴 때, 뭔가 이상함을 느꼈습니다.

팬이 없으니 소음은 없는데, 시스템이 전반적으로 끈적거리는 느낌이 들기 시작한 겁니다. 타이핑 반응이 한 박자 늦고, 창 전환이 버벅거렸죠. 직감적으로 '아, 이게 말로만 듣던 스로틀링(Thermal Throttling)이구나' 싶었습니다.

보통 `iStat Menus` 같은 훌륭한 도구들이 있지만, 저는 단순히 "지금 내 맥이 성능 제한을 걸고 있는가?"에 대한 답을 직관적으로, 즉시 알고 싶었습니다. CPU 사용률이나 온도가 아니라, 시스템이 판단하는 '위기 상태' 그 자체가 궁금했죠. 그래서 직접 메뉴바 앱을 만들어보기로 결심했습니다. 그리고 이 과정에서 macOS의 열 관리 시스템에 대해 꽤 흥미로운 사실들을 알게 되었습니다.

첫 번째 시도: Apple 공식 문서 믿기

개발자라면 으레 공식 문서를 먼저 찾게 됩니다. Apple은 `Foundation` 프레임워크 안에 `ProcessInfo.thermalState`라는 아주 깔끔한 API를 제공합니다.

import Foundation
print(["nominal", "fair", "serious", "critical"][ProcessInfo.processInfo.thermalState.rawValue])

코드는 정말 간단했습니다. 상태도 '보통(Nominal)', '주의(Fair)', '심각(Serious)', '위험(Critical)'으로 나뉘어 있어 직관적으로 보였죠. 하지만 막상 테스트를 해보니 현실은 달랐습니다.

제 맥북이 뜨거워져서 분명히 성능 저하가 체감되는 시점에도 이 API는 여전히 'Fair' 상태라고만 알려주더군요. 실제로 시스템이 클럭을 깎아먹고 있는데도 말이죠. 'Serious' 상태는 정말 맥북이 녹아내리기 직전이 아니면 뜨지 않는 것 같았습니다. 이 정도 세분성으로는 제가 원하는 "성능 저하 감지" 기능을 구현할 수 없었습니다.

두 번째 시도: CLI 도구 파헤치기

공식 API가 미덥지 않을 때, 우리는 보통 터미널로 향합니다. `powermetrics`라는 명령어를 사용하면 훨씬 상세한 정보를 볼 수 있다는 걸 알게 되었습니다.

sudo powermetrics -s thermal

이 명령어를 실행해보니, `ProcessInfo`와는 다른 용어를 사용하더군요. 'Nominal', 'Moderate', 'Heavy', 'Trapping', 'Sleeping' 등 훨씬 구체적이었습니다. 스트레스 테스트를 해보니 제 맥북이 뜨거워질 때 'Moderate'를 거쳐 실제로 스로틀링이 걸리면 'Heavy' 상태로 진입하는 것을 확인할 수 있었습니다.

문제는 여기서 발생했습니다. `powermetrics`는 `root` 권한이 필요합니다. 고작 메뉴바에 온도계 아이콘 하나 띄우자고 매번 비밀번호를 치거나, 앱 내부에 복잡한 권한 상승 로직을 넣는 건 배보다 배꼽이 더 큰 격이었습니다. 보안상으로도 찜찜했고요.

솔직히 이때 좀 막막했습니다. "그냥 포기하고 iStat Menus나 쓸까?" 하는 생각이 들었죠.

유레카: 숨겨진 알림 채널 찾기

하지만 포기하기 전에 한 번만 더 깊게 파보기로 했습니다. `powermetrics`가 보여주는 데이터는 어디서 오는 걸까요? 조사해보니 `thermald`라는 시스템 데몬이 이 정보를 관리하고 있었습니다. 그리고 놀랍게도, 이 데몬은 현재의 열 압력(Thermal Pressure) 상태를 다윈 알림 센터(Darwin Notification Center, notifyd)에 뿌리고 있었습니다.

핵심은 `com.apple.system.thermalpressurelevel`이라는 키값이었습니다.

가장 좋은 점은 이 알림을 구독하는 데에는 루트 권한이 필요 없다는 사실이었습니다! 우리가 찾던 답이 바로 여기에 있었습니다. Swift로 이 시스템 이벤트를 구독하는 코드는 대략 이렇습니다.

import Foundation

// C API 바인딩 생략...

let name = "com.apple.system.thermalpressurelevel"
var token: Int32 = 0
// 알림 등록
notify_register_check(name, &token)

var state: UInt64 = 0
// 상태값 가져오기
notify_get_state(token, &state)

let label = switch state {
case 0: "nominal"
case 1: "moderate" // 성능 제한 시작 전조 증상
case 2: "heavy"    // 실제 스로틀링 발생!
case 3: "trapping"
case 4: "sleeping"
default: "unknown"
}

이제 `root` 권한 없이도, `ProcessInfo`보다 훨씬 정확하고 민감하게 시스템의 스로틀링 상태를 감지할 수 있게 되었습니다.

MacThrottle 앱 완성하기

데이터 소스를 확보했으니 남은 건 구현뿐이었습니다. SwiftUI의 `MenuBarExtra`를 사용하니 메뉴바 앱을 만드는 건 식은 죽 먹기였습니다. `Info.plist`에서 `LSUIElement`를 `true`로 설정하여 Dock 아이콘을 숨기는 센스도 발휘했죠.

앱의 이름은 'MacThrottle'로 지었습니다. 평소에는 녹색이다가, 'Moderate' 상태가 되면 노란색, 'Heavy' 상태가 되면 빨간색으로 변하는 온도계 아이콘을 달았습니다. 이제 코딩하다가 맥북이 버벅거릴 때 메뉴바를 슬쩍 올려다보기만 하면 됩니다. 아이콘이 빨갛다면? "아, 좀 쉬었다 하라는 뜻이구나" 하고 커피 한 잔 하러 가면 되는 것이죠.

마치며

이번 사이드 프로젝트를 통해 얻은 교훈은 명확합니다. 때로는 제조사가 제공하는 하이 레벨 API(ProcessInfo)가 우리가 겪는 실제 현상을 모두 대변해주지 못한다는 것입니다. 그럴 때는 두려워하지 말고 한 단계 아래(System Daemon, IPC)로 내려가 볼 필요가 있습니다.

개발을 하다 보면 "안 되는 건가?" 싶어 포기하고 싶을 때가 많습니다. 하지만 시스템 어딘가에는 분명히 데이터가 흐르고 있고, 그 길목을 찾아내는 것이 우리 엔지니어들의 역할이 아닐까 싶습니다.

여러분도 맥북이 이유 없이 느려진다면, 팬 소리가 들리지 않는다고 안심하지 마세요. 속으로는 비명을 지르고 있을지도 모릅니다.

지금 읽으신 내용, 귀사에 적용해보고 싶으신가요?

상황과 목표를 알려주시면 가능한 옵션과 현실적인 도입 경로를 제안해드립니다.