내 시스템의 모든 게이트는 흉터다
요약
Claude Code를 활용해 다수의 프로젝트를 관리하며 겪은 워크플로우의 한계와 이를 극복하기 위한 시스템 재설계 과정을 다룹니다. 단순한 TODO 파일 방식에서 벗어나, 프로젝트가 늘어남에 따라 발생하는 우선순위 문제와 정보 유실을 해결하기 위한 운영 매뉴얼 구축 경험을 공유합니다.
핵심 포인트
- 프로젝트 개수가 늘어나면 개별 TODO 파일 방식은 관리 한계에 직면함
- 아이디어 유실과 우선순위 결정 장애를 방지할 통합 수집 지점이 필요함
- 시스템의 복잡도가 높아질수록 운영자를 위한 명확한 매뉴얼이 필수적임
- Claude Code를 활용한 자동화된 작업 기록과 툴링 개선의 중요성
지난주 Claude Code 세션은 내 주문 큐(order queue)에 180개의 열린 항목이 있고, 그중 13일보다 오래된 항목은 하나도 없으며, 254개의 항목 중 221개가 Claude Code 세션에 의해 작성되었다고 알려주었습니다. 스스로를 카운트한 것입니다. 시스템에서 가장 큰 단일 메일함은 자기 자신을 작업 중인 시스템이었습니다. 36개의 주문 중 약 30개는 자신의 툴링(tooling)을 개선하는 툴링에 관한 것이었습니다.
한동안 그 목록을 보지 않았습니다. 바로 이 부분이 기록할 가치가 있는 부분입니다.
이 포스트의 내용: 프로젝트당 하나의 TODO.md가 프로젝트 10개 단계에서 어떻게 작동을 멈췄는지, 그 교체물이 어떤 형태를 갖추게 되었는지, 세 가지 고장 난 요소와 각 요소가 남긴 게이트(gate), 그리고 이 모든 것이 여전히 나에게 치르게 하는 비용에 대해 다룹니다. 중간 부분은 여러분이 어떤 도구를 사용하든, 머릿속에 담을 수 있는 것보다 더 많은 리포지토리(repository)를 다룬다면 훔쳐 가야 할 핵심 내용입니다.
10개의 프로젝트와 하나의 마크다운(Markdown) 파일
나는 작년 말 Claude Code를 사용하여 여러 프로젝트를 넘나들며 작업하기 시작했습니다. 당시 전체 시스템은 각 프로젝트 폴더에 있는 TODO.md였습니다. 세션을 열고 해야 할 일을 말하면, 파일이 나머지를 기록했습니다. 두세 개의 프로젝트라면 완전히 괜찮습니다. 그렇게 하는 것을 추천합니다.
그러다 프로젝트가 10개가 되었습니다. 10개의 활성 프로젝트, Mac Studio 위의 10개 폴더, 10개의 TODO 파일, 그리고 세 가지 문제가 동시에 발생하기 시작했습니다.
아이디어들이 집으로 오는 길에 길을 잃었습니다. 외출 중에 무언가 떠올랐지만, 그것을 적을 곳은 내가 앉아 있지 않은 기기 안의 프로젝트 폴더 속 마크다운(Markdown) 파일이었습니다. 집에 돌아왔을 때, 그 아이디어는 사라지고 없었습니다.
아침 시간은 내가 내리고 싶지 않은 결정의 시간이 되었습니다. 10개의 리스트, 공유된 뷰(view) 없음, 리스트 간의 우선순위 없음. 오늘 나를 필요로 하는 프로젝트는 무엇인가? 솔직한 대답은 대개 내가 마지막으로 건드렸던 프로젝트였는데, 이는 우선순위와는 정반대였습니다.
그리고 프로젝트들은 몇 주 동안 조용해졌습니다. 프로젝트가 완료되었기 때문이 아니라, 내 설정 중 그 어떤 것도 손을 들어 신호를 보내지 않았기 때문입니다. 파일은 자신이 무시되고 있다는 사실을 말해주지 않습니다.
약 한 달 전, 저는 모든 것에 걸쳐 무엇이 가장 중요한지를 알려줄 수 있는 무언가를 원한다고 결심했습니다. 또 다른 리스트가 아니라, 리스트들 위의 계층(layer) 말입니다.
현재의 모습
지난주에 저는 계속 미뤄왔던 일을 했습니다. 바로 매뉴얼을 작성한 것입니다. 코드에 대한 문서화(documentation)가 아니라, 운영자를 위한 매뉴얼이며, 그 운영자는 바로 저 자신입니다. 제가 어디서 무엇을 하는지, 어떤 명령(command)들이 존재하는지, 어떻게 메모가 명령(order)이 되는지, 그리고 어느 네 곳에서 멈춰 인간을 기다리는지에 대한 내용입니다. 총 8개의 섹션으로 구성되었습니다. 이를 작성하는 과정은 유익한 의미에서 불편했습니다. 유일한 사용자를 위해 매뉴얼이 필요한 도구는, 그 도구가 어떻게 성장해 왔는지에 대해 당신에게 무언가를 말해주고 있기 때문입니다.
다음에 이어지는 내용은 그 매뉴얼 중 일반화된 부분입니다. 업무(work)가 될 수 있는 모든 것은 하나의 수집 지점(collection point)으로 들어가며, 거기서부터는 정확히 하나의 경로만을 가집니다.
(이곳에 다이어그램 생략. 두 개의 플로우차트(flowchart)는 원문 포스트에 있습니다.)
이 뒤에는 두 개의 클락워크(clockwork)가 작동하며, 둘 중 어느 것도 사고(think)하지 않습니다. 하나의 런칭 에이전트(launch agent)가 15분마다(StartInterval 900) 드롭 박스(drop box)를 비우며, 항목들을 적절한 프로젝트의 리스트로 분류합니다. 두 번째 에이전트는 30분마다(StartInterval 1800) 승인된 명령(order)들을 가져와서, 모든 리포지토리(repository)의 외부인 ~/.cache/master-dispatch-worktrees 아래의 git 워크트리(worktree)에서 헤드리스(headless)로 실행합니다. 분류 단계에는 어떤 모델(model)도 관여하지 않으며, 이것이 바로 이 작업을 그렇게 자주 실행해도 비용이 들지 않는 이유입니다.
제가 가장 많이 사용하는 부분은 가장 영리하지 않은 부분입니다. 저는 저 자신에게 이메일을 보냅니다.
(이곳에 다이어그램 생략. 두 개의 플로우차트(flowchart)는 원문 포스트에 있습니다.)
이것이 제 세 가지 문제 중 첫 번째에 대한 해결책입니다. 제가 언제든 접근할 수 있는 것은 저의 편지함(inbox)이기에, 그것이 진입점(entry point)이 되었습니다. 이 방식이 하지 않는 일은 메모를 업무(work)로 전환하는 것입니다. 메모는 명령(order)이 된 후 멈추며, 이는 왜 이 모든 과정에 게이트(gate)가 존재하는지에 대한 이유로 저를 이끕니다.
세 개의 세션, 하나의 파일
7월 17일, 저는 세 개의 Claude Code 세션을 병렬로 실행하고 있었으며, 이들은 모두 프로젝트별 지출 한도를 보유한 공유 설정 파일(config file)을 건드리고 있었습니다. 각 세션은 파일을 읽고, 자신의 라인을 변경한 뒤, 파일 전체를 다시 썼습니다. 전형적인 읽기-수정-쓰기 (read-modify-write) 패턴이었지만, 쓰기 주체가 스레드(thread)가 아닌 에이전트(agent)였을 뿐이며, 저는 이를 동시성 (concurrency) 문제로 전혀 생각하지 않았습니다. 마지막에 쓴 쪽이 승리했습니다. 한 세션에서 발생한 예외(exception)는 다음 세션에 의해 조용히 지워졌습니다.
해결책은 평범합니다. 이제 공유 리소스는 10분의 만료 시간(stale timeout)과 감사 로그(audit log)를 갖춘 mkdir 기반의 잠금 장치인 작은 mit-lock 래퍼(wrapper)를 통해서만 쓰기가 가능합니다. 프리-툴 훅(pre-tool hook)은 이 래퍼를 거치지 않고 보호된 경로 중 하나에 쓰기를 시도하는 모든 Bash 명령을 거부합니다.
평범하지 않았던 것은 그 이면에 담긴 깨달음이었습니다. 저는 병렬 세션을 마치 서로를 알아차릴 수 있는 병렬적인 인간들처럼 취급해 왔습니다. 하지만 그들은 알아차리지 못합니다. 그들은 프로세스(process)이며, 두 프로세스가 상태(state)를 공유하는 순간, 여러분은 두 개의 스레드에 요구하는 것과 동일한 규율을 그들에게도 적용해야 합니다. 그것이 현재 모든 것의 기반이 되는 세 가지 규칙이 탄생한 배경입니다: 저장소(repository)당 하나의 쓰기 작성자, 공유 리소스당 하나의 소유자, 프로젝트 간 작업을 위한 하나의 채널.
완료한 작업보다 더 많은 작업을 만들어낸 도구
이는 다시 상단의 수치로 저를 돌아오게 합니다.
7월 28일 기준으로 대기열에는 180개의 열린 주문(open orders)이 있었고, 그 중 13일보다 오래된 것은 하나도 없었습니다. 이는 7월 20일 이후 시스템이 하루에 약 20개의 새로운 주문을 생성하는 동안, 제가 2개에서 5개 사이의 주문을 처리했다는 것을 의미합니다. 254개의 드롭박스(drop-box) 항목 중 221개가 제가 아닌 AI 세션에 의해 작성되었으며, 그중 163개는 오케스트레이션(orchestration) 저장소 자체의 세션에서 생성되었습니다.
무엇도 망가지지 않았습니다. 그 명령들 중 하나하나가 모두 합리적이었습니다. 그것이 바로 정확한 문제입니다. Claude Code 세션은 유용해지기를 원하며, 만약 실제 저장소(repository)에서 세션을 열면 열 가지의 진정한 개선 사항을 찾아낼 것입니다. 더 깔끔해질 수 있는 리팩터링 (refactor), 최신 상태가 아닌 문서 (doc), 더 엄격해질 수 있는 테스트 (test) 같은 것들 말입니다. 이 열 가지의 합리적인 관찰 사항을 15개의 저장소에 곱하면, 그 어떤 개인도 결코 다 비워낼 수 없는 흐름이 됩니다. 제 편지함은 실수로 채워지는 것이 아니었습니다. 좋은 아이디어들로 채워지고 있었습니다.
해결책은 입증 책임을 역전시키는 것입니다. 그전까지는 누군가가 능동적으로 삭제하지 않는 한 명령이 존재했습니다. 이제는 트리거 (trigger)를 명시할 수 있는 경우에만 명령이 존재하게 됩니다. 즉, 사용자나 고객에게 발생하는 결함, 보안·자금·데이터에 대한 리스크, 배포 차단 요소 (deploy blocker), 또는 제가 직접 말하는 경우입니다. "더 깔끔해질 것이다", "지나가다 발견했다", "일관성을 위해서"와 같은 것들은 트리거가 아닙니다. 그런 것들은 파이프라인 (pipeline)이 아니라 제가 읽는 세션 요약 (session summary)에 들어갑니다. 이 방식을 역으로 적용하자 19개의 메타 명령 (meta-orders)이 한 번에 아카이브 (archive) 되었고, 총 개수는 165개에서 144개로 줄었습니다.
여기서 제가 의도적으로 구축하지 않은 것이 가장 흥미로운 부분입니다. 뻔한 선택은 AI 게이트키퍼 (gatekeeper)를 만드는 것입니다. 모델이 들어오는 각 명령을 판단하고 노이즈 (noise)를 거부하게 하는 것이죠. 저는 그렇게 하지 않았습니다. 왜냐하면 같은 주에 문장이 서툴게 작성되었지만 길고, 구조가 잘 잡혀 있으며, 매우 그럴듯해 보이는 명령이 들어온 적이 있었기 때문입니다. 만약 그 명령을 문자 그대로 받아들였다면, 186명의 고객 메일함에서 나가는 우편물을 원래 용도가 아닌 서비스를 통해 라우팅 (routing)하게 될 뻔했습니다. 모델 게이트키퍼였다면 그 명령을 그대로 통과시켰을 것입니다. 그것은 모델이 포착해야 했던 바로 그 실패 유형 (failure class)을 정확히 포함하고 있었기 때문입니다. 어떤 문제들은 또 다른 모델을 추가한다고 해서 나아지지 않습니다.
잘못된 곳을 보고 있던 감시자
가장 최근의 사례는 어제 발생한 것이며, 제가 이와 동일한 유형의 실패를 겪은 네 번째 사례입니다.
한 세션이 오직 자신의 저장소(repository)에만 기록하도록 하는 규칙은 pre-tool hook에 의해 강제되며, 어제 전까지 이 hook은 명령어를 확인하는 방식으로 작동했습니다. 이 hook은 외부 경로에 기록하는 형태인 출력 리다이렉션 (output redirection), tee, git -C, sed -i 등을 알고 있었습니다. 7월 28일, 하나의 롤아웃 스크립트(rollout script)가 Python heredoc을 통해 21개의 외부 저장소에 기록을 수행했습니다. hook은 문자열이 첨부된 python3 호출을 확인하고 이를 통과시켰는데, 이는 heredoc이 쓰기 작업처럼 보이는 목록에 없었기 때문입니다.
해결책은 의도를 추측하는 것을 중단하는 것이었습니다. 이제는 자율 실행 (autonomous run) 전의 모든 저장소의 git 상태를 기록하고, 실행 후 이를 비교하는 체크 과정이 있습니다. 이 방식은 변경이 어떻게 이루어졌는지는 상관하지 않으며, 단지 있어서는 안 될 곳에 변경이 발생했는지 여부만을 따집니다. 이 시스템은 세 곳에서 실패 시 차단(fails closed) 방식으로 작동합니다: 체크가 누락되면 디스패처(dispatcher)의 시작을 차단하고, 베이스라인(baseline) 설정에 실패하면 명령을 중단하며, 베이스라인이 없으면 '이상 없음'이 아닌 '불일치'로 간주합니다.
패턴 매칭 (Pattern matching)은 명령어가 무엇을 할지 추측합니다. 전후를 비교하는 것은 명령어가 실제로 무엇을 했는지 측정합니다. 저는 이를 배우기 위해 네 번의 시도가 필요했으며, 첫날부터 이런 방식으로 설계하지는 않았을 것입니다.
비용과 여전히 남아있는 문제
오케스트레이션 계층 (orchestration layer)은 32개의 쉘 스크립트와 약 6,300줄로 구성되어 있으며, 실제 명령에는 절대 손대지 않고 복사본을 대상으로 실행되는 145개의 테스트가 있습니다. 도구 호출 전, 중, 후에 21개의 hook이 실행됩니다: 저장소 경계, 리소스 잠금 (resource locks), 한때 200개 행 중 199개를 파괴했던 데이터베이스 덤프 플래그를 차단하는 체크, 그리고 검증된 목록에 없는 사실을 포함한 발송 메일을 거부하는 체크 등이 있습니다. 이 중 설계된 것은 아무것도 없습니다. 각 조각은 무언가가 뚫고 지나간 자리에 놓여 있습니다.
네 곳의 지점이 여전히 나를 멈춰 세우고 기다리고 있으며, 이것이 바로 놀라운 일이 아무것도 일어나지 않는 이유입니다. 내가 직접 제출한 주문을 포함하여, 어떤 주문도 내가 승인하기 전까지는 아무것도 하지 않습니다. dialogue로 표시된 작업은 나 없이는 실행되지 않습니다. 완료된 작업은 스스로 병합(merge)되지 않습니다. 그리고 텍스트에 결제, 고객 또는 운영 배포(production deploy)가 언급된 모든 것은 승인되었더라도 시작되기 전에 대기 상태로 유지됩니다. 왜냐하면 7월 27일에 50유로 규모의 고객 환불 건이 승인되고, 자율적(autonomous)으로 표시되어 실행 준비가 된 상태로 큐(queue)에 머물러 있었기 때문입니다.
오늘의 솔직한 상태는 이렇습니다: 장부상에 149개의 주문이 있으며, 그중 122개가 승인되어 대기 중입니다. 트리거 규칙(trigger rule)은 유입 속도를 늦췄을 뿐, 유입 자체를 되돌리지는 못했습니다. 근본적인 긴장 상태는 해결되지 않았으며, 해결될 수 있을지도 확신할 수 없습니다. 왜냐하면 이것은 도구(tooling)의 버그가 아니기 때문입니다. 인간이 평가할 수 있는 속도보다 더 빠르게 후보 작업을 생성하는 기계는 언제나 큐(queue)를 만들어낼 것이며, 내가 구축한 모든 게이트(gate)는 내가 의도적으로 병목 현상(bottleneck)이 되기로 선택한 지점들입니다.
만약 내가 작년 말에 10개의 프로젝트와 프로젝트당 하나의 마크다운(Markdown) 파일을 가지고 이 일을 다시 시작한다면, 다르게 할 점은 다음과 같습니다: 드롭박스(drop box)와 승인 게이트(approval gate)를 가장 먼저 구축하고, 그 외에는 아무것도 하지 않는 것입니다. 이 두 가지는 내가 한 번도 수리할 필요가 없었던 유일한 부분입니다. 이 시스템에서 그 외의 모든 것은 내가 두 번씩 다시 만들었습니다.
만약 당신이 유사한 시스템을 운영하고 있으며, 사람이 모든 항목을 읽지 않고도 유입을 정직하게 유지할 방법을 찾았다면, 그 이야기를 듣고 싶습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기