Cron'd Claude 에이전트: 유지보수 기록
요약
20개의 Claude 에이전트 함대를 launchd 스케줄러로 운영하며 겪은 유지보수 경험을 다룹니다. 스케줄링 지연으로 인한 실행 중첩 문제, 환경 변수 설정 오류, 그리고 턴 횟수 기반의 예산 관리 전략을 상세히 설명합니다.
핵심 포인트
- launchd의 실행 지연으로 인한 에이전트 간 작업 중첩 및 SQLite 잠금 문제 해결
- launchd 환경에서의 PATH 설정 및 인터프리터 절대 경로 사용의 중요성
- 토큰 대신 턴 횟수(turn-count) 기반의 예산 관리 및 모니터링 체계 구축
- 강제 적용(enforcement) 이전에 계측(instrumentation) 단계가 선행되어야 함
20개의 Claude 에이전트 함대를 launchd 스케줄에 따라 실행해 보면, 스케줄러의 보장 기능이 예상보다 약하다는 것을 빠르게 배우게 됩니다. 각 에이전트는 깨어나서 작업의 일부를 처리하고, 결과를 기록한 뒤, 다음 틱(tick)이 올 때까지 다시 잠듭니다. 패턴은 단순하지만, 실패 모드(failure modes)는 그렇지 않습니다. 3개월 동안 매일 밤 실행한 결과, launchd의 타이밍 특이점, 세션 종료 실패, 그리고 증가하는 턴 수(turn counts)에 따른 조용한 압박이 드러났습니다. 이 포스트는 솔직한 유지보수 기록입니다. 무엇이 고장 났고, 왜 고장 났으며, 무엇을 변경했는지에 대해 다룹니다.
스케줄러는 마법의 타이머가 아니다
처음 cron 스타일의 작업을 설정했을 때, 저는 macOS와 깔끔하게 통합되고 시작 시간, 리소스 제한, 재시작 정책에 대해 세밀한 제어를 제공하는 launchd를 사용했습니다. 첫 번째 놀라움은 launchd가 정확한 시작 시간을 보장하지 않는다는 점이었습니다. 시스템이 바쁘면 작업이 몇 초 동안 지연될 수 있으며, 그 몇 초가 하루 동안 쌓이게 됩니다. 단일 에이전트의 경우 드리프트(drift)는 무시할 수 있는 수준이지만, 20개의 에이전트 함대에서는 누적된 지연이 마지막 실행을 의도한 시간 범위를 넘어서게 만들어 실행이 중첩되는 현상을 일으킬 수 있습니다.
중첩된 실행은 두 에이전트가 동시에 동일한 SQLite 파일에 쓰려고 시도하는 방식으로 나타났습니다. SQLite는 쓰기를 위해 파일을 잠그기(lock) 때문에, 두 번째 에이전트는 잠금이 해제될 때까지 중단되었습니다. 빡빡한 스케줄에서 이는 타임아웃(timeout)의 연쇄 반응을 의미했으며, 결국 launchd 데몬(daemon)은 해당 작업을 실패로 표시했습니다. 해결책은 각 에이전트의 launchd plist에 고유하고 고정된 시작 분(minute)을 할당하는 것이었습니다. 여러 에이전트가 동일한 시작 슬롯을 공유하게 두는 대신, 하루 동안 스케줄을 수동으로 간격을 두어 배치함으로써 각 에이전트에게 중첩 없는 명확한 시간 창을 제공했고 쓰기 잠금(write-lock) 연쇄 반응을 제거했습니다.
또 다른 숨겨진 특이점은 launchd의 환경 변수(environment variables) 처리 방식입니다. 에이전트들은 Python 인터프리터와 몇 가지 헬퍼 스크립트가 포함된 PATH에 의존합니다. launchd가 작업을 실행할 때, 사용자의 쉘 프로필(shell profile)을 포함하지 않는 최소한의 환경을 상속받습니다. 초기 몇 차례의 실행은 인터프리터를 찾을 수 없어 "command not found" 오류와 함께 실패했습니다. 해결책은 launchd plist 내부에 전체 PATH를 정의하고, 인터프리터를 절대 경로(absolute path)로 참조하는 것이었습니다. 이를 통해 작업들이 어떠한 대화형 쉘(interactive shell) 설정에도 의존하지 않게 되었습니다.
턴 예산(Turn budgets)은 토큰이 아니라 횟수입니다
이 플릿(fleet) 내의 Claude 에이전트들은 토큰 예산(token budgets)이 아니라 티켓당 턴 횟수(turn-count) 상한선에 따라 작동합니다. 이 상한선은 중앙 설정 파일에 저장되며 작업 유형에 따라 다릅니다. 예를 들어, 빌드(build) 티켓은 리뷰(review) 티켓보다 더 높은 상한선을 가집니다. 수치는 과거 실행 데이터를 기준으로 측정된 평균의 약 두 배 정도로 보정되었으므로, 정상적인 작업이 한계치에 도달하는 일은 거의 없어야 합니다.
모니터링 단계가 먼저 이루어졌습니다. 턴 모니터(turn monitor)가 각 에이전트 세션과 함께 실행되며 상한선에 도달하면 경고를 발생시킵니다. 그 이후에 이루어진 것이 강제 적용(enforcement)입니다. 실질적인 교훈은 계측(instrumentation)이 강제 적용보다 앞서야 한다는 것입니다. 어떤 티켓 유형이 오래 실행되는지 알지 못한다면, 설정한 모든 상한선은 특정 유형의 정상적인 작업을 중단시키거나 다른 유형에는 너무 느슨하게 적용될 것입니다. 먼저 계측하십시오. 실제 데이터를 바탕으로 보정하십시오. 그리고 나중에 강제 적용하십시오.
세션 종료 처리 및 데이터 소유권
Claude 에이전트는 각 실행이 끝날 때 세션 요약(session summary)을 자동으로 저장합니다. 가장 흔한 실패 모드는 처리되지 않은 예외(unhandled exception)로 인해 Python 프로세스가 갑자기 종료되는 경우였습니다. 프로세스가 종료되면 SQLite 트랜잭션(transaction)이 열린 상태로 남게 되고, 다음 실행 시 잠긴 데이터베이스에 쓰기를 시도하게 됩니다. 이 잠금 상태는 운영 체제가 파일 핸들(file handle)을 회수할 때까지 지속되었는데, 여기에는 몇 분이 걸릴 수도 있었습니다. 그 시간 동안 플릿 전체가 중단되었습니다.
세션이 정상적으로 종료되든 그렇지 않든, 종료 시 모든 데이터베이스 연결 (database connection)을 명시적으로 닫도록 보장하는 것이 해결책입니다. 충돌(crash)이 발생한 프로세스에 의해 열려 있는 연결은 운영체제(OS)가 파일 디스크립터 (file descriptor)를 회수할 때까지 쓰기 잠금 (write lock)을 유지합니다. 가비지 컬렉션 (garbage collection)에 의존하는 대신 에러 경로 (error path)에 명시적인 닫기 호출을 추가함으로써, 잠금 유지 시간을 단축하고 다음 예정된 실행을 깨끗하게 유지할 수 있습니다.
저는 이전에 에이전트 세션 간 컨텍스트 손실의 비용 (cost of context loss between agent sessions)에 대해 작성한 적이 있는데, 해당 포스트는 토큰 낭비에 초점을 맞추었습니다. 여기서 문제는 구조적인 결과입니다. 즉, 누락된 쓰기 작업이 다운스트림 (downstream) 데이터를 손상시킨다는 점입니다.
또 다른 미묘한 문제는 미결 질문 (open questions)의 처리였습니다. 에이전트는 실행 중에 발생하는 답변되지 않은 항목들을 포착하려고 시도합니다. 이 포착은 최선 노력 (best-effort) 방식입니다. 만약 세션에 충분한 신호 (signal)가 포함되어 있지 않다면, 질문은 기록되지 않습니다. 초기에는 모든 미결 질문이 저장될 것이라고 가정하고, 누락된 행 (rows)을 기반으로 다운스트림 알림을 구축했습니다. 포착이 조용히 실패했을 때, 알림은 노이즈를 생성하고 모니터링 시스템에 대한 신뢰를 떨어뜨렸습니다. 미결 질문 로그를 엄격한 계약 (contract)이 아닌 유용한 힌트로 취급하고, 누락된 항목을 허용할 수 있도록 다운스트림 프로세스를 설계하십시오.
3개월이 내게 가르쳐준 것
Claude 에이전트 플릿 (fleet)을 일정에 따라 실행하는 것은 한 번 설정하면 끝나는 (set-and-forget) 작업이 아닙니다. 스케줄의 신뢰성이 단순한 속도보다 더 중요합니다. 실행 예산 (turn budgets)은 강제 적용하기 전에 계측 (instrumentation)이 필요합니다. 견고한 세션 처리 (session handling)는 전체 플릿을 중단시킬 수 있는 연쇄 실패 (cascading failures)를 방지합니다.
만약 당신이 자율 에이전트 플릿 (agent fleet)을 구축하는 데이터 엔지니어 또는 AI 실무자라면, 이것들이 당신의 유지보수 체크리스트 항목입니다. 잘 정의된 스케줄 (schedule)로 시작하고, 첫날부터 턴 횟수 (turn counts)를 모니터링하며, 데이터 지속성 (data persistence)이 충돌 (crash)에 견딜 수 있도록 만드세요. 만약 처음부터 에이전트형 데이터 파이프라인 (agentic data pipeline)을 구축하는 것을 고려하고 있다면, LangGraph 구현 가이드에서 여기서 다룬 운영적 결정에 앞서 이루어져야 할 아키텍처 결정들을 다루고 있습니다.
이 포스트는 지속적으로 작성될 유지보수 기록 (maintenance log)의 첫 번째 글입니다. Labyrinth Analytics 블로그에서 전체 포스트를 읽거나, 이미 운영 중인 플릿이 있고 설계에 대한 제2의 의견 (second pair of eyes)이 필요하다면 문의해 주세요.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기