AI 에이전트 여러 개를 한 레포에 풀어놨더니 생긴 일
에이전트 세션을 병렬로 띄우면 생산성이 배가 될 줄 알았다. 실제로는 브랜치 탈취, 남의 브랜치에 얹힌 커밋, reflog로 건진 고아 커밋, 같은 기능의 이중 구현까지 일주일 사이에 실사고가 줄줄이 터졌다. 한 체크아웃을 공유하는 병렬 에이전트가 만드는 사고의 유형학과, 그걸 겪으며 세운 규칙들.
핵심 요약
병렬 에이전트 세션의 진짜 적은 모델의 실력이 아니라 공유된 작업 디렉토리다. HEAD는 모두의 전역 변수라서, 한 세션의 브랜치 전환이 다른 세션의 커밋을 엉뚱한 곳에 얹는다. 일주일간 겪은 사고 유형 네 가지와 복구기, 그리고 워크트리 격리·커밋 직전 브랜치 재확인·검증 게이트 스코프 축소라는 세 가지 방어선.
목차
AI 에이전트와 일하다 보면 자연스럽게 욕심이 생긴다. 세션 하나가 버그를 고치는 동안 다른 세션은 리팩토링을 하고, 또 하나는 이슈를 조사하면 되지 않나. 모델은 여러 개 띄울 수 있으니 생산성도 그만큼 늘어날 거라고.
나도 그렇게 시작했다. 그리고 일주일 만에 알게 됐다. 병렬 에이전트의 진짜 적은 모델의 실력이 아니라, 걔네가 공유하는 작업 디렉토리라는 걸.
HEAD는 전역 변수다
원인은 한 문장으로 요약된다. git 체크아웃 하나를 여러 세션이 공유하면, 현재 브랜치라는 상태가 모두의 전역 변수가 된다.
사람 둘이 한 컴퓨터로 동시에 작업하는 상황을 상상하면 이상하다는 게 바로 보이는데, 에이전트를 띄울 때는 이 생각을 못 했다. 터미널 탭마다 세션이 하나씩 떠 있으니 서로 격리된 것처럼 보인다. 하지만 파일 시스템은 하나고, HEAD도 하나다. 한 세션이 git checkout을 하는 순간 나머지 모든 세션의 발밑이 바뀐다.
그 일주일 동안 터진 사고들은 유형이 뚜렷했다.
브랜치 탈취. 세션 A가 토픽 브랜치에서 작업하는 중에 세션 B가 자기 작업을 하려고 브랜치를 전환했다. A는 그 사실을 모른 채 커밋했고, 커밋은 B의 브랜치 위에 얹혔다. 반대 방향도 있었다. A가 커밋하려는 순간 브랜치가 develop으로 바뀌어 있어서, 보호 브랜치 직접 커밋을 막는 훅이 차단해준 덕에 사고를 면했다. 훅이 없었으면 그대로 들어갔다.
고아 커밋. 세션 A의 토픽 브랜치를 세션 B가 정리 작업 중에 삭제했다. A의 커밋은 어느 브랜치에도 속하지 않는 고아가 됐고, reflog를 뒤져 커밋 해시를 찾아 cherry-pick으로 회수했다. 건졌으니 다행이지, reflog가 만료됐거나 못 찾았으면 작업이 통째로 증발할 뻔했다.
스테이징 오염. 세션 A가 커밋을 만드는 시점에, 세션 B가 스테이징해둔 파일 삭제가 스테이징 영역에 같이 올라와 있었다. 그대로 커밋했으면 A의 커밋에 B의 삭제가 섞여 들어갔다. diff를 훑다가 낯선 변경이 보여서 걸러냈는데, 에이전트가 커밋 직전에 diff를 안 봤다면 몰랐을 거다.
이중 구현. 제일 허무한 유형. 세션 두 개가 서로의 존재를 모른 채 같은 기능을 각자 구현했다. 둘 다 성실하게 잘 만들었고, 한쪽은 통째로 버려야 했다. 병렬화로 아낀 시간을 그대로 반납한 셈이다.
검증 게이트까지 서로 밟는다
파일과 브랜치만 문제가 아니었다. 우리 하네스에는 세션이 끝나기 전에 레포 전체의 정적 분석과 테스트를 돌려서, 깨진 상태로는 작업을 못 마치게 하는 게이트가 있다. 단독 세션에서는 훌륭한 장치다.
병렬 세션에서는 이게 서로의 발목을 잡는 덫이 됐다. 세션 A는 문서만 만졌는데, 세션 B가 한창 고치는 중인 파일의 컴파일 에러 때문에 A의 게이트가 실패한다. A는 "내 변경이 아니다"를 증명하느라 턴을 쓰고, 심한 날은 B가 정리할 때까지 30분을 대기했다. 최악의 사건은 어떤 세션이 게이트를 통과하려고 다른 세션이 작업 중인 코드를 고쳐버린 것이다. 게이트가 오히려 남의 작업에 손대게 만든 유인이 됐다.
검사 자체가 잘못된 게 아니다. 검사의 스코프가 "레포 전체"라는 게, 레포가 더 이상 한 명의 것이 아니게 된 순간 잘못이 됐다.
세 가지 방어선
사고를 유형별로 겪고 나서 방어선을 세 겹으로 세웠다.
- 워크트리 격리를 기본값으로.
git worktree로 세션마다 독립된 작업 디렉토리를 주면 HEAD가 더 이상 공유 변수가 아니다. 브랜치 탈취, 고아 커밋, 스테이징 오염이 원천적으로 사라진다. 위의 사고 유형 네 개 중 세 개가 이 한 수로 없어진다. 다만 공짜는 아니다. 모노레포라면 워크트리마다 의존성 설치와 코드 생성을 다시 해야 하고, git-crypt처럼 워크트리와 궁합이 나쁜 도구를 쓰는 레포에서는 강제하기 어렵다. 우리도 레포 하나는 이 제약 때문에 공유 체크아웃을 유지하고 있고, 그래서 나머지 두 방어선이 필요하다. - 커밋 직전에 브랜치를 다시 확인한다. 세션이 작업을 시작할 때 "나는 이 브랜치에서 일한다"를 기록해두고, 커밋 직전에 현재 HEAD와 대조한다. 다르면 커밋을 멈추고 상황부터 파악한다. 규칙은 시시할 만큼 단순한데, 브랜치 탈취 사고는 전부 "커밋하는 순간의 HEAD가 내가 아는 HEAD가 아니었다"에서 나왔다. 사람이라면 프롬프트에 브랜치명이 떠서 눈치채겠지만, 에이전트는 명시적으로 시키지 않으면 확인하지 않는다.
- 검증 게이트를 내 변경으로 스코프한다. 세션 종료 게이트가 검사할 범위를 레포 전체가 아니라 그 세션이 실제로 수정한 파일로 좁힌다. 남의 WIP에 내 종료가 막히는 일도, 게이트를 통과하려고 남의 코드에 손대는 유인도 사라진다. 레포 전체의 건강은 어차피 CI가 커밋된 상태를 기준으로 다시 본다. 세션 게이트까지 전체를 볼 필요가 없었다.
여기에 운영 규칙 하나를 얹었다. 세션을 띄우기 전에 지금 열려 있는 브랜치와 PR을 훑어서 겹치는 스코프가 있는지 본다. 이중 구현은 git이 막아줄 수 있는 문제가 아니라 배차의 문제라서, 도구가 아니라 습관으로 막는 수밖에 없었다.
동시성 문제는 데이터베이스에만 있는 게 아니었다
정리하고 보니 익숙한 그림이다. 공유 자원, 락 없는 동시 접근, 레이스 컨디션, 그리고 격리 수준. 우리가 데이터베이스와 멀티스레드 코드에서 수십 년 배운 바로 그 문제가 작업 디렉토리 위에서 재연된 거다.
에이전트 하나와 일할 때 이 문제는 존재하지 않는다. 사람과 에이전트가 번갈아 일할 때도 사람이 암묵적으로 조율자 역할을 한다. 문제는 에이전트가 여럿이 되고 사람이 조율을 놓는 순간 시작된다. 그 시점부터 작업 디렉토리는 동시성 제어가 필요한 공유 자원이고, 격리 없이 돌리면 데이터베이스가 그랬듯 반드시 데이터를 잃는다.
병렬 에이전트를 포기하라는 얘기가 아니다. 지금도 세션 여러 개를 매일 띄운다. 달라진 건 하나다. 이제는 걔네를 같은 방에 몰아넣지 않는다.
관련 글
- 💻 개발
AI가 고친 코드를 다른 AI에게 반증시키는 파이프라인
같은 모델에게 자기 코드를 리뷰시키면 자기가 낸 결론을 다시 승인한다. 거의 모든 코드를 AI와 함께 쓰는 1인 개발자가, 한 AI의 작업을 다른 AI에게 '반박해보라'고 시키는 교차검증 루프를 상시 파이프라인으로 굳힌 이야기. 셀프 리뷰가 놓친 버그를 세 라운드 연속으로 잡아낸 실제 사례와 프롬프트 설계.
- 💻 개발
AI가 "다 됐어요"라고 할 때, 그걸 믿지 않는 장치
AI 코딩 에이전트는 일을 안 끝내고도 끝났다고 말한다. 테스트를 안 돌리고 통과했다 하고, 재현도 안 해보고 고쳤다 한다. 거의 모든 코드를 AI와 함께 쓰는 1인 개발자가, 완료를 말이 아니라 증거로만 인정하게 만든 검증 게이트 이야기.
- 💻 개발
에이전트의 "완료"를 믿지 않기로 했다 - verify.sh 게이트, prove-it
AI 코딩 에이전트는 테스트를 돌리지 않고도 "테스트 통과"라고 보고한다. 거짓말이 아니라 구조 문제다. 보고를 검사로 바꾸는 오픈소스 게이트 prove-it을 만들면서 배운 것들 - 그리고 직접 짠 다섯 줄 훅이 조용히 실패하는 네 가지 방식.