AI 에이전트를 위해 Mac에 야간 경비병을 배치하다: 녹색 테스트 70개와 실제 사례 3가지
요약
AI 에이전트 작업 중 Mac 시스템이 절전 모드로 진입하거나 종료되는 문제를 해결하기 위해 '야간 경비병' 시스템을 개발했습니다. 이 시스템은 `caffeinate` 프로세스 감지, 상태 기계 로직, 그리고 `nohup` 등을 활용하여 안정적인 근무 환경을 구축하는 방법을 제시합니다.
핵심 포인트
- 에이전트 작업 중 Mac 절전 모드 진입 방지가 핵심 과제입니다.
- 작업 상태 확인은 단순 프로세스 존재 여부보다 정교한 신호 감지(예: `caffeinate` 자식 프로세스)가 필요합니다.
- 시스템 안정성을 위해 순수 상태 기계와 70가지 시나리오 테스트를 거쳤습니다.
- 스크립트 실행의 영속성 확보를 위해 `nohup`과 관리자 권한 관리가 중요합니다.
한국 군대에서 불침번은 밤샘 근무를 의미합니다. 한 병사가 밤새 깨어있어야 다른 모든 사람들이 잠을 잘 수 있습니다. 경계 근무 중에 잠들면 곤란한 상황이 벌어집니다.
제 Mac은 정반대의 문제를 가지고 있었습니다. 너무 잘 자는 것이었죠.
저는 Claude Code에게 "이거 리팩토링해 줘"라고 말하고 잠자리에 들었습니다. 아침이 되면 세 가지 중 하나가 발생했습니다.
- Mac이 절전 모드로 들어가 에이전트가 중간에 멈춰버린 경우 😇
caffeinate -d를 실행 상태로 두고 밤새 화면이 켜져 있던 경우- 최악의 경우:
sudo pmset disablesleep 1을 되돌리는 것을 잊어 배터리가 0%가 된 경우
그래서 저는 제 Mac에게 야간 경계병을 만들어주었습니다. 에이전트가 작업하는 동안, 메뉴 바 아래를 손전등을 들고 순찰하는 픽셀 아트 사병이 있다가 "교대합니다! 🫡"라고 경례하며 Mac이 잠들게 합니다.
에이전트가 "작업 중"인지 어떻게 알 수 있을까요?
버전 1은 간단했습니다. Claude Code 프로세스가 존재하면, 근무를 유지하는 것이었습니다.
결과: 교대가 끝나지 않았습니다. 저는 항상 여러 개의 Claude Code 세션을 열어둡니다. 열려있는 세션 개수를 세는 것은 사병이 해제될 때까지 경계를 서야 한다는 의미였습니다.
제가 완성한 것:
| 신호 | 이유 |
|---|---|
에이전트가 caffeinate 자식 프로세스를 가지고 있음 | Claude Code는 작업 중일 때만 자체적으로 caffeinate -i -t 300을 생성합니다. |
| ... |
저는 5초 간격의 CPU 샘플링으로 시작했습니다. 에이전트가 시작한 파일 감시기가 이따금씩 약간의 CPU를 사용했고, 교대는 18분 동안 끝나지 않았습니다. 이를 60초 평균으로 변경하니 문제가 해결되었습니다. 사병이 게으름을 피운 것이 아니라, 제 측정 방식에 문제가 있었던 것입니다.
근무 로직은 시스템 호출(system calls)이 없는 순수 상태 기계(pure state machine), 즉 DutyEngine 안에 존재합니다. 가짜 시계를 이용해 70가지 시나리오 테스트를 진행했습니다: "5분 동안 조용함 → 인계", "배터리 20% 미만 → 대기", "과열 → 휴식 후 재개".
모두 정상. 완전한 확신을 가지고 배포할 수 있었습니다.
인시던트 1: 감시견이 담당관과 함께 퇴근하다
뚜껑을 닫은 채 깨어 있게 하려면 pmset -a disablesleep 1가 필요합니다. 문제는 앱이 강제 종료되면 그 설정이 유지된다는 것입니다. 다음 장소는 배낭 속의 뜨거운 Mac입니다.
그래서 관리자 권한으로 활성화할 때, 저는 또한 nohup을 사용하여 감시견 스크립트를 실행했습니다. 만약 앱이 죽으면 감시견이 원래 설정을 복원합니다.
실제 기계에서 측정한 결과: 감시견은 관리자 셸이 종료되자마자 사라졌습니다. 근무를 서던 병사는 담당관과 함께 퇴근한 것입니다.
이는 osascript의 do shell script ... with administrator privileges가 완료될 때 같은 세션의 자식 프로세스를 정리하는 것처럼 보였습니다. nohup은 SIGHUP만 무시하므로 살아남지 못했습니다.
수정: /usr/bin/perl을 통해 포크(fork)하고 setsid()를 호출하여 새 세션으로 완전히 분리해야 했습니다. 이것이 인시던트 1b를 유발했습니다: A && B &는 서브셸을 남겨두었고 osascript가 절대 반환하지 않았습니다. 최종 버전은 파이프(pipe)를 통해 동기화합니다: 부모가 종료하는 것은 자식이
macOS는 Developer ID(Team ID)가 없는 ad-hoc 서명된 앱에는 알림 권한을 전혀 부여하지 않습니다. 그리고 제 권한 요청은 다음과 같았습니다:
center.requestAuthorization(options: [.alert, .sound]) { _, _ in }
{ _, _ in }. 실패를 우아하게 무시합니다. 이 사설 에이전트는 보고서를 작성하여 휴지통에 바로 넣는 습관을 가지고 있었습니다.
이제 권한 없이 작동할 때는 osascript의 display notification으로 폴백(fallback)됩니다. 텍스트는 스크립트 문자열 안에 삽입되는 것이 아니라 argv로 전달되므로 AppleScript 주입 표면이 없습니다.
교훈 (Lessons)
- 순수 로직 테스트는 당신이 의도한 것만 검증합니다. OS가 실제로 그것을 할 수 있게 해주는지는 실제 기기가 필요합니다. 세 가지 사건 중 어느 것도 단위 테스트(unit tests)로 포착될 수 없었습니다.
- 실패를 삼키는 콜백(
{ _, _ in })은 AI든 사람이 작성했든 가장 조용하고 위험한 버그입니다. - Claude Code로 이것을 구축하면서 가장 유용한 습관은
👉 https://github.com/s3onghyun/bulchimbeon
End of watch report. 🫡
감사합니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기