짧은 작업은 이런 고민이 필요 없습니다. 실패하면 다시 시키면 됩니다. 문제는 한 시간 넘게 걸리는 작업입니다.
세션은 여러 이유로 끊깁니다. 컨텍스트가 차거나, 실행 환경이 회수되거나, 사람이 중간에 멈추거나, 네트워크가 끊깁니다. 끊기는 것 자체는 막을 수 없습니다. 막아야 할 건 끊겼을 때 진행분이 통째로 사라지는 것입니다.
1. 진짜 문제는 진행 상황이 대화에만 있다는 것
기본 상태에서 "여섯 편까지 끝났다"는 사실은 어디에 있을까요? 대화 기록에만 있습니다. 세션이 끝나면 같이 사라집니다.
더 나쁜 건 작업 결과물도 같이 사라진다는 것입니다. 에이전트가 여섯 편을 다 쓰고 마지막에 한 번 커밋하려 했다면, 다섯 편 반쯤에서 끊길 때 남는 건 아무것도 없습니다.
원칙은 간단합니다. 완료된 단위는 즉시 영구 저장소에 남긴다. 에이전트 작업에서는 그게 커밋과 푸시입니다.
2. 중단 지점마다 무엇이 사라지는가
"중단"을 한 덩어리로 보면 설계가 안 나옵니다. 어느 지점에서 끊겼느냐에 따라 잃는 것이 다르고, 다음 세션이 해야 할 일도 다릅니다.
표를 보면 대책이 두 줄로 압축됩니다. 둘째 줄을 없애려면 계획을 먼저 커밋하고, 넷째·다섯째 줄을 짧게 만들려면 커밋과 푸시를 항목 단위로 붙입니다. 나머지 줄은 어차피 손실이 한 항목 이하입니다.
둘째 줄이 제일 비쌉니다. 초안은 다시 쓰면 비슷하게 나오는데, "왜 이 여덟 개를 이 순서로 골랐는가"는 다시 만들면 다른 답이 나옵니다. 그러면 앞 세션과 뒤 세션이 서로 다른 계획을 절반씩 실행한 결과가 남습니다.
3. 계획을 먼저 커밋한다
여기서 한 걸음 더 나갑니다. 결과물보다 계획을 먼저 남깁니다.
이 블로그의 예약 게시 글은 에이전트가 회차 단위로 미리 씁니다. 그 에이전트가 가장 먼저 하는 일은 글쓰기가 아니라 .content-queue.md를 만들어 커밋하는 것입니다.
이 파일이 있으면 다음 세션이 대화 기록 없이도 상태를 복원합니다. 무엇을 하기로 했는지, 어디까지 했는지, 뭐가 남았는지가 전부 저장소 안에 있습니다.
부수 효과도 큽니다. 계획을 먼저 적으면 에이전트가 중간에 주제를 바꾸지 못합니다. 계획 없이 시작하면 6편쯤에서 원래 의도와 다른 글을 쓰고 있는 경우가 생깁니다.
글 한 편은 다시 쓰면 되지만, "왜 이 여덟 편을 이 순서로 골랐는가"는 다시 만들어내기 어렵습니다. 싸고 사라지기 쉬운 것부터 저장하세요.
큐 파일에 반드시 있어야 하는 것
큐 파일이 상태를 복원하려면 다음 세션이 던질 질문에 전부 답할 수 있어야 합니다. 실제로 필요했던 필드는 여섯 개였습니다.
마지막 두 개가 나중에 추가된 것들입니다. "이 회차는 끝났다"를 적지 않으면 다음 세션은 미완료 항목이 없다는 사실만 보고 판단해야 하는데, 그건 "계획이 아직 안 적힌 회차"와 구분되지 않습니다. 한 줄로 명시하는 편이 훨씬 안전합니다.
큐 파일은 사람도 읽습니다. 그래서 JSON 이 아니라 마크다운 표로 뒀습니다. 기계가 읽어야 하는 값은 체크박스 하나뿐이고, 나머지는 다음 세션이 판단하는 데 쓰는 근거라 문장으로 적는 편이 낫습니다.
4. 커밋 단위 = 작업 항목 단위
커밋을 언제 하느냐가 핵심입니다. 규칙은 하나입니다. 한 항목이 검증을 통과하면 그 자리에서 커밋하고 푸시합니다.
중요한 건 같은 커밋에 결과물과 진행 표시를 함께 넣는 것입니다.
따로 커밋하면 둘이 어긋나는 순간이 생깁니다. 글은 커밋됐는데 체크박스는 안 된 상태에서 끊기면, 다음 세션은 그 글을 다시 씁니다. 하나의 커밋이면 그 틈이 없습니다.
푸시도 매번 합니다. 실행 환경이 통째로 회수되는 경우엔 로컬 커밋도 같이 사라지기 때문입니다. 로컬 커밋은 체크포인트가 아닙니다.
항목을 얼마나 크게 자를 것인가
"항목 하나 = 커밋 하나"는 항목 크기가 적당할 때만 성립합니다. 크기를 정하는 기준은 세 가지였습니다.
- 혼자서 검증이 되는가. 다른 항목이 끝나야 빌드가 통과한다면 그 둘은 한 항목입니다. 이 사이트에서 ko/en 한 쌍이 항상 같은 항목인 이유가 이것입니다 — 한쪽만 커밋하면 빌드가 실패합니다.
- 한 세션 안에 끝나는가. 항목 하나가 세션을 넘길 만큼 크면 항목 단위 커밋이 의미가 없습니다. 그 항목은 다시 쪼개야 합니다.
- 다시 해도 손해가 적은가. 중단되면 그 항목은 통째로 다시 합니다. 다시 하는 비용이 아까울 만큼 크면 그것도 쪼갤 신호입니다.
반대로 너무 잘게 자르면 큐 파일 관리가 작업보다 커집니다. 저희 기준은 "한 항목이 10~40분"이었습니다. 그보다 짧으면 묶고, 길면 쪼갭니다.
5. START와 RESUME을 나눈다
재개를 제대로 하려면 에이전트가 지금이 새 작업인지 이어받는 작업인지 알아야 합니다. 그래서 실행 모드를 명시적으로 나눴습니다.
RESUME의 첫 줄이 특히 중요합니다. 할 일이 없으면 아무것도 하지 않고 끝나야 합니다.
이걸 명시하지 않으면 에이전트는 뭔가를 합니다. 빈손으로 끝내는 것보다 일을 만드는 쪽이 도움이 된다고 판단하기 때문입니다. 그래서 아무도 요청하지 않은 회차가 새로 시작됩니다. 자동으로 도는 작업에서는 이게 조용히 반복됩니다.
이건 에이전트의 기본값이 아닙니다. 무해해 보이는 빈 실행이 몇 번 반복되면 계획에 없던 결과물이 쌓입니다.
6. 재개할 때 무엇을 어떤 순서로 확인하는가
RESUME 이 실제로 하는 일은 판정 다섯 개입니다. 순서가 중요해서 그대로 적어뒀습니다.
1번을 빼먹으면 조용히 어긋납니다. 실행 환경이 새로 만들어지면 저장소는 기본 브랜치 상태로 클론되어 있고, 앞 세션이 만든 브랜치는 원격에만 있습니다. 로컬만 보고 "미완료 회차가 없다"고 판단하면 같은 회차를 처음부터 다시 시작합니다.
3번도 실제로 겪은 것입니다. 브랜치 이름에 날짜를 넣었더니, 같은 날 두 번째로 깨어난 세션이 -2 를 붙인 새 브랜치를 팠습니다. 결과물은 둘 다 남았지만 PR 이 두 개가 되고 큐 파일이 양쪽에서 갈라졌습니다. "같은 이름이 있으면 그것을 이어 쓴다"를 명시한 뒤로 사라진 문제입니다.
7. 완료 판정은 관찰 가능해야 한다
재개가 되려면 "이 항목이 끝났는가"를 대화 기록이 아니라 파일에서 판정할 수 있어야 합니다.
여기서는 두 가지가 서로를 뒷받침합니다.
- 체크박스 — 큐 파일의
[x]가 1차 근거입니다. - 파일 존재 —
src/{ko,en}/posts/{slug}.njk가 있으면 실제로 만들어진 겁니다.
둘이 어긋나면 파일 쪽을 믿습니다. 체크박스는 기록이고 파일은 사실이니까요. 어긋나는 경우 자체가 드물긴 합니다 — 같은 커밋에 넣기 때문에요.
이 구조는 멱등성도 줍니다. 같은 세션을 두 번 돌려도 두 번째는 이미 완료된 항목을 건너뜁니다. 자동으로 도는 작업에서는 중복 실행이 반드시 생기니, 이건 선택이 아닙니다.
8. 검증도 항목 단위로
커밋을 항목 단위로 쪼갰으면 검증도 같이 쪼개야 합니다. 여덟 편을 다 쓰고 마지막에 빌드를 돌리면, 실패했을 때 어느 편이 원인인지 찾는 데 시간이 걸립니다. 더 나쁜 건 중단됐을 때 검증 안 된 결과물이 남는다는 것입니다.
규칙은 통과하지 못한 항목은 커밋하지 않는다입니다. 이러면 저장소의 모든 커밋이 빌드가 통과하는 상태고, 어느 지점에서 끊겨도 남은 것은 전부 정상입니다.
이 사이트는 빌드에 검증 가드가 들어 있어서 이게 특히 잘 맞습니다. 검증이 명령 하나로 끝나면 항목마다 돌리는 비용이 거의 없습니다.
검증 비용이 항목 단위 커밋의 상한을 정합니다. 이 저장소는 빌드가 1초 아래라 매 항목마다 돌려도 부담이 없습니다. 반대로 검증에 10분이 걸리면 항목마다 돌리는 설계는 성립하지 않습니다. 그때는 빠른 검사(문법·타입·해당 항목 테스트)만 항목 단위로 돌리고, 느린 전체 검사는 회차 끝에 한 번 돌리는 식으로 나눕니다. 나누는 기준은 "이 검사가 실패했을 때 원인 항목을 특정할 수 있는가"입니다.
9. 실패한 항목은 어떻게 남기나
여기까지는 전부 "잘 되면"의 이야기입니다. 실제로는 항목 하나가 계속 안 되는 경우가 생깁니다. 이때 최악은 조용히 건너뛰는 것입니다. 다음 세션이 큐 파일을 보고 "미완료구나" 하고 같은 벽에 다시 부딪힙니다.
그래서 완료 표시를 두 값이 아니라 세 값으로 뒀습니다.
세 번째 상태가 있으면 다음 세션이 같은 시도를 반복할지 사람을 기다릴지를 판단할 수 있습니다. 이유가 "빌드가 실패해서"면 다시 해볼 만하고, "번역 원문이 아직 없어서"면 다시 해도 똑같습니다.
실패를 적는 자리가 큐 파일인 것도 의도적입니다. 로그는 세션과 함께 사라지고, 이슈는 별도 시스템이라 다음 세션이 안 봅니다. 이어받는 쪽이 반드시 읽는 파일에 남겨야 읽힙니다.
10. 규칙 문서에 무엇을 적을까
이 구조를 굴리는 데 필요한 규칙은 생각보다 적습니다. 에이전트 규칙 문서에 적어둔 건 이 정도입니다.
두 번째 줄의 "몰아서 커밋하지 마라"가 없으면 에이전트는 대체로 몰아서 커밋합니다. 그게 사람 기준으로는 깔끔한 이력이니까요. 하지만 중단을 전제하면 깔끔한 이력보다 잦은 체크포인트가 낫습니다.
11. 자주 묻는 것
큐 파일을 저장소에 커밋하면 이력이 지저분해지지 않나요?
항목마다 한 줄씩 바뀌므로 커밋이 늘긴 합니다. 그런데 그 커밋들이 곧 작업 이력입니다. "언제 무엇을 어떤 순서로 했는가"가 결과물 옆에 같이 남습니다. 별도 대시보드를 만드는 것보다 싸고, 무엇보다 다음 세션이 읽을 수 있는 자리에 있습니다.
이슈 트래커나 프로젝트 보드를 쓰면 안 되나요?
써도 됩니다. 조건은 하나입니다 — 에이전트가 작업 중에 읽고 쓸 수 있어야 합니다. 사람이 보기엔 보드가 낫지만, 매 항목마다 API 를 호출해 상태를 갱신해야 하고 그 호출이 실패하면 상태가 어긋납니다. 저장소 안의 파일은 결과물과 같은 커밋에 들어가므로 어긋날 틈이 구조적으로 없습니다.
여러 에이전트가 동시에 같은 큐를 처리해도 되나요?
이 구조 그대로는 안 됩니다. 둘이 같은 브랜치에 커밋하면 푸시가 충돌하고, 같은 항목을 동시에 집을 수도 있습니다. 병렬로 돌리려면 항목마다 브랜치를 나누거나, 집는 순간 "진행 중"을 커밋해 선점해야 합니다. 저희는 한 번에 하나만 도는 전제로 뒀습니다 — 스케줄이 하루 한 번이라 병렬이 필요 없었습니다.
완료 표시만 있고 결과물이 없으면 어떻게 되나요?
파일 쪽을 믿고 다시 합니다. 이 조합은 결과물과 표시를 다른 커밋에 넣었을 때만 생깁니다 — 같은 커밋에 넣으면 구조적으로 불가능합니다. 반대 조합(결과물은 있는데 표시가 없음)도 마찬가지고, 이쪽이 훨씬 나쁩니다. 이미 있는 파일을 덮어쓰며 다시 쓰기 때문입니다.
중단이 흔하지 않은데도 이렇게까지 해야 하나요?
비용을 재보면 답이 나옵니다. 이 구조가 추가로 요구하는 건 계획 커밋 하나와 항목마다의 git push 뿐입니다. 반면 중단 한 번의 손실은 그때까지 한 작업 전부입니다. 확률이 낮아도 손실이 크면 값을 치를 만합니다. 게다가 사람이 안 보는 시간에 도는 작업이라면 중단은 드물지 않습니다.
세션이 끊긴 게 아니라 잘못된 결과를 냈으면요?
그건 재개 설계가 아니라 범위 설계의 문제입니다. 항목 단위 커밋은 잘못된 결과도 항목 단위로 남기므로, 되돌릴 때는 그 커밋만 되돌리면 됩니다 — 그 점에서는 도움이 됩니다. 하지만 애초에 잘못된 결과가 배포로 직행하지 않게 하는 건 다른 문제고, 브랜치와 PR 이 그 자리를 맡습니다.
12. 정리
결국 오래된 원칙들입니다. 배치 작업에 체크포인트를 두고, 작업 단위를 원자적으로 만들고, 재실행이 안전하게 만드는 것. 에이전트라고 다르지 않습니다.
- 진행 상황을 저장소에 남긴다. 대화에만 있으면 세션과 함께 사라집니다.
- 계획을 결과물보다 먼저 커밋한다. 재생성하기 가장 어려운 게 계획입니다.
- 항목 하나 = 검증 하나 = 커밋 하나 = 푸시 하나. 결과물과 진행 표시는 같은 커밋에.
- 완료 판정을 파일에서 할 수 있게 한다. 그래야 재실행이 안전합니다.
- 할 일이 없으면 종료하라고 명시한다. 이건 기본값이 아닙니다.
- 재개는 원격을 먼저 가져오는 것에서 시작한다. 로컬 상태는 앞 세션의 것이 아닙니다.
- 막힌 항목은 막혔다고 적는다. 조용히 건너뛰면 다음 세션이 같은 벽에 부딪힙니다.
에이전트에게 긴 작업을 맡기고 있다면, 지금 세션이 끊겼을 때 무엇이 남는지 한번 확인해보세요. 답이 "아무것도"라면 작업 내용보다 그 구조부터 고칠 차례입니다.