ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 에이전트 셋을 동시에 풀었더니 서로의 코드를 덮어썼다 — git worktree로 푼 병렬 격리
    IT 2026. 7. 28. 21:00
    에이전트 셋을 동시에 풀었더니 서로의 코드를 덮어썼다 — git worktree로 푼 병렬 격리

    집에 둔 로컬 AI 서버에서 코딩 에이전트를 한 번에 세 개 띄워 일을 나눠 줬다. 하나는 테스트를 맡기고, 하나는 리팩터링을 하고, 하나는 문서를 손보게 했다. 셋이 동시에 달리니 체감 속도는 분명히 빨랐다. 그런데 작업이 끝나고 나서 결과를 합쳐 보니, 한 에이전트가 방금 고친 파일이 다른 에이전트의 옛 버전으로 덮여 있었다. 빨라진 줄 알았는데 사실은 서로의 일을 지우고 있었던 것이다.

    이 글은 그 "서로 덮어쓰는" 문제가 왜 생기는지, 그리고 git worktree(하나의 git 저장소에서 여러 개의 독립된 작업 디렉토리를 만드는 git 기본 기능)로 어떻게 깔끔하게 풀었는지를 정리한 기록이다. 에이전트를 병렬로 굴리는 사람이라면 한 번은 만나는 함정이라 공유해 둔다.

    배경 — 왜 에이전트를 병렬로 돌리고 싶은가

    AI 코딩 에이전트는 한 작업을 처리하는 동안 사람이 기다려야 한다. 작업이 서로 독립적이라면(테스트 작성, 문서 수정, 리팩터링처럼 겹치지 않는 일) 굳이 한 줄로 세워 차례를 기다릴 이유가 없다. 셋을 동시에 풀면 벽시계 시간(wall-clock time, 실제로 흐른 시간)은 대략 셋 중 가장 오래 걸리는 하나만큼으로 줄어든다.

    여기까지는 직관적이고, 실제로도 빠르다. 문제는 "서로 독립적이라고 생각했던" 작업들이 같은 파일을 건드릴 때 드러난다.

    문제 — 한 작업 디렉토리, 여러 손

    git 저장소를 평범하게 clone하면 작업 디렉토리(working tree, 실제 파일들이 풀려 있는 폴더)는 딱 하나다. 에이전트 셋이 모두 이 한 폴더 위에서 일하면, 같은 파일을 동시에 고칠 때 마지막에 저장한 쪽이 이긴다. 앞서 저장한 변경은 흔적도 없이 사라진다. 이것이 파일 경합(race condition, 둘 이상이 같은 자원을 동시에 건드려 결과가 실행 순서에 좌우되는 상황)이다.

    diagram

    다이어그램 설명. 한 폴더 위에서 두 에이전트가 같은 파일을 각자 읽어 고친 뒤 차례로 저장하는 흐름이다. 둘 다 처음 읽은 시점의 파일을 기준으로 작업하기 때문에, 나중에 저장하는 쪽은 상대가 방금 추가한 내용을 모른 채 자기 버전으로 파일 전체를 덮어쓴다. 핵심은 "동시에 썼다"가 아니라 "같은 원본에서 갈라져 나와 합치는 단계가 없다"는 점이다 — 그래서 먼저 저장한 함수 추가본이 통째로 날아간다. 흔히 빠지는 오해는 "에이전트들이 알아서 서로 피해 가겠지"인데, 각 에이전트는 다른 에이전트의 존재 자체를 모르므로 양보란 없다.

    여기서 한 가지 짚어야 한다. git의 commit 단위 충돌과 이건 다르다. git은 서로 다른 commit을 merge할 때 충돌을 감지하고 사람에게 알려 준다. 하지만 지금 문제는 commit 이전, 작업 디렉토리의 날 파일(아직 git에 기록되지 않은 편집 중인 파일) 단계에서 일어난다. 한 폴더 안에서 파일이 그냥 덮여 버리면 git은 "원래 그렇게 저장됐다"고 받아들일 뿐, 사라진 변경을 알려 줄 방법이 없다.

    왜 이게 중요한가: 병렬화로 아낀 시간보다, 사라진 변경을 추적하고 다시 작업하는 비용이 더 크면 병렬화는 손해다. 그래서 "동시에 돌린다"는 결정에는 "서로 안 밟는다"는 보장이 반드시 따라붙어야 한다.

    흔히 떠올리는 회피책과 그 한계

    가장 먼저 떠오르는 건 "그냥 저장소를 세 번 clone하자"다. 폴더가 셋이면 파일이 안 겹치니 맞는 방향이긴 하다. 하지만 full clone은 .git 히스토리 전체를 복제하므로, 큰 저장소에서는 디스크와 시간이 작업 수만큼 곱으로 늘어난다. 게다가 세 clone은 서로 완전히 남남이라, 작업이 끝난 뒤 한 곳으로 변경을 모으려면 remote를 거쳐 push/pull하는 번거로운 단계가 필요하다.

    다른 회피책은 "파일에 잠금(lock)을 걸어 한 번에 하나만 쓰게 하자"다. 이건 사실상 병렬을 직렬로 되돌리는 것이라, 애초에 동시에 돌린 이유를 없앤다.

    회피책 충돌 막힘? 대가
    저장소를 N번 full clone 막힘 히스토리 N배 복제, 작업 후 remote 경유 병합 필요
    파일 단위 잠금(lock) 막힘 사실상 직렬화 — 병렬 이득 소멸
    그냥 한 폴더에서 동시 작업 안 막힘 덮어쓰기로 변경 유실
    git worktree 막힘 작업 디렉토리만 추가 (히스토리 공유)

    표 설명. 네 방식을 "충돌이 막히는가"와 "그 대가가 무엇인가" 두 축으로 비교했다. 핵심은 git worktree만이 충돌을 막으면서도 히스토리를 한 벌만 유지한다는 점이다 — 작업 디렉토리(파일이 풀려 있는 폴더)만 따로 떼어 주고, 그 아래 .git 저장소 본체는 모두가 공유한다.

    방법 — git worktree로 에이전트마다 자기 폴더를 준다

    git worktree는 git에 원래 들어 있는 기능이다. 하나의 저장소(정확히는 하나의 .git 데이터베이스)에 여러 개의 작업 디렉토리를 연결한다. 각 작업 디렉토리는 서로 다른 브랜치를 체크아웃해 독립적으로 파일을 풀어 놓는다. 비유하자면, 책 한 권(히스토리)을 여러 책상(작업 디렉토리) 위에 펼쳐 놓되 각 책상은 다른 페이지(브랜치)를 보고 있는 셈이다. 책 자체는 한 권뿐이라 복제 비용이 거의 없다.

    diagram

    다이어그램 설명. 가운데 저장소 본체 하나에 작업 디렉토리 셋이 매달려 있고, 각 디렉토리는 서로 다른 브랜치를 들고 있다. 에이전트는 자기에게 배정된 폴더 안에서만 파일을 만진다 — 물리적으로 다른 경로에 있는 파일을 편집하므로 같은 파일을 동시에 덮어쓸 일이 구조적으로 없다. 한 폴더에서 충돌하던 앞 절의 그림과 비교하면, 충돌을 "조심해서 피하는" 게 아니라 충돌할 수 있는 공간 자체를 분리해 버렸다는 점이 다르다. 놓치기 쉬운 부분은 히스토리(.git 본체)는 여전히 한 벌이라는 것 — 그래서 한 에이전트가 commit한 작업물을 다른 작업 디렉토리에서 바로 볼 수 있고, 마지막에 합치기도 쉽다.

    명령은 단순하다. 핵심은 git worktree add 한 줄이다.

    # 1단: 에이전트마다 별도 작업 디렉토리 + 별도 브랜치를 만든다
    #   - 첫 인자: 새 작업 디렉토리가 생길 경로 (저장소 바깥 권장)
    #   - -b: 그 디렉토리에서 체크아웃할 새 브랜치 이름
    git worktree add ../wt-tests    -b agent-tests
    git worktree add ../wt-refactor -b agent-refactor
    git worktree add ../wt-docs     -b agent-docs
    
    # 2단: 각 에이전트를 자기 디렉토리에서 실행 (셋이 동시에 달려도 안전)
    #   에이전트 1은 ../wt-tests 안에서만, 2는 ../wt-refactor 안에서만 편집한다
    ( cd ../wt-tests    && run_agent "테스트 채우기" ) &
    ( cd ../wt-refactor && run_agent "리팩터링" ) &
    ( cd ../wt-docs     && run_agent "문서 수정" ) &
    wait   # 세 작업이 모두 끝날 때까지 대기
    
    # 3단: 작업이 끝난 브랜치를 main으로 합친다 (여기서 비로소 git이 충돌을 감지)
    git checkout main
    git merge agent-tests agent-refactor agent-docs
    
    # 4단: 다 쓴 작업 디렉토리는 정리한다
    git worktree remove ../wt-tests
    git worktree remove ../wt-refactor
    git worktree remove ../wt-docs
    

    코드 설명. 이 스크립트는 worktree의 생애주기를 한눈에 보여준다 — 디렉토리 생성, 병렬 실행, 병합, 정리의 네 단계다. 가장 중요한 건 2단의 cd로, 각 에이전트가 자기 디렉토리 안에 갇혀 작업하기 때문에 셋이 동시에 달려도 파일이 겹치지 않는다. 그리고 충돌 감지는 3단의 git merge 단계로 미뤄진다 — 이때는 각자 commit된 정식 변경끼리 합치는 것이라, 같은 줄을 고쳤으면 git이 충돌을 표시해 사람이 판단하게 해 준다(앞 절의 "조용히 덮어쓰기"와 정반대다). 자주 하는 실수는 worktree 경로를 저장소 폴더 안에 두는 것 — 그러면 worktree 파일들이 원본 저장소의 추적 대상으로 섞여 들어가니, 위처럼 ../로 저장소 바깥에 두는 게 안전하다.

    worktree의 생애주기 — 생성에서 정리까지

    diagram

    다이어그램 설명. worktree 하나가 태어나서 사라지기까지의 흐름이다. 디렉토리를 만들어 에이전트가 그 안에서 일하고, 끝나면 변경 유무를 확인해 합칠 건 합친 뒤 디렉토리를 지운다. 여기서 중요한 분기는 "변경이 남았나?"다 — 에이전트가 아무것도 바꾸지 않았다면(예: 작업이 불필요했거나 빈손으로 끝났다면) 그 작업 디렉토리는 그냥 지워도 잃을 게 없다. 즉 worktree는 가볍게 만들고 가볍게 버리는 일회용 작업 공간으로 쓰기에 적합하다. 함정은 정리를 깜빡하는 것 — 작업이 끝난 worktree를 안 지우면 디스크에 폴더가 계속 쌓이므로, 위 흐름의 마지막 정리 단계를 자동화해 두는 게 좋다.

    언제 worktree를 써야 하나 — 결정 트리

    worktree가 공짜는 아니다. 작업 디렉토리를 만들 때 그 브랜치의 파일들을 디스크에 풀어 놓아야 하므로, 약간의 시간과 디스크 공간을 쓴다. 그러니 모든 병렬 작업에 무조건 씌우는 건 과하다. 판단 기준은 단 하나, 병렬로 도는 작업들이 같은 파일을 쓰기까지 하느냐다.

    diagram

    다이어그램 설명. worktree를 쓸지 말지를 위에서 아래로 따라 내려가며 판단하는 트리다. 순차로 도는 작업은 애초에 동시에 같은 파일을 만질 수 없으니 필요 없고, 병렬이라도 파일을 읽기만 하는 작업(코드 분석, 요약, 검색 같은)은 서로의 파일을 건드리지 않으니 worktree의 셋업 비용만 헛되이 든다. worktree가 값을 하는 건 마지막 경로 하나뿐 — 여러 작업이 파일을 쓰면서 같은 파일을 건드릴 가능성이 있을 때다. 이 경우에만 "디렉토리 만드는 비용"이 "변경이 사라져 다시 작업하는 비용"보다 확실히 싸다. 흔한 과용은 읽기 전용 병렬 작업에까지 worktree를 씌우는 것인데, 그건 얻는 안전이 없는데 비용만 무는 셈이다.

    전체 그림 — 병렬 작업 파이프라인

    diagram

    다이어그램 설명. 분할부터 결과까지 병렬 파이프라인 전체를 한 줄로 꿴 그림이다. 작업을 N개로 쪼개 각자 격리된 디렉토리에서 동시에 돌리고, commit으로 변경을 정식 기록한 뒤, 마지막에 한 브랜치로 모으고 디렉토리를 청소한다. 이 구조의 핵심 가치는 "위험한 동시 쓰기"를 격리된 디렉토리로 분산하고, "안전한 충돌 검사"를 맨 끝의 합치는 단계 한 곳으로 모았다는 데 있다 — 위험은 흩고 검사는 모은 셈이다. 그래서 병렬의 속도 이득은 누리면서도, 변경이 조용히 사라지는 사고는 막는다.

    효과와 트레이드오프

    worktree로 바꾼 뒤, 에이전트 셋을 동시에 돌려도 한 에이전트의 변경이 다른 에이전트에게 덮이는 일은 사라졌다. 같은 파일을 정말로 둘이 고친 경우는 맨 끝 merge 단계에서 git이 충돌로 잡아 줘서, 모르고 유실되는 대신 내가 보고 판단할 수 있게 됐다. 병렬화의 속도 이득은 그대로 가져가면서, 그 이득을 깎아먹던 "재작업 비용"이 빠진 것이다.

    대가도 분명하다. worktree마다 디렉토리를 만들고 파일을 푸는 데 시간과 디스크가 든다. 작업이 짧고 가벼우면 이 셋업 비용이 작업 자체보다 클 수도 있다. 그리고 작업이 끝난 디렉토리를 제때 정리하지 않으면 폴더가 쌓인다. 그래서 결론은 균형의 문제다 — 여러 에이전트가 파일을 동시에 수정하고 같은 파일을 건드릴 위험이 있을 때만 worktree를 쓰고, 읽기만 하거나 겹칠 일이 없는 병렬 작업에는 쓰지 않는다. 도구가 좋다고 모든 곳에 바르는 게 아니라, 그 도구가 막아 주는 사고의 비용이 도구의 비용보다 클 때 꺼내 드는 것이다.

    git worktree는 새로 배울 라이브러리가 아니라 git에 처음부터 있던 기능이다. 에이전트를 여러 개 병렬로 굴리기 시작했다면, 그 순간 한 번쯤 떠올려 볼 만한 가장 값싼 안전장치다.


    이 글은 생성형 AI의 도움을 받아 작성되었습니다. 원본 자료를 기반으로 AI가 초안을 생성하고, 작성자가 검토·편집하였습니다.

Designed by Tistory.