Claude Code로 만드는 자율 멀티 에이전트 개발 체제 — 허브&스포크로 여러 프로젝트를 동시에 굴리기
목차
Claude Code를 단일 세션으로 운용하면, 여러 프로젝트를 몇 달씩 병렬로 돌리려는 순간 무너집니다. 이 책은 그 진단에서 출발해, 감독역(허브)과 실무역(스포크)으로 나눠 자율 가동시키는 체제를, 실제로 몇 달간 운영해 온 설정 파일·운영 규칙·실패담을 1차 정보로 풀어냅니다. tmux로 병렬 실행하는 방법 자체보다, 상태를 파일에 정리해 두어 세션이 죽어도 곧바로 복구되는 설계, 사람의 승인 게이트, 모델 계층에 따른 비용 배분, 그리고 실제 장애 대응까지 파고듭니다. 장기 운용을 견디는 "체제 만드는 법"을 다루는 책입니다.
제1장 — "1세션 Claude Code"가 한계에 부딪히는 이유
어느 밤, 세 개의 태스크가 동시에 막혔다
Claude Code를 쓰기 시작하고 몇 달쯤 지났을 무렵, 매번 같은 지점에서 막히기 시작했습니다.
평일 밤, 대회용 코드 수정을 Claude Code에 맡겨 두고, 다른 터미널에서는 연구 주제 서베이를 정리하게 하고, 그 틈에 개인 개발 중인 수익화 프로젝트 원고까지 쓰게 한다. 이걸 세션 하나로 다 하려고 하면 대개 이렇게 됩니다.
대회 실험 결과를 보며 다음 지시를 궁리하는 사이, 조금 전까지 이야기하던 연구 주제의 컨텍스트가 어느새 밀려나 사라져 있습니다. "아까 말한 그 수정, 아직 안 했지?"라고 물어봐도 세션은 이미 기억하지 못합니다. 태스크 세 개를 대화 하나로 오갈 때마다 Claude 자신도 "지금 어느 프로젝트 이야기를 하고 있는지" 매번 다시 확인해야 하니, 지시가 점점 길어집니다. 정신 차려 보면 어느새 내가 "진척 관리 담당"이 되어 있습니다. 구현은 에이전트에게 맡기고 싶었는데, 정작 에이전트의 기억을 사람이 대신 짊어지고 있는 셈입니다.
이건 능력의 문제가 아닙니다. 대화 세션 하나에 전부 욱여넣는 설계 자체가 여러 태스크를 병행하는 사용법과 애초에 맞지 않을 뿐입니다.
이 책에서는 바로 이 벽에 부딪힌 제가 실제로 짜서 몇 달간 운영해 온 체제를 소개합니다. 여러 Claude Code 세션에 역할을 부여하고, 상태를 파일에 떨궈 두고, 위험한 작업은 격리하고, 태스크의 무게에 따라 모델을 나눠 쓰는 "허브&스포크" 체제입니다. 구체적인 설정과 커맨드, 실패담까지 함께 풀어 나가겠습니다.
단일 세션 운용이 무너지는 세 가지 벽
왜 1세션 운용은 여러 프로젝트를 동시에 안게 되는 순간 무너질까요? 원인은 세 가지로 나눌 수 있습니다.
벽 1: 컨텍스트는 유한하고, 게다가 "읽기만 해도" 줄어든다
Claude Code 세션이 담을 수 있는 컨텍스트 윈도에는 상한이 있습니다. 이 상한은 내가 입력하는 지시만이 아니라 파일을 읽고, 커맨드 출력을 읽고, 과거 대화를 떠올리는 그 모든 행위로 깎여 나갑니다.
여러 프로젝트를 한 세션에서 다루면, 프로젝트 A의 코드를 읽어들인 직후 프로젝트 B 이야기로 넘어갔다가 다시 돌아오는 일을 반복하게 됩니다. 그때마다 "지금은 상관없는 정보"까지 컨텍스트에 계속 눌러앉아, 정작 필요한 정보를 밀어냅니다. 이건 대화가 길어질수록 심해지고 한 방향으로만 나빠지는 성질이라, 세션을 재시작하지 않는 한 되돌아오지 않습니다.
벽 2: 주의는 단선적이라, 병행하는 것처럼 보일 뿐이다
Claude Code는 한 번에 응답 하나만 생성할 수 있습니다. 여러 태스크를 "병행해서 시키고 있는" 것처럼 보여도, 실제로는 세션 하나가 순서대로 처리하고 있을 뿐입니다. 오래 걸리는 실험을 하나 돌리는 동안, 그 세션은 (백그라운드 실행 구조를 쓰지 않는 한) 다른 데 쓸 수 없습니다.
사람 쪽에서 "그럼 다음은 이쪽 태스크를"이라고 말을 걸어도, 세션의 의식은 지금 하던 작업에 향해 있어서, 맥락을 넘겨주는 데 비용이 듭니다. 결국 "다음은 무엇을 시킬지" 스케줄링을 사람이 떠안게 되는데, 이거야말로 원래 에이전트에게 맡기고 싶었던 일 그 자체입니다.
벽 3: 태스크 경계가 흐릿한 채로, 판단의 입자 크기가 안 맞는다
세션 하나에서 "가벼운 조사"도 "중요한 설계 판단"도 "위험도 높은 코드 실행"도 똑같이 다루면, 이런 문제가 생깁니다.
- 가벼운 태스크에도 무거운 모델을 써서 비용과 시간을 낭비한다.
- 반대로 중요한 판단을 대충 끝내 버려서, 나중에 되돌리는 일이 생긴다.
- "이건 사람 승인이 필요한 조작인가"의 선 긋기가 그때그때 흔들려서, 정신 차려 보면 외부 공개나 과금을 수반하는 조작까지 "기세로" 실행하려 든다.
세 가지 벽은 독립된 문제가 아니라 뿌리가 하나입니다. "뇌(세션) 하나에, 성질이 다른 여러 일을 전부 욱여넣으려 한다." 여기에 다 담깁니다.
발상의 전환: 에이전트를 "한 명"이 아니라 "팀"으로 다룬다
이 벽을 넘는 발상은 단순합니다. 사람의 조직 운영에서 그대로 빌려올 수 있어요. 매니저 한 명이 모든 프로젝트의 실무를 자기 손으로 해내는 게 아니라, 건별로 담당자를 세우고 매니저는 진척 관리와 의사 결정에 집중한다. 그걸 Claude Code의 세션 설계에 그대로 대입합니다.
구체적으로는 역할을 둘로 나눕니다.
- 허브(hub): 전체를 감독하고, 태스크를 고르고, 담당 세션에 지시를 내리고, 성과물을 검수하고, 사람에게 보고하는 역할. 자기가 직접 구현이나 실험을 하지는 않는다.
- 스포크(spoke): 프로젝트마다 세우는, 실작업 전담 세션. 담당 프로젝트의 코드를 만지고, 실험을 돌리고, 원고를 쓴다.
이 분업에는 단순히 "역할을 나눈다" 이상의 효과가 있습니다. 허브는 실작업을 하지 않으니 컨텍스트 소비를 최소한으로 억제할 수 있어요. 허브가 읽는 건 각 프로젝트 상태를 요약한 파일과 스포크의 보고뿐이면 충분하고, 코드 내용이나 로그 전문을 읽어들일 필요가 없습니다. 그 덕에 허브는 오랫동안 다운되지 않고 전체를 계속 감독할 수 있습니다. 한편 스포크는 프로젝트 하나에 전념하니, 그 프로젝트의 맥락에만 컨텍스트를 다 쓸 수 있습니다.
게다가 이 구조는 위험도에 따른 격리와도 잘 맞물립니다. 실험적인 코드를 시행착오하는 스포크에는 샌드박스 환경을 주고, 호스트 환경에는 손대지 못하게 한다. 다루는 조작의 무게에 따라 모델 등급을 바꾼다. 그리고 사람 승인이 필요한 조작(공개, 과금, 외부 전송 등)은 어느 세션이든 반드시 사람에게 되돌린다는 규칙을 처음부터 심어 둡니다.
체제의 전체 그림
이 책에서 다루는 체제를 그림으로 그리면 이렇게 됩니다.
flowchart TD
subgraph Hub["허브 (감독 세션)"]
H["감독 에이전트
• 계획 수립
• 위임
• 검수
• 상태 유지
❌ 직접 구현 안 함"]
end
subgraph Workers["워커 세션 (tmux)"]
A["프로젝트 A"]
B["프로젝트 B"]
C["프로젝트 C"]
end
Human["사람"]
Sandbox["샌드박스
일회용 Docker"]
H -->|"위임"| A
H -->|"위임"| B
H -->|"위임"| C
A -->|"결과"| H
B -->|"결과"| H
C -->|"결과"| H
H -. "승인 필요" .-> Human
Human -. "승인 / 반려" .-> H
A -. "위험한 작업" .-> Sandbox
구성 요소는 크게 다음 네 가지입니다.
- 허브: 감독 전담 세션. 각 프로젝트의 TASKS.md(태스크 관리)만 읽고, 실행 가능한 태스크를 골라 스포크에 던진다.
- 스포크: tmux 위에서 도는, 프로젝트별 독립 Claude Code 세션. 허브에게서 지시를 받아 실작업을 하고, 상태를 파일에 써서 되돌린다.
- 샌드박스: 위험도 높은 조작(강한 권한으로 코드 실행 등)을 격리하는 Docker 컨테이너. 프로젝트 디렉터리만 마운트하고, 호스트 파일 시스템에는 물리적으로 손대지 않는다.
- 모델 계층: 태스크의 무게에 따라 경량 모델·표준 모델·고정밀 모델을 나눠 쓰는 구조. "헷갈리면 싼 쪽부터 써 본다"를 원칙으로 삼는다.
이 모두를 떠받치는 게 상태를 대화의 기억이 아니라 파일에 갖게 한다는 설계입니다. TASKS.md에 할 일·하는 일·멈춘 일·끝난 일을 쓰고, JOURNAL.md에 작업 로그를 남깁니다. 이 두 파일만 있으면 어느 세션이 죽든, 어느 세션을 다시 만들든, 곧바로 상황을 복원할 수 있습니다. 제3장에서 자세히 다룰, 이 책 전체를 통틀어 가장 중요한 설계 원칙입니다.
구체적으로 어떻게 돌아가는지, 하루치 예시
감이 잡히도록, 실제 하루의 흐름을 일반화해 보여 드리겠습니다.
프로젝트 이름은 이 책 전체에 걸쳐 가상의 것으로 바꿔 두었습니다.
아침, 허브 세션이 각 프로젝트의 TASKS.md를 읽고 실행 가능한 태스크를 두세 건 고릅니다. 정형적인 정보 수집은 경량 모델 서브에이전트에, 구현이나 실험은 표준 모델 스포크 세션에, 난도 높은 설계 판단은 고정밀 모델 서브에이전트에 각각 나눠 배정합니다.
TASKS.md 실제 예시
# TASKS.md (스포크가 관리하는 예)
## Doing
- [ ] [builder] 특징량 X 추가와 CV 재측정
## Backlog
- [ ] [scout] 외부 데이터셋 조사
## Blocked (사람의 판단 대기)
- [ ] 프로덕션 환경 반영 (승인 필요)
## Done
- [x] 베이스라인 모델 재현
낮 동안 각 스포크가 tmux 세션 위에서 자율적으로 작업을 진행합니다. 허브는 이따금 각 스포크의 최근 출력을 들여다보며(실제 대화 로그 전부가 아니라, 끝부분 수십 줄만) 막히지 않았는지, 옆길로 새지 않았는지 확인하는 정도로만 관여합니다.
위험도 높은 조작(가령 신뢰할 수 없는 코드 실행이나, 권한을 크게 푼 상태에서의 시행착오)이 필요한 태스크는, 미리 Docker 컨테이너에 격리된 스포크로 돌립니다. 이 컨테이너는 프로젝트 디렉터리만 마운트해 두었기에, 호스트의 다른 부분에는 손대지 못합니다.
밤, 외출한 곳에서 스마트폰으로 SSH 접속해 tmux 세션에 어태치(attach)하고, 진행 중인 스포크의 상태를 직접 확인합니다. 무언가 판단이 필요한 알림이 와 있으면 그 자리에서 승인하거나, 다음 지시를 짧게 돌려줍니다. PC 앞에 없어도 체제는 멈추지 않습니다.
이렇게 쓰면 거창하게 들릴지 모르지만, 실제로 필요한 건 tmux와 Docker, 그리고 셸 스크립트 몇 개뿐입니다. 특별한 기반이나 클라우드 서비스는 필요 없습니다.
이 책의 지도
이후 장에서는 이 체제를 요소별로 분해해 설명해 나갑니다.
- 제2장에서는 허브&스포크라는 설계 사상 자체, 즉 왜 허브는 직접 작업하면 안 되는지, 컨텍스트를 "유한한 예산"으로 다룬다는 게 어떤 뜻인지, 권한 경계를 어떻게 3층으로 나누는지를 파고든다.
- 제3장에서는 이 체제의 심장부인 TASKS.md / JOURNAL.md의 구체적인 운영 포맷을 보여 준다.
- 제4장에서는 tmux로 여러 세션을 실제로 세우고 지시를 보내는 절차, 그리고 모델 계층을 어떻게 나눠 배정할지 판단 기준을 다룬다.
- 제5장에서는 Docker 샌드박스를 구체적으로 만드는 법과, 격리해도 여전히 남는 리스크에 대한 대처를 다룬다.
- 제6장에서는 모바일에서의 개입과 알림 설계, 사람의 승인 게이트를 긋는 법을 다룬다.
- 제7장에서는 실제로 이 체제에서 일어난 장애(샌드박스가 한꺼번에 죽은 사고 등)와 복구를, 숨기지 않고 공유한다.
- 제8장에서는 이 체제의 적용 범위, 즉 몇 개 프로젝트까지 굴릴 수 있는지, 맞지 않는 작업은 무엇인지를 솔직하게 검토한다.
다 읽을 무렵이면 "Claude Code를 여러 프로젝트에서 병행해 자율적으로 돌리는" 개념만이 아니라, 실제로 손으로 짤 수 있는 설계도가 손에 들어와 있을 겁니다. 다음 장부터 허브&스포크라는 사고방식의 속으로 들어가겠습니다.
제2장 — 설계 사상(허브&스포크란 무엇인가)
이 장에서 정할 것
앞 장에서 "1세션에 전부 욱여넣는 게 붕괴의 원인이다"라는 진단을 내렸습니다. 이 장에서는 그 처방전, 곧 허브&스포크라는 체제의 설계 사상을 구체적으로 다져 나갑니다.
다룰 논점은 네 가지입니다.
- 허브와 스포크의 역할 분담을 어떻게 선 그을까
- "허브는 직접 작업하지 않는다"는 규칙이, 왜 단순한 정신론이 아니라 실리로서 작동하는가
- 컨텍스트를 "유한한 예산"으로 다룬다는 게 어떤 뜻인가
- 사람의 승인이 필요한 조작을 어디에서 선 그을까 (권한 경계의 3층 설계)
전부 이어져 있으니, 순서대로 살펴보겠습니다.
허브와 스포크, 각자의 일
우선 역할을 명확히 나눕니다. 허브는 "감독", 스포크는 "실무"입니다.
허브의 일
- 각 프로젝트의 상태 파일(TASKS.md)을 읽고, 다음에 무엇을 시킬지 고른다.
- 스포크나 서브에이전트에 지시를 내린다.
- 성과물을 검수한다(전문을 읽어들이는 게 아니라, 요점만 확인).
- 사람에게 진척·블록 사항을 보고한다.
- 새 프로젝트 착수를 제안한다.
스포크의 일
- 담당 프로젝트의 코드를 쓰고, 실험을 돌리고, 원고를 쓴다.
- 담당 프로젝트 안에서 완결되는 조사·설계 판단을 스스로 한다.
- 작업 결과를 상태 파일에 써서 되돌린다.
경계선은 "그 일이 한 프로젝트 안에서 닫히는지, 여러 프로젝트를 가로지르는 판단인지"로 긋습니다. 구현이나 실험은 거의 확실하게 한 프로젝트 안에서 닫히니 스포크의 일이 됩니다. 반면 "이번 주 어느 프로젝트에 시간을 배분할지", "새 프로젝트를 시작해야 할지" 같은 판단은 여러 프로젝트를 가로지르니 허브의 일이 됩니다.
이 선 긋기를 흐릿하게 두면, 허브가 구현 세부까지 참견하기 시작해서, 결국 1세션으로 전부 하던 시절과 똑같은 상태로 되돌아갑니다. 반대로 스포크에게 "오늘 어느 프로젝트를 우선해야 하는지"까지 판단하게 하면, 스포크는 자기 프로젝트 바깥 정보를 갖고 있지 않으니 잘못된 판단을 하기 쉬워집니다.
"허브는 직접 작업하지 않는다"는 규칙이 먹히는 이유
이 규칙은 얼핏 갑갑해 보입니다. 눈앞에 버그가 있고 고치는 법도 아는데, 왜 허브가 직접 고치면 안 될까요?
이유는 세 가지입니다.
1. 컨텍스트 소비의 비대칭성
허브가 직접 한 프로젝트의 코드를 읽고, 고치고, 테스트를 돌리면, 그 프로젝트의 맥락(파일 구성, 변수명, 과거 설계 판단)이 허브의 컨텍스트에 계속 눌러앉습니다. 다음에 다른 프로젝트를 볼 때도 이 정보는 남은 채 계속 소비됩니다. 허브가 프로젝트 다섯 개를 볼 때마다 이걸 반복하면, 허브 세션은 금세 컨텍스트 상한에 다가가 버립니다.
스포크에게 구현을 맡기면 허브가 갖는 건 "진척은 어떤가"라는 요약 정보뿐이면 됩니다. 프로젝트 내용 자체는 스포크 쪽 컨텍스트에서 완결됩니다.
2. 판단의 질 저하를 막는다
허브가 구현 세부에 손대기 시작하면, 여러 프로젝트의 상황을 위에서 내려다본다는 본래의 일에 쏟을 주의가 줄어듭니다. "나무를 보고 숲을 못 보는" 상태가 되어, "지금 어느 프로젝트가 늦어지고 있는지", "어디에 사람 판단이 필요한지" 같은, 허브만이 할 수 있는 일이 소홀해집니다.
3. 복구 비용의 차이
스포크 세션이 망가져도 상태 파일만 최신이면 새 스포크를 세워 곧바로 재개할 수 있습니다(이 설계는 제3장에서 자세히 다룹니다). 그런데 허브가 구현 세부를 머릿속(컨텍스트)에만 갖고 있었다면, 허브 세션이 망가질 때 그 세부 정보가 통째로 사라집니다. 허브가 가진 정보는 되도록 파일로 흘려보내고, 휘발성 높은 "지금 대화의 기억"에 의존하지 않게 한다. 이게 일관된 설계 방침입니다.
이 규칙을 지키고 있는지는, 사실 "허브가 코드 에디터로 파일을 편집하고 있지 않은가"를 스스로 점검하면 금방 알 수 있습니다. 필자의 운용에서도 피곤할 때일수록 "내가 하는 게 빠르다"는 유혹에 져서, 허브가 코드에 손대기 일쑤였습니다. 그럴 때마다 컨텍스트가 빨리 소비되어, 그날 안에 허브 세션을 다시 만드는 처지가 된 적도 있습니다. 규칙을 어긴 대가는, 어긴 직후가 아니라 몇 시간 뒤에 "어라, 아까 그 얘기 기억해?"라는 형태로 찾아옵니다.
컨텍스트를 "예산"으로 설계한다
컨텍스트 윈도는 유한한데도, 무한한 것처럼 굴기 십상입니다. 효과가 있는 건, 이걸 처음부터 "다 쓰면 곤란한 유한 자원"으로 다루는 자세입니다.
구체적으로 하는 일은 다음 세 가지입니다.
- 허브는 요약만 읽는다: 각 프로젝트의 TASKS.md는 허브가 직접 읽지만, 로그 전문이나 코드 전체는 읽지 않는다. 세부 확인이 필요하면 그것 자체를 경량 서브에이전트(다음 장 이후에서 다룰 scout 상당)에 위임하고, 결과 요약만 받는다.
- 성과물은 첫머리만 검수한다: 스포크나 서브에이전트에게서 돌아온 보고는, 전문을 읽어들이는 게 아니라 요점(결론·수치·다음 액션)을 확인하는 정도로만 둔다. 세부 타당성은 스포크 쪽 책임으로 돌린다.
- 오래 걸리는 작업은 백그라운드로 위임한다: 허브가 대기하는 동안에도 컨텍스트를 소비하니, 시간이 걸리는 태스크는 백그라운드로 진행시키고 허브는 다른 프로젝트 감독으로 돌아간다.
이 "예산 감각"을 가지면, 자연스레 "이 태스크는 정말 허브가 읽어야 하나"를 매번 자문하게 됩니다. 안 읽어도 되는 건 안 읽는다. 이건 대충 하는 게 아니라, 허브라는 역할의 생존 전략입니다.
모델도 컨텍스트와 마찬가지로 예산으로 다룬다
컨텍스트만이 아니라 모델 등급(경량/표준/고정밀)도 비용과 응답 품질의 트레이드오프를 지닌 예산으로 다룹니다. 정형적인 정보 수집에 고정밀 모델을 쓰는 건 예산 배분으로는 비효율입니다. "헷갈리면 가벼운 쪽부터 써 보고, 품질이 모자라면 올린다"는 원칙을 채택하고 있습니다.
이 판단 기준의 구체적인 배정 예시는 제4장에서 다룹니다.
권한 경계의 3층 설계
체제가 아무리 자율적으로 돌아가더라도, 외부에 영향을 주는 조작은 사람의 승인을 거쳐야 합니다. 여기가 흐릿하면 자율성의 혜택보다 사고 리스크 쪽이 커져 버립니다. 필자는 조작을 3층으로 나눕니다.
- 제1층: 자유롭게 실행해도 됨
- 로컬 파일 편집, 실험, 테스트 실행
- 리포지토리 안에서의 git commit (push는 포함하지 않음)
- 공개된 정보 조사
- 제2층: 사람의 확인이 필수
- git push, 외부 리포지토리 반영
- 대회·플랫폼 제출 행위
- 과금이 발생하는 조작 (유료 플랜 계약, 결제 설정)
- 외부 서비스 신규 등록·전송
- 제3층: 절대 금지 (자율성 이야기와는 다른 차원에서, 항상 금지)
- 기밀 정보·실데이터를 다루는 프로젝트에서의 비공개 정보 사용
- 스팸성 양산 행위, 약관 위반, 스텔스 마케팅
- 투기적 자동 매매
이 3층의 핵심은, 선 긋기 기준이 "되돌릴 수 있느냐 없느냐"라는 점입니다. 제1층은 다시 할 수 있습니다(파일은 원래대로 되돌릴 수 있고, 테스트는 몇 번이든 실행할 수 있습니다). 제2층은 한번 실행하면 외부에 영향이 미쳐 쉽게 취소할 수 없습니다. 제3층은 애초에 실행해선 안 되는 성질의 조작이라, 자율성 이야기와는 별개 차원의 금지 사항으로 다룹니다.
실무에서는 제2층에 해당하는 조작을 "실행 직전까지 준비하고, 실행 자체는 사람에게 맡기는" 형태로 만들어 두었습니다. 예를 들어 원고 집필·정리·리포지토리 커밋까지는 전부 에이전트가 하고, 외부 플랫폼에 "공개" 버튼을 누르는 조작만 사람에게 되돌립니다. 이렇게 하면 사람의 부담은 "최종 승인"만으로 압축됩니다.
이 경계선이 느슨해진 실패
솔직히 적자면, 이 경계선을 처음부터 깔끔하게 그었던 건 아닙니다. 운용 초기에 "잠깐 확인하는" 셈 치고 가벼운 판단까지 자동 승인 대상에 넣어 버렸는데, 나중에 다시 보니 본래 제2층에 둬야 할 조작(외부로 정보를 보내는 것)이 제1층 쪽에 섞여 들어가 있었습니다. 다행히 실제 피해가 나기 전에 알아챘지만, 이런 "어쩌다 느슨해진" 판단은 나중에 고치는 비용이 더 비싸게 먹힙니다. 경계선은 처음에 빡빡하게 긋고 필요에 따라 푸는 편이 안전하게 넘어갑니다. 이게 얻은 교훈입니다. 이 실패의 자세한 내용과, 그 밖에 일어난 운용상의 트러블은 제7장에서 한데 모아 다룹니다.
다음 장으로
이 장에서 정한 설계 사상(허브는 작업하지 않는다, 컨텍스트는 예산, 권한은 3층)을 실제로 "돌아가는 것"으로 만들려면, 상태를 어디에 어떻게 기록할지가 열쇠가 됩니다. 다음 장에서는 이 체제의 심장부인 TASKS.md와 JOURNAL.md의 구체적인 포맷과 운영 규칙을 살펴보겠습니다.
제3장 — 상태를 파일에 떨구다(TASKS.md / JOURNAL.md 운영법)
이 장의 주제
앞 장에서 "허브는 직접 작업하지 않는다", "컨텍스트는 예산"이라는 설계 사상을 다졌습니다. 이 사상을 실제로 돌리려면 상태를 어디에 둘지, 이 한 가지가 열쇠가 됩니다. 답은 단순합니다. 대화의 기억이 아니라 파일에 둡니다. 이 장에서는 그 구체적인 포맷과 운영 규칙을 설명합니다.
왜 채팅 이력에 상태를 갖게 하면 안 되는가
소박하게 생각하면, Claude Code 세션은 대화 안에서 태스크 진척을 기억할 수 있어 보입니다. 실제로 짧은 시간·단일 프로젝트 작업이라면 그걸로 문제없습니다. 하지만 여러 프로젝트를 장기간 굴리는 체제에서는, 세 가지 이유로 성립하지 않게 됩니다.
1. 컨텍스트는 유한하고, 게다가 한 방향으로만 나빠진다
제1장에서 짚은 대로, 대화가 길어질수록 오래된 정보는 밀려납니다. "지난주에 한 태스크의 후속"을 오늘 세션에 물어봐도, 그 세션이 지난주 세션과 동일하다는 보장이 없고, 동일하더라도 그사이 끼어든 대량의 오고 감으로 기억은 옅어져 있습니다.
2. 세션은 죽는다
샌드박스 컨테이너가 업데이트로 죽고, 인증이 끊기고, 네트워크가 불안정해진다. 이유는 여러 가지지만, 세션은 언젠가 반드시 끊깁니다. 대화 안에만 상태가 있으면, 세션이 죽는 순간 그때까지의 진척 파악이 원점에서 다시 시작됩니다.
3. 사람이 확인할 때의 전제가 무너진다
사람(필자)이 진척을 확인하고 싶을 때, 일일이 긴 대화 로그를 거슬러 올라가 "지금 어떤 상태였는지"를 재구성하는 건 비용이 너무 큽니다. 상태가 파일에 정리돼 있으면, 그 파일을 여는 것만으로 현황을 알 수 있습니다. 이건 허브가 확인할 때도 마찬가지라, 제2장에서 말한 "허브는 요약만 읽는다"는 원칙을 성립시키는 전제 그 자체입니다.
그래서 세션의 기억은 휘발성 캐시로, 파일이야말로 정본(source of truth)으로 자리매김합니다. 대화 안에서 정해진 건, 그 자리에서 파일에 써서 되돌리기 전까지는 "아직 확정되지 않은 것"으로 다룹니다.
태스크 관리 파일: TASKS.md
각 프로젝트 루트에 TASKS.md를 두고, 다음 네 섹션으로 관리합니다.
# TASKS: 프로젝트 이름
## Doing
- [ ] [builder] 특징량 X 추가와 CV 재측정
## Backlog (우선순위 순)
- [ ] [builder] 전처리 파이프라인 리팩터
- [ ] [scout] 외부 데이터셋 조사
- [ ] [deep] 새 평가 지표 설계 검토
## Blocked (사람의 입력·판단 대기)
- [ ] 프로덕션 환경 반영 (승인 필요)
- [ ] 외부 API 키 발급 (사용자 쪽 조작 대기)
## Done
- [x] 베이스라인 모델 재현 (2026-07-10)
- [x] 데이터 전처리 기본 파이프라인 구축 (2026-07-11)
각 섹션의 역할은 다음과 같습니다.
- Doing: 지금 막 누군가(스포크나 서브에이전트)가 손을 움직이고 있는 항목. 동시에 돌리는 건 기본 1~2건으로 좁힌다. 여기가 부풀면 "병행해서 하는 셈"인데 실제로는 아무것도 안 끝나는 상태에 빠진다.
- Backlog: 다음에 해야 할 것의 우선순위 붙은 목록. 앞머리에 태그([scout] [builder] [deep])를 붙여, 어느 모델 계층에 위임해야 할지 한눈에 알 수 있게 한다. 이 태그 붙이는 기준은 제4장에서 다룬다.
- Blocked: 에이전트 쪽에서는 더 나아갈 수 없고, 사람의 입력·승인·외부 조작이 필요한 항목. 여기 놓인 항목은 방치되지 않도록 정기적으로 허브가 사람에게 알리는 대상이 된다.
- Done: 완료한 항목. 날짜를 붙여 남긴다. 단순한 기록이 아니라, 나중에 "언제 무엇을 했는지"를 거슬러 볼 때의 색인 역할도 한다.
태스크를 Backlog에서 Doing으로 옮길 때도, Doing에서 Done으로 옮길 때도, 그 이동 자체를 에이전트가 하고 파일을 고쳐 씁니다. 사람이 손으로 이 파일을 편집하는 건 Blocked 항목을 해소했을 때(예: 승인했다, 계좌를 설정했다) 정도로 끝나도록 설계했습니다.
작업 로그: JOURNAL.md
TASKS.md가 "지금 무엇이 어디에 있는가"의 스냅숏이라면, JOURNAL.md는 "무슨 일이 있어 왔는가"의 시계열 로그입니다. 한 엔트리의 기본 포맷은 다음 네 가지 세트로 합니다.
## 2026-07-14
- **한 일**: 특징량 X를 추가하고, 5-fold CV로 재평가했다
- **결과**: CV 스코어가 0.812 → 0.828로 개선(베이스라인 대비 +0.016)
- **알아챈 점**: 특징량 X는 결측률이 높은 카테고리에서 효과가 약하다. 결측 보완
방법을 바꾸면 더 오를 여지가 있다
- **다음 한 수**: 결측 보완을 median → 모델 기반 보완으로 바꿔 재측정한다
핵심은 "결과"를 반드시 수치로 쓰는 것입니다. "잘됐다", "개선됐다"만으로는 나중에 다시 볼 때 무엇이 얼마나 좋아졌는지 판단할 수 없습니다. 수치화할 수 없는 작업(조사나 집필)이라도 "몇 글자 썼는지", "몇 건 조사했는지"처럼 정량화할 수 있는 부분은 남깁니다.
또 하나의 핵심은 "다음 한 수"를 반드시 붙이는 것입니다. 이게 있으면 세션이 끊겨 다른 세션(혹은 며칠 뒤의 자신)이 이 로그를 읽었을 때, 헤매지 않고 작업을 재개할 수 있습니다. 반대로 "다음 한 수"를 안 쓰고 끝난 로그는, 재개할 때 "이건 이미 완료된 건가, 도중에 멈춘 건가"를 판단하는 비용을 발생시킵니다.
끊기 좋은 지점(태스크 1건이 완료됐다, 실험 1개가 끝났다)마다 로그를 한 엔트리 추가하고, 대응하는 git commit을 하나 만듭니다. 이렇게 하면 로그의 각 엔트리와 리포지토리의 커밋 이력이 대응해서, "이 로그 시점의 코드는 이것"이라는 참조가 쉬워집니다.
세션이 망가져도 곧바로 재개할 수 있는 설계
이 두 파일의 진짜 가치는, 평소에 잘 돌아갈 때보다 무언가가 망가졌을 때 발휘됩니다.
실제로 일어난 예를 일반화해 적으면, 한번은 쓰고 있던 컨테이너 관리 도구의 자동 업데이트로 가동 중이던 샌드박스 컨테이너가 죄다 죽은 적이 있습니다(이 사고의 자세한 내용은 제7장에서 다룹니다). 이때 만약 진척이 대화의 기억에만 있었다면, 여러 프로젝트분의 상황을 사람이 처음부터 떠올려 복원해야 했을 겁니다. 하지만 실제로는 각 프로젝트의 TASKS.md를 보면 Doing에 무엇이 있었는지, Blocked에 무엇이 남았는지 곧바로 알 수 있고, JOURNAL.md의 최근 엔트리를 보면 "다음 한 수"까지 적혀 있었기에, 새 세션을 세워 해당 태스크를 넘기는 것만으로 작업을 재개할 수 있었습니다.
이 "곧바로 재개할 수 있는" 성질을 지키기 위해, 다음 세 가지를 운영 규칙으로 철저히 지킵니다.
- 상태 변경은 반드시 그 자리에서 파일에 쓴다: "나중에 몰아서 쓰자"를 허용하면, 쓰기 전에 세션이 망가진 경우 정보가 사라진다.
- 끊기마다 커밋한다: 파일 변경을 쌓아 두지 말고, 의미 있는 단위마다 커밋한다. 이러면 파일의 변경 이력 자체도 복구의 실마리가 된다.
- 허브는 정기적으로 파일의 신선도를 확인한다: 스포크가 오래 아무것도 쓰지 않은 경우, 단지 순조로워서 보고가 필요 없는 건지, 막힌 건지를 허브가 가볍게 들여다보고 확인한다(이 방식은 제4장의 proj peek 상당 구조에서 다룬다).
이 설계의 한계
솔직히 적어 두면, 이 방식은 만능이 아닙니다. 파일에 써 내는 데에도 당연히 비용이 들고, 쓰는 걸 잊으면 의미가 없습니다. 또 파일의 입자가 너무 잘면 갱신 품이 늘고, 너무 굵으면 복구 때 정보가 모자랍니다. 필자의 운용에서는 "태스크 1건 완료", "실험 1개 종료"를 기본 단위로 삼고 있지만, 이건 프로젝트 성질에 따라 조정이 필요한 부분입니다. 완벽한 입자가 따로 있는 게 아니라, 운용하며 조정해 나가는 것이라고 봅니다.
다음 장으로
상태를 파일에 떨구는 설계가 갖춰졌으니, 다음은 그 상태를 실제로 돌리는 실행 환경 이야기로 넘어갑니다. 제4장에서는 tmux 위에 여러 Claude Code 세션을 띄우고, 허브에서 스포크로 지시를 보내고, 모델 계층을 태스크 성질에 따라 나눠 배정하는 구체적인 절차를 다룹니다.
제4장 — 스포크를 기동한다(tmux × 다중 세션 실무)
이 장에서 다룰 것
제3장에서 "상태는 파일에 둔다"는 설계를 다졌습니다. 이 장에서는 그 상태를 실제로 돌리는 실행 환경, 곧 tmux 위에서 여러 Claude Code 세션(스포크)을 병행 가동하고, 허브에서 지시를 내리고, 모델 계층을 나눠 쓰는 실무 이야기로 넘어갑니다.
tmux로 여러 세션을 병행 가동한다
스포크를 tmux 위에 두는 이유는 단순합니다. tmux 세션은 SSH 접속이나 터미널이 끊겨도 계속 살아 있습니다. 허브 쪽 세션이 재시작되어도 tmux 위의 스포크는 그대로 계속 도니, 감독 쪽 사정으로 스포크를 말려들게 할 필요가 없습니다.
명명 규칙은 proj-<프로젝트명>으로 고정합니다. 대회 참전이면 proj-competition, 연구 주제면 proj-research, 수익화 프로젝트면 proj-income처럼, 프로젝트 디렉터리 이름과 tmux 세션 이름을 일치시킵니다. 이름이 어긋나면 "어느 세션이 어느 프로젝트인지" 확인하는 비용이 발생하니, 여기는 철저히 맞춰 둡니다.
이 기동·지시·정지를 매번 손으로 tmux 커맨드를 조립하는 건 번거로우니, proj라는 작은 래퍼 스크립트로 고정해 두었습니다. 실체는 다음 같은 구조의 셸 스크립트입니다(요점만 발췌).
#!/usr/bin/env bash
# proj launch <name> [--model sonnet|haiku|opus] [--docker]
case "$cmd" in
launch)
dir="$PROJECTS_DIR/$name"
sess="proj-$name"
if [ "$use_docker" = 1 ]; then
tmux new-session -d -s "$sess" -c "$dir" \
"docker run -it --rm --name agent-$name \
-v '$dir':/work -w /work -e IS_SANDBOX=1 agent-base:latest \
claude --model '$model' --dangerously-skip-permissions; exec bash"
else
tmux new-session -d -s "$sess" -c "$dir" "claude --model '$model'; exec bash"
fi
;;
send) tmux send-keys -t "proj-$name" "$*"; sleep 0.3; tmux send-keys -t "proj-$name" Enter ;;
peek) tmux capture-pane -pt "proj-$name" -S "-${lines:-30}" ;;
status) tmux ls | grep '^proj-' ;;
stop) tmux kill-session -t "proj-$name"; docker rm -f "agent-$name" ;;
esac
컨테이너를 쓸지 말지는 --docker의 유무로 전환합니다(컨테이너 설계는 제5장에서 자세히 다룹니다). GPU를 쓰지 않는 구현·집필 계열 프로젝트는 기본으로 컨테이너 운용, 호스트 GPU가 필요한 작업은 비컨테이너로 돌린다, 이렇게 나눠 씁니다.
허브에서 스포크로 지시 내리기·상태 보기·정지
이 래퍼가 있으면 허브(감독역 세션)는 다음처럼 운용할 수 있습니다.
$ proj launch income --model sonnet --docker
launched proj-income (model=sonnet docker=1) attach: tmux attach -t proj-income
$ proj send income "TASKS.md의 Backlog 맨 앞 태스크를 진행해 줘. 끝나면 JOURNAL.md에 추가하고 commit해 줘"
sent to proj-income
$ proj peek income 20
(최근 20줄의 화면 출력이 여기에 표시된다)
$ proj status
proj-income: 1 windows (created ...)
proj-research: 1 windows (created ...)
$ proj stop income
stopped proj-income
핵심은 peek의 사용법입니다. tmux attach로 직접 달라붙으면 그 세션에 집중하게 되어 다른 프로젝트 감독이 소홀해집니다. peek은 화면의 최근 수십 줄을 가져올 뿐이라, 허브 쪽 세션에 달라붙을 필요가 없습니다. 제2장의 "허브는 요약만 읽는다"는 원칙을 tmux 운용 수준에서 구체화한 구조가 바로 이것입니다.
실무에서는 send로 지시를 내린 뒤 곧바로 peek하지는 않습니다. 오래 걸리는 태스크(실험 1개, 원고 한 장 분량 집필 등)는 수십 분 단위로 내버려 두고, 다음 순회 타이밍에 몰아서 peek합니다. 여기서 자주 들여다보러 가면, 들여다보는 행위 자체가 허브의 컨텍스트를 소비해 본말전도가 됩니다.
표준 "Agent Teams" 기능과의 관계
Claude Code에는 2026년 초에, 여러 에이전트를 병렬로 띄워 tmux 페인에 배정하는 "Agent Teams"(teammate)라는 구조가 실험적 기능으로 추가되었습니다.
이 책 집필 시점에서는 플래그 이름이나 동작이 바뀔 수 있는 실험적 위치입니다. 최신 세부는 공식 문서를 확인하세요.
[보완 — 추가된 내용] Agent Teams는 2026년 초 Opus 4.6 세대와 함께 실험적 기능으로 등장했으며, 집필 시점 기준으로는 환경 변수
CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS=1을 설정해야 활성화됩니다. 팀 리드 세션이 teammate 세션들을 만들고, 이들이 공유 태스크 목록을 함께 읽고 서로 직접 메시지를 주고받는다는 점이 서브에이전트와 다릅니다(서브에이전트는 메인 에이전트에게 결과만 보고할 뿐 서로 대화하지 않습니다). 공식 문서는https://code.claude.com/docs/en/agent-teams에서 확인할 수 있습니다. 플래그 이름과 동작은 이후 변경될 수 있으니, 실제 적용 전에 최신 문서를 확인하시기 바랍니다.
"tmux 위에서 여러 세션을 늘어놓고 돌린다"는 겉모습 부분은, 이 표준 기능과 이 장의 proj 스크립트가 겹칩니다. 당연히 "왜 기성 기능을 쓰지 않고 자작한 얇은 래퍼를 쓰는가"라는 의문이 나올 테니, 여기서 입장을 분명히 해 두겠습니다.
표준 Agent Teams는 기본적으로 하나의 목표를 향해 여러 에이전트를 협업시키는, 말하자면 "하나의 프로젝트 안에서 팀을 짜는" 걸 상정한 만듦새입니다. 반면 이 책의 관심은, 독립된 여러 프로젝트를 몇 달씩 따로따로 계속 돌리며 각각을 성기게 감독하는 데 있습니다. 이 용도에서는 각 스포크가 다음을 만족해야 합니다.
- 프로젝트마다 독립된 작업 디렉터리와 상태 파일(TASKS.md / JOURNAL.md)을 갖고, 다른 프로젝트의 목표와 섞이지 않는다.
- 감독역(허브) 세션이 재시작되어도, 스포크 쪽은 말려들지 않고 계속 돈다.
- 위험도 높은 프로젝트만 Docker 컨테이너에 격리하고(제5장), 프로젝트 단위로 모델 계층이나 컨테이너 유무를 전환할 수 있다.
proj launch <name> --model ... --docker처럼 프로젝트 단위로 기동 조건을 세밀하게 지정하고 싶다는 이 요건에는, 맨몸의 병렬 기동 기능보다 한 단계 앞의, 얇은 자작 래퍼 쪽이 다루기 쉬운 게 실정입니다. 표준 기능이 성숙해 이 입자 수준의 제어를 흡수할 수 있게 되면 갈아탈 가치가 있지만, 현시점에서는 "병렬로 보인다"는 것과 "여러 프로젝트를 따로따로 장기 운용한다"는 건 별개 문제라고 봅니다. 표준 기능은 단일 프로젝트 안의 병렬 작업에, 이 장의 체제는 여러 프로젝트의 장기 감독에, 이렇게 나눠 쓰는 게 현실적입니다.
모델 계층을 태스크 성질에 따라 나눠 배정한다
스포크 안에서 실제로 태스크를 처리하는 건 서브에이전트인데, 이걸 경량(haiku)·표준(sonnet)·고정밀(opus)의 3계층으로 나눕니다. 판단 기준은 다음과 같습니다.
| 계층 | 모델 | 용도 예 |
|---|---|---|
| scout | haiku | 정보 수집, 파일 다운로드, 환경 셋업, 로그·문서 요약, 정형적인 상태 체크 |
| builder | sonnet | 구현, 실험 실행, EDA, 테스트 작성·실행, 리팩터, TASKS.md/JOURNAL.md 갱신 |
| deep-thinker | opus | 연구 아이디어 발상, 기법 설계, 수리적 검토, 실험 계획, 난해한 버그의 근본 원인 분석 |
[역주 — 참고] 본문의
haiku/sonnet/opus는 Claude Code CLI에서 각 등급의 최신 모델을 가리키는 별칭(alias)입니다. 별칭은 그대로 유지되며 내부적으로 최신 버전에 매핑되므로, 예컨대 이 번역 시점 기준으로는haiku가 Claude Haiku 4.5,sonnet이 Claude Sonnet 5,opus가 Claude Opus 4.8에 해당합니다. 원문이 별칭으로 쓴 부분은 그대로 두었으니, 실제 사용 시점의 매핑은 공식 문서에서 확인하시기 바랍니다.
구체적인 예로 보면 이해하기 쉽습니다. "대회 규약을 확인해서 해법 글 공개가 가능한지 조사한다"는 태스크는, 공식 페이지를 읽고 해당 조항을 주워 오는 정형 작업일 뿐이니 scout(haiku)에 던집니다. 몇 분 만에 Yes/No와 근거 조항이 돌아오면 충분하고, 여기에 고정밀 모델을 쓸 이유가 없습니다. 반면 "특징량 엔지니어링을 추가해 CV 스코어를 재측정한다"는 코드를 쓰고 실행하고 결과를 해석해야 하니 builder(sonnet)의 일이 됩니다. "지금 모델의 CV 스코어가 정체된 원인을 검토하고, 다음에 시도할 기법의 방향을 여럿 제안한다"는 단순 구현 작업이 아니라 설계 판단 그 자체라, deep-thinker(opus)에 위임합니다.
TASKS.md의 Backlog에 [scout] [builder] [deep] 태그를 붙이는 건(제3장 참조), 이 배정을 미리 정해 두어 실행할 때 헤매지 않기 위함입니다.
"헷갈리면 싼 쪽부터 써 본다"의 비용 대비 효과
판단 기준표가 있어도, 실제로는 경계선상의 태스크가 나옵니다. 예를 들어 "원고 한 단락을 읽고 문체가 통일돼 있는지 확인한다"는 정형 체크 작업처럼 보여 scout에 던지고 싶어지지만, 문체 일관성 판단은 의외로 맥락 이해를 요구합니다. 필자는 처음 이걸 scout(haiku)에 맡기고 "문제없음"이라는 보고를 받아 앞으로 진행했는데, 나중에 builder가 실제로 전문을 처음부터 끝까지 읽었을 때 장마다 "~습니다체"와 "~다체"가 섞여 있는 걸 발견했습니다. scout의 보고가 거짓말은 아니었지만, 넘겨받은 범위(한 단락 단위)에서만 봤기 때문에, 장을 가로지르는 일관성의 어긋남을 검출하지 못한 것입니다.
이 일에서 얻은 교훈은, "가벼운 모델에 던져 실패했으면, 실패 원인이 모델의 능력 부족인지 태스크 설계 실수(넘긴 범위가 너무 좁았다 등)인지를 구별한 다음에 상위 모델로 바꾼다"는 것입니다. 단순히 opus에 전부 다시 던지는 게 아니라, 우선 넘기는 컨텍스트 범위를 넓혀 builder에 다시 확인시키고, 그래도 판단이 갈리면 deep-thinker로 올린다, 이런 단계를 밟습니다. 결과적으로 이 경우는 builder 재위임(전문을 한꺼번에 읽힘)으로 해결됐고, opus까지 올릴 필요는 없었습니다.
"헷갈리면 싼 쪽부터 써 본다"는 원칙은, 실패를 두려워 말고 저비용부터 시작하는 것과, 실패한 이유를 대충 모델 탓으로 돌리지 않는 것, 이 둘이 있어야 비로소 작동합니다.
다음 장으로
스포크를 기동·운용하는 절차가 다져졌으니, 다음에 문제가 되는 건 "그 안에서 어디까지 강한 권한을 에이전트에게 줄 것인가"입니다. 다음 장에서는 --dangerously-skip-permissions 같은 강한 권한을 왜 호스트에서 직접 쓰면 안 되는지, 그리고 Docker 샌드박스로 어떻게 격리하는지를 구체적으로 살펴보겠습니다.
제5장 — 위험한 작업을 격리한다(Docker 샌드박스 설계)
이 장에서 다룰 것
앞 장에서 스포크를 tmux 위에 세우는 절차를 다졌습니다. 여기서 피해 갈 수 없는 게 권한 이야기입니다. 스포크를 완전히 내버려 두고 자율 가동시키려면, Claude Code의 --dangerously-skip-permissions에 해당하는 "확인 없이 실행해도 됨" 설정이 필요해집니다. 하지만 이걸 호스트 셸에서 그대로 쓰는 건 너무 위험합니다. 이 장에서는 왜 위험한지, 어떻게 격리하는지, 격리해도 여전히 남는 리스크는 무엇인지를 차례로 살펴봅니다.
왜 강한 권한을 호스트에서 직접 쓰면 안 되는가
--dangerously-skip-permissions는 이름 그대로, 파일 삭제·임의 커맨드 실행·외부 통신을 포함한 거의 모든 조작을 확인 없이 실행하게 하는 플래그입니다. 스포크를 완전 방치로 굴리려면 이런 종류의 설정이 사실상 필수가 됩니다(일일이 확인이 끼어들면 무인 가동의 의미가 옅어집니다).
--dangerously-skip-permissions를 호스트 홈 디렉터리에서 직접 쓰면, 영향 범위가 그 프로젝트 디렉터리에 머물지 않습니다. 에이전트가 만에 하나 지시를 잘못 읽거나, 할루시네이션으로 엉뚱한 경로를 겨냥해 커맨드를 조립한 경우,~/.ssh의 키, 다른 프로젝트 디렉터리, 셸 설정 파일처럼 본래 손대선 안 될 곳까지 도달할 수 있습니다. "그런 오작동은 좀처럼 안 일어난다"는 전제에 거는 게 아니라, 애초에 도달할 수 있는 범위를 물리적으로 제한하는 편이 안전하게 넘어갑니다.
WSL2 환경에서는 중첩된 VM(경량 VM에 의한 격리)이 무겁고 실용적이지 않은 반면, Docker는 이미 가동 중인 환경이 많습니다. 그래서 채택한 게, 프로젝트 디렉터리만 마운트한 일회용 컨테이너입니다.
일회용 컨테이너 만드는 법
우선 이미지 쪽은, Claude Code·codex CLI·Python 툴체인처럼 스포크가 작업에 쓰는 도구 한 벌을 넣어 두기만 한 단순한 것으로 합니다.
# agent-base: 스포크 에이전트용 샌드박스 이미지
FROM node:22-bookworm
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates python3 python3-venv python3-pip \
jq less procps build-essential tmux \
&& rm -rf /var/lib/apt/lists/*
RUN npm install -g @anthropic-ai/claude-code @openai/codex
RUN curl -LsSf https://astral.sh/uv/install.sh | env UV_INSTALL_DIR=/usr/local/bin sh
ENV IS_SANDBOX=1
WORKDIR /work
CMD ["bash"]
기동할 때는 이 이미지에 프로젝트 디렉터리"만" 마운트합니다.
docker run -it --rm --name agent-income \
-v "$HOME/projects/income":/work \
-v "$HOME/.agent-homes/income/claude":/root/.claude \
-w /work -e IS_SANDBOX=1 \
agent-base:latest \
claude --model sonnet --dangerously-skip-permissions
여기서의 핵심은 세 가지입니다.
- 마운트는 대상 프로젝트 디렉터리 하나만:
-v로 다른 프로젝트나 홈 디렉터리 전체를 넘기지 않는다. 컨테이너 안에서 보이는 파일 시스템은/work아래(=그 프로젝트)로 한정된다. --rm으로 일회용으로 만든다: 컨테이너는 정지와 동시에 파기된다. 컨테이너 안에서 무슨 일이 일어나든, 다음에 띄울 때는 깨끗한 상태에서 시작된다.- 인증 정보는 최소한만 복사한다: 홈 디렉터리 전체를 넘기는 게 아니라,
~/.agent-homes/<name>/claude/같은 전용 디렉터리에 인증 정보만 미리 복사하고 그것만 마운트한다. 온보딩 상태 등도 필요한 키(oauthAccount 등)만 추출한 최소한의 JSON을 만들어 넘기고, 이력이나 그 밖의 설정은 넘기지 않는다.
참고로 위 이미지는 root 사용자로 돕니다. Claude Code는 --dangerously-skip-permissions를 root 권한으로 쓰려고 하면, 본래 "root 권한에서는 위험"으로 기동을 거부합니다. Dockerfile과 기동 커맨드에서 IS_SANDBOX=1을 넘기는 건, 격리된 환경임을 알려 이 체크를 통과시키기 위함입니다. 더 보수적으로 하려면, 컨테이너 안에 root가 아닌 작업 사용자를 만들어 그쪽에서 돌리는 방법(공식 devcontainer가 취하는 방식)도 있습니다. 이 책은 컨테이너 전체를 일회용으로 하고, 마운트도 프로젝트 디렉터리만으로 좁힌 전제에서 이 간편한 방법을 택했습니다.
이
IS_SANDBOX=1에 의한 우회는 집필 시점에서는 비공식 뒷문이며, 앞으로 동작이 바뀔 수 있는 점은 유의하세요.
[역주 — 참고] 공식 문서에서는 이 동작을 "인식된 샌드박스 환경(recognized sandbox environment)을 감지하면 root 체크를 건너뛴다"라고 설명하며, 정식으로 권장하는 방식은 비root 사용자를 쓰는 것입니다(공식 devcontainer가 이 방식입니다). 또한 공식 문서는 dev 컨테이너 안에서
--dangerously-skip-permissions를 쓰더라도 컨테이너 안에 있는 자격 증명(~/.claude)이 새어 나갈 위험은 막지 못하니, 신뢰할 수 있는 리포지토리에서만 쓰고~/.ssh같은 호스트 비밀 정보는 마운트하지 말라고 권고합니다. 본문의 인증 정보 최소화·토큰 스코프 축소 방침은 이 권고와 방향이 같습니다.
이 설계 덕에, 컨테이너 안에서 --dangerously-skip-permissions를 써도 피해 범위는 "그 프로젝트의 파일과 복사된 최소한의 인증 정보"로 한정됩니다. 호스트 파일 시스템에는 물리적으로 손대지 못합니다.
GPU 패스스루를 못 쓰는 환경에서의 분업
이상을 말하자면, 컨테이너 안에서 전부 완결하고 싶은 게 맞습니다. 하지만 필자의 환경(WSL2 + Docker)에서는 nvidia-container-toolkit 설정이 갖춰지지 않아, 컨테이너에서 GPU가 보이지 않는 상태입니다. 고치려면 sudo nvidia-ctk runtime configure --runtime=docker 실행과 Docker 데몬 재시작이 필요한데, 이건 호스트 쪽 sudo 조작을 수반하기에 사용자 본인의 대응 대기 상태입니다.
이 제약이 풀릴 때까지의 잠정 운용으로, 다음 같은 분업 설계를 하고 있습니다.
- GPU가 필요한 실행은 허브가 호스트 쪽에서 한다: 모델 학습·추론처럼 GPU를 요하는 처리는, 컨테이너를 거치지 않고 호스트에서 직접(감독역 세션에서 서브에이전트를 세워) 실행한다.
- 스포크는 컨테이너 안에서 CPU 작업을 담당한다: 구현, 데이터 전처리, 설정 파일 준비, 단위 테스트 작성·실행처럼 GPU가 필요 없는 작업은 컨테이너 안의 스포크가 한다.
- 다리 놓기는 experiments/ 디렉터리에서 한다: 스포크는 학습 스크립트와 실행 절차(커맨드, 예상 VRAM 사용량, 설정 파일)를
experiments/에 준비하고, TASKS.md에 "GPU 실행 대기" 항목으로 기록한다. 허브 쪽은 이걸 보고 호스트에서 실행한 뒤, 결과를 로그에 써서 되돌린다.
이건 이상적인 분업이라고는 못 합니다. GPU 실행 때마다 허브의 컨텍스트를 일부 소비하고, 컨테이너 격리라는 이점이 GPU 작업에는 미치지 않습니다. 다만 현 제약하에서는, 컨테이너 격리를 포기하기보다 GPU가 필요한 부분만 호스트에 남기는 편이 현실적인 타협점이라고 판단합니다. VRAM 12GB라는 상한도 감안해, 학습 잡 설계 자체를 이 제약 안에 들어오도록 처음부터 작게 짜는 방침을 철저히 지킵니다.
샌드박스화해도 여전히 남는 리스크와 대처
컨테이너로 격리해도 리스크가 0이 되는 건 아닙니다. 실제로 운용하며 보이게 된 잔존 리스크는 다음 두 가지입니다.
1. 복사한 인증 정보 자체가 유출 경로가 될 수 있다
컨테이너에 넘기는 인증 정보는 최소한으로 좁혀 두었지만, 그래도 "그 프로젝트용 인증 정보"는 컨테이너 안에 존재합니다. 컨테이너가 만에 하나 어떤 형태로든 예기치 못한 외부 통신을 한 경우, 이 인증 정보가 샐 가능성은 0이 아닙니다. 대처로, 권한 스코프가 넓은 토큰이 아니라 프로젝트 단위로 좁혀진 토큰을 쓰고 정기적으로 로테이션하는 운용을 철저히 지킵니다. 나아가 외부 통신이 필요 없는 태스크에서는 기동할 때 --network none을 붙여 컨테이너로부터의 바깥 방향 통신 자체를 차단하고, 통신이 필요한 경우도 목적지를 좁히는 식으로 네트워크 면의 조임을 병용하면, 이 유출 경로를 사실상 막을 수 있습니다. 마운트 범위 한정이 "읽을 수 있는 범위"를 좁히는 것이라면, 네트워크 한정은 "빼낼 수 있는 경로"를 좁히는 것이라는 보완 관계입니다.
2. 인증 정보의 신선도가 유지되지 않으면, 도리어 모르는 새 멈춘다
이건 위험이라기보다 운용상의 함정이지만, 호스트 쪽에서 인증 토큰을 갱신해도 ~/.agent-homes/<name>/ 쪽 복사본은 자동으로 갱신되지 않습니다. 실제로, 호스트 쪽 인증을 갱신한 뒤 스포크를 재시작했더니, 컨테이너 안 인증 정보가 옛것 그대로여서 "로그인하지 않은" 상태가 되어, 그 스포크가 반나절 가까이 침묵한 채 방치돼 있던 적이 있습니다. 원인은 금방 판명됐지만, 이런 "격리돼 있는 탓에 호스트 쪽 변경이 전파되지 않는" 성질은, 격리 설계의 부작용으로 인식해 둘 필요가 있습니다. 이후로는, 스포크를 재시작하기 전에 반드시 최신 인증 정보를 agent-homes 쪽에 다시 복사한다는 절차를 체크리스트로 만들어 대처하고 있습니다.
컨테이너에 의한 격리는 "사고가 나도 피해 범위를 한정한다"는 것이지, "사고가 안 나게 한다"는 게 아닙니다. 이 전제를 갖고 있는 것이 과신을 막습니다.
다음 장으로
위험한 작업을 컨테이너로 격리할 수 있게 됐으니, 다음은 운용 면 이야기로 넘어갑니다. 24시간 뭔가 스포크가 돌고 있는 상태에서, 사람은 어디서 어떻게 체제에 개입해야 할까요? 다음 장에서는 알림 설계와 모바일에서의 승인 게이트를 긋는 법을 다룹니다.
제6장 — 모바일에서의 개입(상시 가동과 사람의 승인 게이트)
이 장에서 다룰 것
제4장·제5장에서 스포크의 기동·격리라는 "돌리는 법"을 다졌습니다. 이 체제의 노림수는, 사람이 PC 앞에 없어도 에이전트가 계속 작업을 진행하는 것입니다. 그렇다면 사람이 PC 앞에 없는 시간에 어떻게 체제에 관여할지를 설계하지 않으면, 모처럼의 자율성이 "알아채고 보니 뭔가가 계속 멈춰 있었다"는 형태로 헛되이 날아가 버립니다. 이 장에서는 알림 설계, 모바일에서의 개입 수단, 승인 게이트를 긋는 법, 그리고 잘못 지나치게 자동화한 실패담을 다룹니다.
알림 설계: 무슨 일이 생기면 사람에게 알려야 하는가
전제로, 모든 진척을 사람에게 알리는 건 악수입니다. 알림이 너무 많으면 사람은 그걸 무시하는 습관이 들어, 정말 중요한 알림까지 파묻힙니다. 반대로 알림이 너무 적으면 막힌 상태를 알아채는 게 늦어집니다. 필자가 실제로 채택한 알림 트리거는 다음 네 종류로 좁혔습니다.
- TASKS.md의 Blocked 섹션에 새 항목이 추가된 순간: 에이전트 쪽에서는 더 나아갈 수 없는 상태가 발생했다는 뜻이다. 방치하면 그만큼 전체 진행이 늦어진다.
- 권한 경계 제2층(사람 확인 필수)에 해당하는 조작 직전: git push, 대회·플랫폼 제출, 과금을 수반하는 조작 등이 실행 직전까지 준비된 상태.
- 비정상 종료·예상 밖 에러: 컨테이너가 죽었다, 인증 에러로 동작이 멈췄다, 테스트가 원인 불명으로 계속 실패한다 등.
- 끊기 좋은 성과물이 나왔을 때: 한 장 분량 원고가 다 써졌다, 실험 하나가 완료돼 수치가 나왔다 같은 "봐 줬으면 하는 타이밍".
반대로 알리지 않는 건 Doing·Backlog의 일상적 진척, 사소한 리팩터, scout 수준의 정형 조사 등입니다. 이것들은 TASKS.md/JOURNAL.md를 보면 나중에 언제든 좇을 수 있는 정보라, 실시간으로 사람에게 끼어들 가치가 없습니다.
알림 발송 자체는 푸시 알림 구조를 쓰고 있어, 리모트 접속이 유효한 시간대라면 스마트폰 쪽으로 그대로 도착합니다. 알림 문면은 "무슨 일이 일어났는가"만이 아니라 "지금 무엇을 하면 이 막힘이 풀리는가"를 한마디 덧붙이게 하고 있습니다.
알림 문면 실제 예시
"Blocked에 계정 설정 대기 항목이 추가되었습니다"만이 아니라 "Blocked에 계정 설정 대기 항목이 추가되었습니다. 설정 완료 후 TASKS.md의 해당 줄을 체크 완료로 바꾸면 다음 순회에서 처리를 재개합니다"까지 쓰면, 알림을 받은 쪽이 헤매지 않고 움직일 수 있습니다.
스마트폰 SSH 접속으로 tmux 세션에 끼어들기
알림을 받아도, 내용에 따라서는 가벼운 확인으로 끝나지 않고 실제로 스포크 화면을 보고 판단하고 싶은 장면이 나옵니다. 이 경우는 스마트폰 SSH 클라이언트 앱에서 호스트에 SSH 접속하고, 해당 tmux 세션에 직접 어태치합니다.
# 스마트폰 SSH 클라이언트에서
$ ssh <호스트>
$ tmux attach -t proj-income
스마트폰용 SSH 클라이언트는 iOS라면 Termius나 Blink Shell, Android라면 Termius나 JuiceSSH가 널리 쓰입니다. tmux 세션에 붙어 두면 화면이 좁아도 상태 확인과 짧은 지시 정도는 충분히 됩니다. 공용 와이파이에서 접속할 때는 키 기반 인증을 쓰고 비밀번호 로그인을 끄는 편이 안전합니다.
어태치하면, 그 스포크가 지금 막 표시하고 있는 화면이 그대로 보입니다. 여기서 필요하면 직접 텍스트를 쳐서 지시를 더하거나, 승인을 요구받은 조작에 Yes라고 답하기도 합니다. 확인이 끝나면 Ctrl-b d로 디태치합니다. 디태치해도 스포크 쪽 세션은 그대로 계속 도니, 모바일 쪽 접속을 끊어도 프로세스가 죽는 일은 없습니다.
일상적으로는 허브 쪽 proj peek(제4장 참조)로 엿보는 것만으로 대부분의 상황 판단이 끝납니다. 실제로 모바일에서 어태치하는 건, peek 정보만으로는 판단이 서지 않을 때, 혹은 사람 본인의 입력(승인 의사 표시나 추가 방침 지시)이 필요할 때로 한정됩니다. 통근 중에 알림을 받고, 전철 안에서 SSH 접속해 상황을 확인하고, 한마디만 지시를 쳐서 곧바로 디태치하는 정도의 관여가 실무상 표준적인 사용법이 됩니다.
"승인만 사람, 실행은 전부 에이전트"의 경계선
제2장에서 권한 경계의 3층 설계를 정했습니다. 모바일에서의 운용에서는, 이 경계선을 "사람이 누르는 버튼 수를 최소화한다"는 형태로 구체화하고 있습니다.
예를 들어 원고를 써서 수익화하는 작업에서는, 집필·퇴고·체재 정리·리포지토리 커밋까지 전부 에이전트가 합니다. 사람이 하는 건 공개 플랫폼의 "공개하기" 버튼을 누르는 조작만으로 좁힙니다. 이 설계라면 모바일 쪽에서 할 일은 알림을 받고, 내용을 가볍게 확인하고, 버튼을 누르는 3단계로 끝납니다. 긴 문장을 쓰거나 세세한 설정을 확인할 필요가 없습니다.
마찬가지로 코드 변경을 수반하는 작업에서는, 구현·테스트·프로젝트 안에서의 commit까지는 에이전트가 하고, 외부 리포지토리로의 git push만을 사람 확인 사항으로 두고 있습니다. 이 선 긋기 기준은 일관되게 "되돌릴 수 있는 조작인가 아닌가"입니다(자세한 건 제2장 참조). 모바일의 작은 화면에서 판단할 수 있는 건 기껏해야 "Yes/No" 수준의 승인뿐이고, 복잡한 diff 리뷰를 스마트폰으로 시키는 건 비현실적입니다. 그렇기에 승인이 필요한 조작 자체를 "Yes/No로 답할 수 있는 입자"까지 미리 에이전트 쪽에서 압축해 두는 설계가 중요해집니다.
잘못 지나치게 자동화한 실패 예
솔직히 적으면, 알림 설계는 처음부터 지금 형태였던 게 아닙니다. 운용 초기에 알림 빈도가 아무튼 많아서(하루에 20건 넘게 오던 시기가 있었습니다), 체감으로 "또야"라는 상태였습니다. 이에 대한 대처로, 처음에는 "Blocked 추가 이외는 기본 뮤트한다"는 조잡한 임계값을 설정했습니다.
이 설정을 넣고 얼마 지났을 무렵, 어느 프로젝트의 스포크 컨테이너가 비정상 종료해서, 그대로 이틀 가까이 아무도 알아채지 못한 채 방치되는 사태가 일어났습니다. 비정상 종료 자체는 본래 "비정상 종료·예상 밖 에러"로 알려야 할 사건이었지만, 당시 알림 필터는 "Blocked 추가"만을 조건으로 삼고 있었기에 이런 종류의 이벤트가 그냥 지나가 버렸습니다. 알아챈 계기는, 마침 허브가 정기 순회로 proj status를 두드렸을 때 그 프로젝트의 tmux 세션이 사라져 있는 걸 발견한, 우연에 가까운 형태였습니다.
이 일을 계기로, 알림 조건은 "나중에 좁힌다"보다 "처음엔 넓게 설정하고, 실제로 노이즈라고 판명된 것만 솎아낸다"는 방향으로 다시 설계했습니다. 알림이 너무 많은 문제에 대한 대처는, 알림 종류를 줄이는 게 아니라 알림 문면을 간결하게 해서 확인 비용을 낮추는 것으로 해결하고 있습니다.
"뭐든 자동 승인·자동 뮤트로 하면 편해진다"는 발상은, 편해진 만큼 알아챔이 늦어지는 리스크를 뒷면에 안고 있습니다. 이게 이 실패에서 얻은 교훈입니다.
다음 장으로
이 장에서 본 "비정상 종료를 알아채지 못했다"는 이야기는, 사실 빙산의 일각에 불과합니다. 다음 장에서는 실제로 가동 중인 체제를 덮친 장애(컨테이너 관리 도구의 자동 업데이트로 샌드박스가 죄다 죽은 사고, 인증 정보 실효로 허브와 스포크의 동기가 무너진 이야기)를, 대응의 전말까지 포함해 자세히 다룹니다.
제7장 — 장애와 복구(실제로 일어난 일)
이 장에서 다룰 것
앞 장까지 허브&스포크 체제의 "돌리는 법"을 한 차례 다졌습니다. tmux로 스포크를 세우고(제4장), 위험한 조작은 Docker로 격리하고(제5장), 모바일에서 개입하는 동선도 마련했습니다(제6장). 여기까지 읽으면 체제가 깔끔하게 완성돼 있는 것처럼 보일지 모릅니다. 하지만 실운용에서는, 상정하지 못한 형태로 체제 그 자체가 망가지는 순간이 몇 번 있었습니다. 이 장에서는 실제로 일어난 두 가지 장애를, 대응의 전말까지 포함해 솔직히 공유합니다. 깔끔한 성공담만 쓰는 건 실무서로서는 반쪽짜리라고 봅니다.
사고 1: 컨테이너 관리 도구의 자동 업데이트로 모든 샌드박스가 죽었다
필자의 환경에서는 Docker의 실체가 Linux 배포판의 패키지 관리 시스템(snap)을 거쳐 들어가 있습니다. 이 패키지 관리 시스템에는 백그라운드에서 정기적으로 패키지를 최신판으로 자동 업데이트하는 구조가 있습니다. 어느 밤, 가동 중이던 스포크가 셋 다 동시에 침묵하는 사태가 일어났습니다.
원인을 조사하니, 이 snap의 자동 업데이트가 Docker 본체를 말려들게 해서, 업데이트 과정에서 Docker 데몬이 재시작돼 있었습니다. 제5장에서 설명한 대로, 스포크 컨테이너는 --rm을 붙인 일회용으로 기동합니다. 데몬 재시작으로 모든 컨테이너가 정지했고, 그리고 --rm의 성질상 정지와 동시에 파기되었습니다. tmux 위의 해당 세션을 들여다보니, 컨테이너 프롬프트가 사라지고 호스트 셸로 돌아와 있을 뿐인 상태였습니다.
이 사고가 성가신 건, 에러 메시지가 아무것도 안 뜬 채 일어난다는 점입니다. 컨테이너가 비정상 종료해도, tmux 세션 자체는(제4장의 proj 스크립트 구현상) exec bash로 살려 둔 채라, 세션이 사라진 것처럼은 보이지 않습니다. 그저 화면이 호스트 프롬프트로 돌아와 있을 뿐이라, 얼핏 봐서는 "스포크가 지금 마침 아무것도 안 하고 있는" 것과 구별이 안 됩니다. 실제로 이 사고를 알아챈 것도 정기 순회로 proj status를 두드렸을 때 컨테이너 이름 목록이 비어 있던 데서였고, 알림 구조가 이런 종류의 비정상 종료를 검지하지 못했습니다(이 알림 설계의 미비는 제6장에서 다룬 이야기와 맞닿아 있습니다).
복구 작업 자체는 단순했습니다. 각 프로젝트의 TASKS.md의 Doing을 보면 무엇이 진행 중이었는지 한눈에 알 수 있고, JOURNAL.md의 최근 엔트리의 "다음 한 수"를 보면 재개해야 할 작업 내용도 명확했습니다. 해당 프로젝트에 대해 proj launch를 다시 치고, proj send로 TASKS.md의 Doing을 진행하라고 지시하는 것만으로, 실질 몇 분 만에 작업은 원래 지점에서 재개할 수 있었습니다.
항구 대책으로는, snap에 의한 Docker 본체의 자동 업데이트를 멈추는 설정(snap refresh --hold docker 상당의 커맨드)이 후보로 올라 있습니다. 완전히 사고를 막는 것보다, 사고가 나도 몇 분 만에 알아채고 복구할 수 있는 체제 쪽을 우선한 형태입니다.
이 대책은 호스트 쪽 패키지 관리에 손을 대는 조작이라, 권한 경계(제2장)의 사고방식에 따르면 사람의 승인을 거쳐야 할 변경에 해당합니다. 그래서 항구 대책 적용 자체는 보류하고, 당분간은 "죽으면 알아채고 다시 세운다"는 운용을 이어 가고 있습니다.
사고 2: 인증 정보 실효로 허브와 스포크의 동기가 깨졌다
또 하나, 성질이 다른 장애가 일어났습니다. 이쪽은 컨테이너가 죽는 것 같은 요란한 사고가 아니라, 서서히 알아채기 어려운 형태로 진행됐습니다.
제5장에서 짚은 대로, 컨테이너 안 스포크에는 호스트의 인증 정보를 그대로 넘기지 않고, 전용 디렉터리에 복사한 최소한의 인증 정보만 마운트합니다. 이 설계 자체는 안전 쪽으로 넘어가기 위함이지만, 부작용으로 "호스트 쪽에서 인증을 갱신해도 복사본에는 자동 반영되지 않는다"는 성질을 가집니다.
실제로, 호스트 쪽 인증 토큰을 갱신한 타이밍에 복사본 갱신을 잊은 채 스포크를 재시작한 적이 있습니다. 그 결과 그 스포크는 "로그인하지 않은" 상태로 기동해, 지시를 보내도 반응이 돌아오지 않는 상태가 이어졌습니다. 이때는 반나절 가까이 지나서야 알아챘고, 원인을 가려내는 데도 쓸데없는 시간이 걸렸습니다. "컨테이너가 죽어 있는" 것이라면 proj status로 곧바로 판별할 수 있지만, "컨테이너는 돌고 있는데 인증만 옛것"인 상태는 컨테이너 목록상으로는 정상으로 보여 버리기에, 더 발견이 늦어지기 쉽습니다.
이와 별개로, 외부 리포지토리를 조작하기 위한 토큰이 유효 기간 만료가 되어, 허브 쪽에서 준비한 변경을 리포지토리에 반영하는 조작(제2장에서 말한 권한 경계 제2층)이 전부 멈춘 적도 있습니다. 이 경우는 에이전트 쪽 작업 자체는 문제없이 진행되고 있었기에 실제 피해는 가벼웠지만, "허브와 스포크의 동기"라기보다는 "에이전트 쪽과 외부 서비스의 동기"가 깨진 예로 기록해 둡니다.
장애에서 배운 것(상태를 파일에 갖게 하는 것의 진짜 가치)
이 두 장애에 공통되는 건, "세션이라는 실행체 그 자체가 망가진다"는 점입니다. 컨테이너는 파기되고, 인증은 끊기고, tmux 세션의 내용물은 리셋됩니다. 만약 진척 정보가 세션의 대화 이력에만 있었다면, 이런 종류의 장애는 그대로 정보의 소실로 직결됐을 겁니다.
실제로는 제3장에서 정한 "상태는 대화가 아니라 파일에 둔다"는 원칙 덕에, 어느 장애에서도 복구 비용은 "해당 세션을 다시 세우고, 파일 내용을 넘긴다"만으로 끝났습니다. 이건 미리 장애를 상정해 설계했다기보다, 컨텍스트를 예산으로 다룬다(제2장)는 다른 목적으로 채택한 설계가 우연히 장애 내성으로도 기능했다는 편이 정확합니다. 결과적으로 얻은 교훈은, "세션은 언젠가 반드시 망가진다는 전제로, 망가진 뒤의 재개 비용을 최소화하는 설계를 먼저 마련해 둔다"는 것의 가치가, 평소보다 장애 때 오히려 뚜렷이 드러난다는 점입니다.
장애 대응을 에이전트 자신에게 1차 대응시키는 설계
이 장애들을 겪은 뒤, 장애 대응의 1차 가려내기를 허브 자신에게 맡기는 운용으로 바꿨습니다. 구체적으로는, 정기 순회 때마다 다음 세 가지를 기계적으로 확인하게 합니다.
정기 순회에서 기계적으로 확인하는 3가지
1. proj status로, 가동하고 있어야 할 스포크가 전부 살아 있는가
2. 살아 있는 스포크에 대해, 최근 TASKS.md / JOURNAL.md의 갱신 시각이
극단적으로 오래되지 않았는가 (인증 끊김 등으로 응답이 멈춘 징후)
3. Blocked 섹션에, 승인·외부 조작 대기 항목이 정체돼 있지 않은가
이 체크로 이상을 검지한 경우, 허브가 하는 건 "이상의 검지와 1차 가려내기"까지이고, 항구 대책 적용(패키지 관리 설정 변경 등)은 사람의 승인을 기다립니다. 반면 컨테이너 재시작이나 인증 정보 재복사처럼 제1층(자유롭게 실행해도 되는 범위)에 들어오는 복구 조작은, 허브가 사람을 기다리지 않고 실행합니다. 이 선 긋기 덕에, 심야에 일어난 사고라도 아침에 사람이 확인할 무렵이면 대개의 스포크는 스스로 복구를 마친 상태를 만들 수 있습니다.
다음 장으로
장애와 복구라는 "잘 안 풀린 이야기"를 다뤘으니, 마지막으로 이 체제 그 자체의 적용 범위를 솔직히 검토하려 합니다. 다음 장에서는 몇 개 프로젝트까지라면 한 명의 허브로 감독할 수 있는지, 러닝 코스트는 어느 정도인지, 그리고 이 체제가 맞지 않는 작업은 무엇인지를 다루며 이 책을 마무리합니다.
제8장 — 이 체제가 맞는 장면·맞지 않는 장면, 그리고 확장
이 장에서 다룰 것
여기까지 일곱 장에 걸쳐, 허브&스포크 체제의 설계 사상(제2장), 상태 관리(제3장), 실행 환경(제4장), 격리(제5장), 모바일 운용(제6장), 그리고 실제 장애(제7장)를 한 차례 다뤄 왔습니다. 마지막 장에서는 관점을 바꿔, 이 체제의 적용 범위를 솔직히 검토합니다. 무엇에든 먹히는 만능 체제인 양 쓰는 건 성실하지 않습니다. 맞는 장면과 맞지 않는 장면을 분명히 하고, 끝으로 앞으로의 확장 방향을 제시하며 이 책을 마무리합니다.
몇 개 프로젝트까지라면 한 명의 허브로 감독할 수 있는가
필자의 현재 운용은, 우선순위가 다른 3~4개 프로젝트를 한 명의 허브가 병행 감독하는 규모입니다. 이 규모에서의 체감은 "아직 여유가 있다"는 것입니다. 한 번의 순회로 TASKS.md를 읽고, Blocked 유무를 확인하고, 실행 가능한 태스크를 몇 건 골라 위임하는 일련의 작업은, 익숙해지면 몇 분 만에 끝납니다.
한편으로, 이 규모를 넘어섰을 때 어디에서 무리가 나는지도 보이기 시작했습니다. 병목이 되는 건 구현력이나 모델 성능이 아니라 허브 자신의 순회 빈도입니다. 프로젝트가 늘수록, 한 번의 순회로 전 프로젝트의 TASKS.md를 다 읽는 데 드는 시간(≒컨텍스트 소비)이 선형으로 늘어납니다. 제2장에서 말한 "허브는 요약만 읽는다"는 원칙을 지켜도, 요약을 읽을 대상의 수 자체가 늘면 소비량은 쌓입니다.
체감으로서의 어림은, 우선순위가 가장 높은 1개 프로젝트에는 촘촘한 감독(수십 분마다 순회)을 하면서 그 밖의 프로젝트를 성긴 감독(수 시간하루마다 순회)으로 돌리는 형태라면 56개 프로젝트 정도까지는 무너지지 않는다는 것입니다. 이를 넘어 모든 프로젝트에 촘촘한 감독을 요구하게 되면, 허브의 순회 자체가 따라가지 못하게 되어, 결국 Blocked 항목을 알아채는 게 늦어지는, 제7장에서 본 문제의 변종이 일어납니다. "프로젝트 수의 상한"보다 "촘촘한 감독이 필요한 프로젝트 수의 상한"으로 생각하는 편이 실태에 가깝다, 이게 현시점의 결론입니다.
러닝 코스트 시산(모델 계층별 토큰 소비 감각)
체제를 이어 가는 데 무시할 수 없는 게 API 비용입니다. 엄밀한 수치를 내는 건 환경 의존이 커서 이 책의 범위를 넘지만, 체감으로서의 비용 구조를 공유해 두겠습니다.
제4장에서 정한 모델 계층(scout=haiku, builder=sonnet, deep-thinker=opus)은, 그대로 비용의 경사도 됩니다. 정형 작업을 scout에 몰아넣음으로써, 전체 토큰 소비의 대부분은 가장 저렴한 계층에 흡수됩니다. 중량급 작업(구현·실험)은 builder가 담당하기에, 체감으로서의 비용 대부분은 여기에 집중됩니다. deep-thinker 위임은 빈도로는 가장 적고, 그만큼 1회당 단가가 높아도 총비용에 대한 영향은 한정적입니다.
비용 감각의 어림 (상대비, 체감값이며 실측값 아님)
scout (haiku) : 1
builder (sonnet): 6~8
deep-thinker (opus) : 20 전후
→ 총비용의 내역은, 빈도 높은 builder 위임이 지배적이 되기 쉽다
(단가 높은 deep-thinker는 빈도를 좁혀 총액 영향을 억제한다)
여기서 얻을 수 있는 실무상의 시사는, "헷갈리면 싼 쪽부터 써 본다"(제4장)는 원칙이 단순히 품질 담보를 위해서만이 아니라 비용 설계로서도 기능한다는 점입니다. builder로 충분한 작업을 deep-thinker에 계속 던지면, 빈도 × 단가가 양쪽 다 뛰어올라 총비용이 체감으로 몇 배로 부풀어 오릅니다. 반대로 scout로 끝날 작업을 아까워하며 builder에 던지는 건 비용 면에서는 큰 차이가 아니지만, 판단의 질이라는 뜻에서 과잉도 부족도 아닌 적정 배치를 유념하고 있습니다.
맞지 않는 경우
솔직히 적으면, 이 체제가 어떤 작업에든 맞는 건 아닙니다. 운용해 보고 맞지 않는다고 판단한 경우는 주로 두 가지입니다.
1. 강한 동기가 필요한 페어프로 같은 작업
설계 판단을 사람과 에이전트가 몇 수 앞까지 서로 읽으며 다져 가는 작업(가령 아키텍처의 근간에 관한 결정을, 여러 선택지를 그때그때 비교하며 사람과 함께 다져 가는 장면)은, 허브&스포크식의 "던지고 나중에 검수한다"는 운용과 궁합이 나쁩니다. 이런 종류의 작업은 상태를 파일에 떨궈 비동기로 주고받기보다, 세션 하나에서 사람과 에이전트가 같은 화면을 보며 촘촘히 대화하는 편이 빠릅니다. 무리하게 스포크로 잘라내면, 도리어 지시가 오가는 횟수가 늘어 비효율이 됩니다.
2. 기밀성 높은 데이터를 다루는 경우
필자가 안고 있는 프로젝트의 하나는, 성질상 실데이터나 기밀 정보를 일절 다루지 않고 공개 데이터·합성 더미 데이터로만 검증한다는 강한 제약을 걸고 있습니다. 이건 체제의 한계라기보다, 체제와는 독립적으로 반드시 지켜야 할 제약입니다.
"격리하면 뭐든 에이전트에게 맡길 수 있다"는 발상 자체를 다시 볼 필요가 있습니다. 제5장에서 다룬 Docker에 의한 격리는 어디까지나 "오작동의 피해 범위를 좁히기" 위한 것이지, "기밀 데이터를 다뤄도 되는 이유"가 되지는 않습니다. 다루는 데이터의 성질 그 자체가 체제 설계보다 상위의 제약이 되는 장면이 있다는 것은 강조해 두고 싶습니다.
앞으로의 확장 방향
현시점의 체제는 허브와 스포크 사이의 일대다 지시·보고만으로 완결돼 있어, 스포크끼리 직접 주고받는 구조는 갖고 있지 않습니다. 어느 프로젝트에서 얻은 지견(가령 모델 계층 판단 기준의 개선이나, 어떤 도구 사용법의 발견)을 다른 프로젝트에 반영하고 싶은 경우, 지금은 한번 허브를 거쳐야 합니다. 스포크 간 직접 연계, 혹은 공통 노하우를 축적하는 공유 파일 같은 구조는, 프로젝트 수가 늘었을 때 효과를 보는 확장 후보라고 봅니다.
또 하나의 확장 방향은, 새 프로젝트 제안을 허브 자신에게 시키는 것입니다. 지금도 사람의 목적에 맞는 새 프로젝트를 허브가 제안할 여지는 남겨 두었지만, 실행에 옮길지 여부의 판단은 반드시 사람의 승인을 거칩니다(제2장의 권한 경계 사고방식을 그대로 적용합니다). 이 "제안은 에이전트, 착수 판단은 사람"이라는 비대칭 분담은, 체제의 자율성을 높이면서도 멋대로 프로젝트가 증식하는 리스크를 막기 위한, 지금으로선 타당한 선 긋기라고 봅니다.
참고로 tmux에서의 병렬 기동 같은 "메커니즘" 부분은, 표준 병렬 에이전트 기능(제4장에서 짚은 Agent Teams)의 성숙에 따라 언젠가 자작 래퍼에서 표준 기능으로 옮겨 갈 가능성이 높습니다. 이 책이 거듭 강조해 온 건, 그 메커니즘 자체보다 상태를 파일에 정리하는 설계·권한 경계를 긋는 법·장애로부터의 복구 설계라는 운용의 규율 쪽입니다. 메커니즘이 표준화되더라도, 이 규율들을 어떻게 설계할지라는 문제는 독자 자신의 손에 계속 남습니다.
맺으며
이 책에서는 1세션 Claude Code 운용이 여러 프로젝트의 병행 운용에서 한계를 맞는다는 진단(제1장)에서 출발해, 허브&스포크라는 설계 사상(제2장), 상태를 파일에 떨구는 구체적인 운영법(제3장), tmux에 의한 실행 환경(제4장), Docker에 의한 격리(제5장), 모바일에서의 개입과 승인 게이트(제6장), 그리고 실제로 일어난 장애와 그 복구(제7장)를, 되도록 구체적인 커맨드와 설정 파일, 그리고 실패담까지 곁들여 소개해 왔습니다.
이 체제는 완성품이 아니라, 지금도 장애가 날 때마다, 판단 실수를 할 때마다, 조금씩 수정이 더해지고 있는 운용 중인 것입니다. 이 책에 쓴 내용도 몇 달 뒤에는 모습을 바꾸고 있을지 모릅니다. 그래도 "에이전트를 한 명이 아니라 팀으로 다룬다"는 발상의 전환과, "상태는 대화가 아니라 파일에 둔다"는 원칙, 이 두 가지는 앞으로 체제가 아무리 변해도 흔들리지 않는 핵심으로 남는다고 봅니다. 독자가 자기 환경에서 이 체제를 짤 때의 출발점으로 이 책이 도움이 된다면 다행이겠습니다.
이 글이 도움이 됐다면 추천해 주세요