조용함을 믿을 수 있게 — 에이전트에게 맡긴 1인 운영 알림 체계
9월 28일, 도커를 업그레이드한 뒤 서버의 리버스 프록시가 재시작을 반복했다. CPU, 메모리, 디스크는 전부 정상이었다. 리소스 그래프만 봤다면 아무 일도 없는 날이었다. 폰을 울린 것은 그 옆에 나란히 둔 HTTP 응답 검사였다.
알림을 받는 자동화를 만들면 금방 부딪히는 문제가 있다. ‘오늘은 알릴 게 없다’ 와 ‘알리는 쪽이 죽었다’ 는 똑같이 조용하다. 폰에 아무것도 오지 않는 아침은 평온한 아침일 수도 있고, 감시가 끊긴 아침일 수도 있다. 그 침묵을 믿을 수 있어야 알림 체계가 쓸모 있다.
요즘 내 개인 서버에서는 이런 작은 감시 작업들이 돈다. 서버 세 대를 서로 감시하는 모니터링, 인기 많은 예약처의 취소표 감시, 운영 중인 앱의 지원 메일함과 스토어 리뷰 감시다. 모두 텔레그램 봇 하나로 내 폰에 도착한다. 그리고 이 코드는 내가 쓰지 않았다. 설계부터 배포까지 Claude Code 에이전트가 했고, 나는 무엇을 볼지, 무엇을 하지 말아야 할지, 결과가 믿을 만한지를 정했다. 이 글은 그렇게 만들어진 알림 체계가 어떤 모양으로 수렴했는지에 대한 기록이다.
사람이 한 일은 신원과 동의뿐이었다
돌이켜 보면 사람이 직접 손을 댄 단계는 놀라울 만큼 적었다. 그리고 전부 내 신원이나 내 동의가 필요한 일이었다.
- 텔레그램에서
@BotFather로 봇을 만들고, 그 봇에게 먼저 말을 거는 일. 텔레그램 봇은 사람이 먼저 대화를 열어야 그 사람에게 보낼 수 있다. - 발급받은 봇 토큰을 서버의 시크릿 파일에 넣는 일.
나머지는 에이전트 몫이었다. 토큰이 맞는지 확인하고, 내 대화방 id 를 찾아내고, 메시지 형식을 맞추고, 서비스 유닛을 쓰고, 배포하고, 첫 메시지가 폰에 도착하는 것까지 확인했다. 첫 봇을 붙이던 날, 내가 봇에게 말을 걸었다고 한 직후 에이전트의 조회 결과가 비어 있었다. 에이전트는 그걸 실패로 단정하지 않고 대기 중인 업데이트 수를 따로 확인했고, 잠시 뒤 메시지가 잡혔다. 텔레그램 쪽 전달이 늦었을 뿐이었다.
이 경계는 설계로 남았다. 각 작업의 배포 스크립트는 시크릿 파일을 절대 쓰지 않고, 시크릿이 비어 있으면 타이머를 켜지 않는다. 에이전트는 몇 번이고 다시 배포할 수 있지만, 토큰은 사람이 채우는 값으로 남는다.
폴더 하나, 타이머 하나, 알림 하나
작업이 늘자 모양을 정해야 했다. 내가 정한 규칙은 한 줄이다. 작은 프로그램 하나 + 타이머 하나 + 알림 하나. 화면이 생기거나 자기 데이터베이스가 생기면 그 작업은 자기 저장소로 독립한다. 그 규칙 아래 에이전트가 만든 구조는 이렇다.
automations/ # one repo, one folder per job
├── infra-watch/ # servers watching each other
├── booking-watch/ # cancelled-slot watch
└── business-watch/ # support inbox, store reviews, deadlines
/opt/automations/<job>/ # on the server
├── <job>.py # standard library only
├── .config/ # secrets — filled by a person, never by deploy
└── state/ # what was already announced, last error
auto-<job>.service / .timer # systemd, one-shot + timer공유 라이브러리는 없다. 텔레그램으로 보내는 코드는 폴더마다 따로 들어 있다. 에이전트가 코드를 쓰는 비용이 거의 0 이 되면, 중복을 없애는 것보다 작업끼리 서로 모르게 하는 것이 더 값지다. 한 작업을 고치다 다른 작업이 깨질 일이 없고, 끝난 작업은 폴더와 유닛을 지우면 사라진다.
에이전트는 함정을 다음 작업에 넘긴다
첫 텔레그램 알림을 붙이던 날, 에이전트는 막힌 곳을 전부 지식 저장소(브레인)에 문서로 남겼다. 특수문자를 이스케이프하지 않으면 알림이 통째로 사라진다는 것, 사진 캡션은 1,024자에서 잘린다는 것, 이미지를 못 가져오면 텍스트로 떨어뜨려서라도 알림은 도착하게 해야 한다는 것 같은 것들이다.
다음 날 두 번째 작업인 취소표 감시가 같은 패턴으로 붙었다. 그 작업의 설명서에서 봇 설정 절차는 한 줄이다 — 브레인의 그 문서를 보라는 것. 같은 날 오후에는 서버 감시의 아침 리포트를 다듬다가 새로 알게 된 함정 세 개 — 고정폭 표에 한글을 넣으면 칸이 어긋난다는 것 등 — 가 같은 문서에 보태졌다.
사람이 혼자 만들 때 함정은 머릿속에 남고, 석 달 뒤 같은 일을 할 때쯤이면 흐려진다. 에이전트는 세션이 끝나면 아무것도 기억하지 못하는 대신, 기억할 것을 문서로 남기는 일을 빠뜨리지 않게 할 수 있다. 다음 작업은 그 문서에서 출발한다.
침묵을 믿을 수 있게 만드는 장치
채널이 생기면 다음 문제는 무엇을 보낼지가 아니라 아무것도 안 보낼 때 그 침묵을 믿을 수 있는가다. 모든 작업에 공통으로 다섯 가지 장치가 들어갔다. 원칙은 내가 정했고, 구현은 작업마다 에이전트가 맞춰 넣었다.
- 매일 한 번은 할 일이 없어도 보낸다. 서버 감시는 매일 아침 어제의 점검 횟수와 아직 열려 있는 문제를, 취소표 감시는 어제 몇 번 조회했고 몇 번 알렸는지를 보낸다. 이 요약이 하트비트다. 요약이 오지 않으면 그 자체가 알림이다.
- 같은 실패는 한 번만 알리고, 복구도 한 번 알린다. 5분마다 같은 에러가 오면 사람은 곧 대화방을 음소거하고, 그날부터 감시는 없는 것과 같다.
- 첫 실행은 기준선만 잡는다. 메일함 감시를 처음 켜면 기존 메일 수백 통이 전부 ‘새 메일’ 이다. 첫 실행은 스냅샷만 저장하고 한 통으로 끝낸다.
- 서버끼리 서로 본다. 죽은 서버는 자기가 죽었다고 알릴 수 없으니 옆 서버가 알린다. 둘 다 죽으면 그날 아침 요약이 안 온다. 하트비트가 마지막 그물이다.
- 리소스만 보지 않는다. 글 첫머리의 장애가 그 이유다. 서버가 건강한 것과 서비스가 응답하는 것은 다른 질문이라, HTTP 응답과 컨테이너 상태를 리소스 검사 옆에 나란히 둔다.
그리고 하나가 뒤늦게 추가됐다. 보낼 게 비었으면 보내지 말고 실패해야 한다. 설정이 빠진 채 배포돼 제목만 있는 리포트가 나간 날이 있었다. 로그에는 ‘전송 성공’ 이 찍혔다. 에이전트가 본 로그로는 성공이었고, 이상함은 내가 폰을 보고서야 알았다. 에이전트가 검증할 수 있는 것은 기계가 남긴 신호까지다. 그 신호가 거짓말을 하지 않게 만드는 규칙은 결과를 직접 받아 보는 사람이 찾아내야 했다.
데이터가 거짓말할 때는 원천을 한 번 더 본다
취소표 감시에서는 반대 방향의 문제가 나왔다. 알림이 너무 시끄러웠다.
어느 날 특정 시간대가 몇 시간째 ‘빈 자리 있음’ 으로 잡혔다. 확인해 보니 예약처가 온라인 예약 자체를 꺼 둔 상태였다. 그런데도 스케줄 조회는 좌석을 계속 내주고 있었다. 스케줄은 ‘좌석 재고’ 를 말할 뿐, ‘지금 예약을 받는가’ 를 말하지 않았다.
그래서 빈 자리가 보이면 장소 페이지를 한 번 더 조회해 예약이 실제로 열려 있는지 확인하도록 바뀌었다. 원칙으로 정리하면 이렇다. 신호가 오면, 그 신호가 가리키는 행동이 실제로 가능한지를 원천에서 한 번 더 확인한다. 알림을 받고 들어갔는데 할 수 있는 게 없으면, 그 알림은 채널 전체의 신뢰를 깎는다.
자동화는 알리기까지, 조치는 사람이
에이전트에게 맡기면서 끝까지 지킨 선이 하나 있다. 봇은 되돌리기 어려운 일을 하지 않는다.
메일함 감시는 라벨만 붙인다. 보내지도, 초안을 쓰지도, 지우지도 않는다. 텔레그램에는 발신자와 제목, 메일 링크만 가고 본문은 가지 않는다. 알림 채널은 메일함보다 보호 수준이 낮은 곳이고, 본문을 옮기는 순간 그 채널이 메일함의 사본이 된다. 취소표 감시는 예약을 대신 하지 않는다. 예약처가 본인 계정으로 직접 예약하기를 요구하고, 자동 예약은 플랫폼 약관 위반이기도 하다.
이 선은 두 겹이다. 에이전트가 만든 봇이 사람 대신 조치하지 않듯, 봇을 만든 에이전트도 사람 대신 자격을 다루지 않는다. 안쪽은 ‘관찰과 전달’ 만 하고, 바깥쪽은 ‘구현과 배포’ 만 한다. 판단과 동의는 둘 다 사람에게 남는다.
마무리
이 작업들을 다시 보면, 정작 ‘할 일’ 을 하는 코드는 얼마 되지 않는다. 스케줄을 조회하고, 라벨을 붙이고, 응답 코드를 보는 부분은 짧다. 나머지는 거의 전부 침묵에 의미를 부여하는 장치다. 매일 오는 요약, 한 번만 오는 실패와 복구, 비어 있으면 던지는 예외, 원천을 한 번 더 보는 확인, 시크릿이 없으면 켜지지 않는 타이머.
그 장치들의 코드는 에이전트가 썼다. 하지만 어떤 침묵을 믿지 말아야 하는지는 폰을 들고 있는 사람만 알 수 있었다. ‘전송 성공’ 로그 뒤의 빈 리포트를 알아챈 것은 결과를 받아 보는 쪽이었다. 에이전트에게 일을 맡긴다는 것은 손을 떼는 것이 아니라, 손을 대는 자리가 구현에서 결과를 받아 보고 규칙을 고치는 자리로 옮겨 가는 것이다.
다음 글에서는 이 알림 체계 위에 모델을 한 겹 더 얹은 이야기를 쓴다. 매일 아침 피드 33개를 모아 claude -p 로 요약해 보내는 브리핑과, 그 브리핑이 아흐레 동안 조용히 멈췄던 이유다.
댓글 0