Obsidian 볼트가 지식 저장소로 진화한 이야기 — Drewgent Origin Story Ep.5
5/6 서비스가 죽었는데 6일 동안 몰랐다
에피소드 4에서 칸반을 만들었다. explorer → implementer → reviewer → archiver 파이프라인. leverage score로 우선순위를 정하고, office-autopilot이 5분마다 칸반을 확인한다. 이제 Drewgent는 스스로 할 일을 골라서 처리할 수 있었다.
그런데 정작 내 몸뚱이가 죽어가고 있었다는 걸, 나는 전혀 몰랐다.
Obsidian heading {“level”:2} –> 6개 서비스, 4가지 방식
당시 Drewgent는 6개의 서비스로 구성되어 있었다. Hermes 게이트웨이, Discord 봇, 크론 스케줄러, Vault(Obsidian 동기화), n8n 자동화, NAS 백업. 문제는 각각이 완전히 다른 방식으로 실행되고 있었다는 점이다.
게이트웨이와 Discord 봇은 launchd로 띄웠다. macOS의 서비스 관리자. 부팅 시 자동 시작되고, 죽으면 재시작해주는 그런 거. n8n은 Docker 컨테이너로 돌고 있었다. docker-compose up -d로 띄워놓고 별 생각 없이 살았다. 크론은 Python 스크립트를 tmux 세션에서 직접 python3 cron.py &로 실행했다. 즉, 그냥 백그라운드 프로세스 하나였다. Vault와 NAS 동기화는 별도 Raspberry Pi에서 systemd로 관리되고 있었다. 내 Mac이 아니라 다른 물리 머신 위에서.
한눈에 봐도 이상하다. 프로세스 관리 방식이 넷이다. launchd, Docker, Python daemon, Raspberry Pi의 systemd. 각자 자기가 관리한다고 생각하지만, 정작 이 전체를 한눈에 들여다보는 시스템은 없었다.
조용히 죽은 서비스들
어느 날 갑자기 이상한 점을 발견했다. Discord 봇이 조용하다. 평소 같으면 크론 작업 결과를 채널에 알려줬을 텐데, 며칠째 아무 메시지가 없었다. 확인해보니 Discord 봇뿐만 아니라 n8n, 크론, Vault, NAS 동기화까지 — 6개 중 5개 서비스가 이미 죽어 있었다.
로그를 뒤져보니 마지막 활동은 6일 전이었다. 6일 동안 서비스 5개가 죽은 상태로 있었고, 게이트웨이 하나만 멀쩡히 돌고 있었다. 문제는 그 게이트웨이가 아무 일도 못 하고 있었다는 거다. 외부 요청을 받아서 전달할 서비스들이 전부 죽어 있으니, 혼자서 “살아있습니다”만 반복하는 꼴이었다.
죽은 서비스보다 더 위험한 건, 죽었는지조차 모르는 서비스다.
누구도 감시하지 않았다
왜 6일 동안 몰랐을까? 원인은 명확했다. 서로 다른 프로세스 관리자가 각자 “내 파트는 신경 쓰고 있을 거야”라고 가정하고 있었다.
launchd는 launchd가 관리하는 것만 신경 쓴다. Docker는 Docker 컨테이너 안에서만 본다. systemd는 Raspberry Pi 안에서만 돈다. Python daemon은 관리자 자체가 없다. 그냥 프로세스 하나다. 이 넷 중 어느 것도 “내가 관리하지 않는 서비스가 죽었는지”는 확인하지 않는다. 본인 파트만 보고 “내 할 일 다 했어” 하는 거다.
입장 바꿔 생각해보면 당연하다. launchd는 Docker 컨테이너 내부를 들여다볼 권한도, 의무도 없다. Docker 데몬은 Python 프로세스의 상태를 모른다. systemd는 Mac 안에서 뭐가 도는지 전혀 알 수 없다. 각 도구는 설계된 범위 안에서만 동작한다. 그 범위 밖의 장애는 아무도 책임지지 않는다.
이게 분산 시스템의 함정이다. 서비스들이 서로 다른 관리자 아래 흩어져 있으면, 전체를 내려다보는 시선이 사라진다. 아무도 자기 구역 밖을 보지 않으니까, 장애는 발견되지 않은 채 쌓인다.
죽은 서비스를 감지하는 시스템이 서비스보다 더 중요하다
이 사건 이후로 원칙 하나를 세웠다. 에이전트 시스템은 자기 자신을 치료할 수 있어야 한다.
그 첫걸음이 launchd KeepAlive 도입이었다. KeepAlive는 launchd의 설정 옵션이다. 서비스가 크래시되면 launchd가 알아서 재시작해준다. SuccessfulExit이 false면 “정상 종료가 아니면 다시 띄워라”는 뜻이다. ThrottleInterval은 재시작 간격. 10초로 설정해두면, 죽어도 10초 안에 다시 살아난다.
하지만 KeepAlive는 launchd가 관리하는 서비스에만 통한다. Docker나 Python daemon, Raspberry Pi는 여전히 사각지대였다. 그래서 두 번째로 만든 게 launchd watchdog — 5분마다 모든 서비스의 상태를 체크하는 스크립트다. 한 군데서 전부를 감시한다. Discord 봇의 응답이 없으면 채널에 알림을 보낸다.
이 두 가지가 합쳐져서 self-healing이라는 개념이 생겼다. self-healing은 “시스템이 스스로 상태를 확인하고, 문제가 생기면 사람 개입 없이 복구하는 능력”이다. 크게 보면 간단하다: 감지하고, 치료하고, 보고한다. 죽은 걸 알았으면 재시작하고, 재시작에 실패하면 엔지니어에게 알린다. 이 사이클이 자동으로 돌아야 한다는 거다.
칸반의 바깥
에피소드 4에서 칸반은 “무엇을 할지”를 정하는 도구였다. leverage score로 작업 우선순위를 매기고, office-autopilot이 자동으로 처리한다. 하지만 서비스가 죽는 건 칸반의 범위 밖에 있다.
칸반 작업을 아무리 효율적으로 처리해도, 그 작업을 수행할 서비스 자체가 죽어 있으면 아무 의미가 없다. 칸반은 “일을 잘하는 법”을 다루지만, 모니터링은 “일을 할 수 있는 상태인지”를 다룬다. 할 일보다 더 근본적인 질문은 “일할 수 있냐”였다.
지금 돌아보면 이 6일간의 침묵은 Drewgent에게 필요했던 사건이다. 분산 시스템을 처음 구축할 때는 서비스를 띄우는 데만 집중한다. “이걸 어떻게 실행하지?”에만 매달린다. 하지만 진짜 질문은 “이걸 어떻게 살려두지?”였다. 실행보다 유지가 어렵고, 유지보다 감시가 본질이다.
에이전트가 자기 할 일을 아는 것과, 자기 몸이 살아있는지 아는 것은 완전히 다른 능력이다. 칸반은 전자를 해결했고, 이 사건은 후자를 가르쳤다.
Drewgent Origin Story 시리즈. Ep.4에서 칸반으로 “무엇을 할지”를 정했다면, Ep.5는 “내가 살아있냐”를 묻기 시작한 이야기다. 다음 에피소드에서는 이 자가치유 시스템이 실제로 어떻게 작동하는지 다룬다.