AI한테 시키면 알아서 하는 거 아냐?
“AI한테 시키면 알아서 하는 거 아냐?”
매년 5월이 되면 삼쩜삼 간편인증 팀은 전쟁이다. 종소세 시즌, 트래픽은 평소의 10배. PM 4~5명이 15분마다 Amplitude 대시보드를 직접 열어서 지표를 확인하고, 이상하면 Slack에 수동으로 올린다. 누군가 자리를 비우면 그 시간대는 그냥 구멍이 난다.
팀에서 이 대시보드를 가장 자주 들여다보던 도연은 생각했다. “이건 사람이 할 일이 아닌데.”
5분마다 알아서 보고하는 시스템
도연이 만든 건 단순하다. 5분마다 퍼널 지표 4개를 자동으로 확인하고, Slack 쓰레드에 신호등을 올려주는 시스템. 초록불이면 넘기고, 빨간불이면 당일 담당자한테 멘션이 간다.
macOS의 launchd가 5분마다 claude -p를 실행한다. Claude가 MCP(외부 서비스를 AI가 직접 호출할 수 있게 해주는 프로토콜)를 통해 Amplitude에서 차트 4개를 쿼리하고, 판정하고, Slack에 올린다. 한 번 실행되면 세션은 바로 종료. Haiku 모델을 써서 토큰 소모도 최소화했다.

출근해서 Slack 채널을 열면 밤새 쌓인 리포트가 쓰레드에 있다. 전부 초록불이면 3초 만에 확인 끝. 작년에는 “체감상 느린 것 같은데…”였다면, 올해는 5분 단위 수치가 있다.
같은 알림이 세 번씩 온 날
그런데 어느 날, Slack에 같은 메시지가 18초 간격으로 세 번 들어왔다.
도연이 세션 로그를 까봤다. Claude가 curl로 Slack에 메시지를 보냈는데, 메시지 자체는 잘 갔다. 문제는 그 다음이었다. launchd 환경의 locale이 C로 잡혀 있어서 한글이 들어간 쉘 명령어 처리에서 에러가 터졌다. Claude는 “응답 처리가 실패했네”라고 판단하고 Python으로 다시 짜서 보냈다. 또 같은 에러. 또 다른 방식으로. 결국 Slack에는 세 번 들어가 있었다.
API 호출은 매번 성공했는데, LLM이 후속 쉘 처리 에러를 보고 “전체 실패”로 잘못 읽은 거다.
도연이 찾은 해결책은 이렇다. locale 문제 자체를 plist에서 잡고, Slack 전송을 별도 Python 스크립트 한 곳으로 모았다. 같은 분 안에 두 번째 호출이 오면 자동으로 무시되게 멱등 락도 걸었다.
LLM은 JSON도 “더 자연스럽게” 만들고 싶어한다
두 번째 사고는 더 교묘했다. 스킬 파일 안에 Amplitude API 호출용 JSON을 인라인으로 넣어두고 “이대로 호출해”라고 했는데, Claude가 이걸 슬쩍 바꿔버렸다.
"timezone"이 "timeZone"으로, "useGranularTimestamps"가 "precisionTimestamp"로. LLM이 markdown 텍스트를 보면 “더 자연스럽게” 만들려는 충동을 받는 것 같다. 학습 데이터에 camelCase가 더 흔해서 그런 건지는 모르겠지만, 어쨌든 변환해버린다.
진짜 위험한 건 Amplitude가 모르는 필드를 조용히 무시한다는 점이었다. 에러는 안 나는데 결과가 미묘하게 달라진다. 가장 찾기 어려운 종류의 버그.
해결은 간단했다. JSON 정의를 별도 파일로 빼고, 스킬에서는 “이 파일을 Read 해서 그대로 패스”만 시켰다. LLM은 markdown 텍스트는 변형해도, 파일에서 직접 읽어온 내용은 건드리지 않았다.
변형되면 안 되는 건 가둬야 한다
두 번의 사고에서 도연이 뽑아낸 원칙은 하나다. 외부로 나가는 호출은 모두 별도 파일로 분리해두고, LLM은 그걸 “그대로 패스”만 시킨다.
LLM이 자유롭게 코드를 변형할 수 있는 능력, 그 자체가 양날의 검이다. 외부 API 호출, 정확한 스키마 매칭이 필요한 정의, 멱등성이 중요한 로직 — 이런 건 LLM의 변형 충동을 미리 가둬야 안정성이 나온다. “자연어로 시키면 알아서 한다”는 건 일부만 사실이다.
결국 자동화의 핵심은 “어디까지 맡기고 어디부터 잠그느냐”의 경계를 정하는 일이었다. 그리고 지금, 그 경계를 제대로 그은 모니터링은 5분마다 조용히 돌아가고 있다. 올해 종소세 전쟁에서는 아무도 대시보드를 직접 열지 않아도 된다.