AI 에이전트 여러 개가 노트북 한 대를 같이 쓰면 무슨 일이 생길까? - 난테 스튜디오 아키텍처 2편
에이전트를 많이 띄울수록 일이 빨라질 줄 알았는데, 부하 평균이 726까지 올라가고 17분짜리 검사가 2시간을 넘겼습니다. 맥북 한 대를 여러 에이전트가 나눠 쓰는 방법을 정리했습니다.
난테 스튜디오의 에이전트는 대부분 병호 님의 맥북 한 대에서 일합니다. 세션 여러 개가 동시에 떠서 각자 이슈를 하나씩 맡는데요. 세션마다 빌드도 하고 테스트도 돌리기 때문에, 노트북이 버텨 주는 만큼만 일이 빨라집니다.
그런데 10월 4일에는 반대 일이 생겼습니다. 한 세션이 백그라운드 에이전트 11개와 에이전트 5개짜리 워크플로를 한꺼번에 띄웠는데, 부하 평균이 726까지 올라갔어요. 코어 수의 60배가 넘는 숫자입니다. 스왑은 가득 찼고, 모든 작업이 하나씩 차례로 돌릴 때보다 느려졌습니다.
지금은 세 가지 장치로 이런 일을 막고 있습니다. 디스크는 작업을 시작하기 전에 미리 잡습니다. 무거운 빌드는 맥북 전체에서 두 개까지만 동시에 돌립니다. 그리고 맥북이 과부하일 때는 에이전트를 새로 띄우지 못하게 합니다. 이번 글에서는 어떤 일들이 있었는지, 그리고 각 장치가 어떻게 동작하는지 공유하려고 합니다.
에이전트들은 무엇을 나눠 쓰고 있을까
먼저 세션 하나가 일하는 방식을 보겠습니다. 세션은 이슈를 하나 받으면 작업 공간을 하나 만듭니다. 작업 공간은 git worktree인데, 필요한 저장소만 받아 온 별도 폴더예요. 코드는 세션마다 따로 갖고 있으니 서로 덮어쓸 일이 없습니다.
하지만 맥북 안에서 따로 가질 수 없는 것들이 있습니다.
- CPU와 메모리: Gradle, Xcode, Flutter, cargo 빌드가 모두 같은 코어를 씁니다.
- 디스크: 작업 공간 하나에 빌드 결과물까지 쌓이면 수 GB가 금방 넘어갑니다.
- Rust 빌드 캐시: 모든 작업 공간이 캐시 폴더 하나를 같이 쓰고, cargo는 이 폴더에 잠금을 하나만 겁니다.
- 시뮬레이터: iOS 시뮬레이터 하나가 백그라운드 프로세스를 100개 가까이 띄웁니다.
세션 하나만 보면 다 합리적인 작업입니다. 빌드가 필요하니 빌드하고, 확인이 필요하니 시뮬레이터를 켭니다. 문제는 이런 세션이 여러 개 겹칠 때 생겼습니다.
어떤 일들이 있었을까
한 달 동안 네 번 크게 막혔습니다. 일어난 순서대로 적어 보겠습니다.
그림 1. 한 달 동안 있었던 네 번의 사고와, 각 사고 뒤에 생긴 장치입니다.
- 9월 12일: Rust 코드를 고친 세션 7개가 거의 동시에 푸시했습니다. 푸시할 때 도는 검사는 원래 17분이면 끝나는데요. 일곱 개가 cargo 잠금 하나를 기다리느라 2시간 넘게 걸렸습니다. 정작 코드 작업은 처음 20분 안에 다 끝나 있었어요.
- 10월 4일: 앞에서 말한 부하 평균 726입니다. 에이전트 16개가 각자 푸시 검사, Gradle, Flutter, 시뮬레이터를 돌렸습니다.
- 10월 8일: 이번에는 저희가 만든 장치가 문제였습니다. 디스크에 224 GiB가 비어 있었는데, 9 GiB짜리 작업이 디스크 부족으로 거절됐어요. 이미 다 받아 둔 작업 공간 160 GiB를 아직 쓸 공간으로 한 번 더 셌기 때문입니다.
- 10월 11일: iOS 시뮬레이터 세 개가 동시에 켜지면서 부하 평균이 276까지 올라갔습니다. 같은 날, 끝나지 않는 명령인
flutter run이 빌드 자리 하나를 1시간 29분 동안 붙잡았고, 그 뒤에 작업 네 개가 줄을 섰습니다.
네 사고 모두 원인이 한 세션 안에 있지 않았습니다. 각 세션은 자기 일을 했을 뿐이고, 합쳐 놓으니 맥북이 감당하지 못한 거죠. 그래서 해결책도 세션 안이 아니라 맥북 전체에 걸어야 했습니다.
디스크는 언제 잡아야 할까
작업 공간을 만드는 스크립트는 저장소를 받기 전에 디스크부터 확인합니다. 계산은 단순합니다.
- 지금 비어 있는 공간을 잽니다.
- 다른 세션이 아직 받고 있는 작업 공간과, 지금 돌고 있는 검사가 쓸 공간을 뺍니다.
- 이번 작업이 받을 저장소 크기를 뺍니다.
- 남는 공간이 60 GiB보다 적으면 작업을 시작하지 않습니다.
10월 8일의 문제는 2번에 있었습니다. 처음에는 끝나지 않은 작업 공간을 전부 "아직 쓸 공간"으로 셌어요. 그런데 이미 다 받아 둔 작업 공간은 비어 있는 공간 숫자에 이미 반영돼 있습니다. 같은 공간을 두 번 뺀 거죠. 지금은 받는 중인 작업 공간만 셉니다. 받기 시작한 지 6시간이 지난 기록은 중간에 멈춘 것으로 보고 빼고요.
그림 2. 작업 공간 하나가 거치는 상태입니다. 디스크 예약을 잡고 있는 상태는 "만드는 중" 하나뿐입니다.
작업 공간마다 기록이 하나씩 남습니다. 어느 세션이 주인인지, 어느 이슈를 맡았는지, 지금 어떤 상태인지가 적혀 있어요. 10월 21일 이후 이 맥북에서 만들어진 기록이 86개이고, 글을 쓰는 지금 24개가 진행 중입니다.
끝난 작업 공간은 정리 명령으로 지웁니다. 지우기 전에 브랜치의 마지막 커밋을 복구용 ref로 남기고, 커밋하지 않은 변경이 있거나 다른 세션 것이면 지우지 않습니다. 디스크를 비우려고 남의 작업을 지우는 일은 없게 한 거예요.
빌드는 왜 두 개까지만 돌릴까
Gradle, Xcode, Flutter, cargo처럼 무거운 작업은 모두 같은 스크립트를 거쳐서 실행합니다. 이 스크립트는 맥북 전체에 빌드 자리를 두 개만 둡니다. 자리가 차 있으면 기다리고, 건너뛰지는 않습니다.
그림 3. 빌드 자리 두 개와 대기열입니다. 대기열은 먼저 온 순서대로만 자리를 받습니다.
단순한 규칙인데, 쓰다 보니 예외가 몇 가지 필요했습니다.
- 먼저 온 순서: 처음에는 자리가 비면 먼저 확인한 작업이 가져갔습니다. 그러면 오래 기다린 작업이 새로 온 작업에 계속 밀릴 수 있어요. 지금은 도착할 때 번호표를 받고, 가장 오래된 번호표만 빈자리를 가져갑니다.
- 안쪽 작업: 푸시 안에서 검사가 돌고, 검사 안에서 다시 빌드가 돕니다. 바깥 작업이 자리를 잡은 상태에서 안쪽 작업이 또 자리를 기다리면, 두 자리가 모두 바깥 작업으로 차 있을 때 영원히 기다리게 됩니다. 그래서 안쪽 작업은 바깥 작업의 자리를 그대로 씁니다.
- 메모리: 자리가 비어도 남은 메모리가 10%보다 적으면 최대 10분까지 기다립니다.
- 끝나지 않는 명령:
flutter run이나 로그를 계속 보여 주는 명령은 자리를 받지 못합니다. 10월 11일처럼 자리를 무한정 붙잡기 때문이에요. 빌드만 자리 안에서 하고, 설치와 실행은 밖에서 합니다. - 가벼운 푸시: 린트만 도는 푸시는 자리를 잡지 않습니다. 빌드가 필요한 검사만 자리를 잡아요.
Rust 빌드는 왜 한 줄로 설까
Rust 쪽은 빌드 자리와 별개로 줄이 하나 더 있습니다. 모든 작업 공간이 빌드 캐시 폴더 하나를 같이 쓰고, cargo는 그 폴더에 잠금을 하나만 걸기 때문입니다. 캐시를 같이 쓰면 두 번째 빌드부터 빨라지지만, 동시에 빌드할 수는 없어요.
9월 12일 이후로 두 가지를 바꿨습니다.
- 잠금을 120초 넘게 기다리면, 누가 잡고 있는지 알려 주고 작업 공간 안의 별도 폴더에서 빌드합니다. 캐시를 못 쓰니 느리지만, 끝없이 기다리지는 않습니다.
- Rust를 고치는 작업 두 개를 동시에 돌리지 않습니다. 웹이나 Kotlin, Swift 작업은 여러 개를 동시에 돌려도 되지만, Rust 작업은 하나씩 차례로 진행합니다.
에이전트를 더 띄워도 될지는 누가 정할까
빌드 자리가 두 개여도, 에이전트를 스무 개 띄우면 대기열만 길어집니다. 그래서 에이전트를 띄우는 순간에도 검사가 하나 있습니다. 에이전트나 워크플로를 시작하려고 하면 훅이 먼저 맥북 상태를 잽니다. 아래 중 하나라도 해당하면 시작을 막습니다.
- 1분 부하 평균이 코어 수의 2배보다 높을 때
- 남은 메모리가 10%보다 적을 때
- 스왑이 85% 넘게 차 있고, 남은 메모리도 25%보다 적을 때
- 켜져 있는 시뮬레이터와 에뮬레이터가 2개보다 많을 때
스왑 조건에 메모리 조건을 붙인 데는 이유가 있습니다. macOS는 메모리 압박이 끝난 뒤에도 스왑을 한동안 잡아 둡니다. 스왑만 보면 멀쩡한 맥북을 과부하로 판단하게 돼요.
이 시리즈를 쓰는 동안에도 이 검사에 걸렸습니다. 조사를 에이전트 여러 개에 나눠 맡기려고 재 보니 부하 평균이 코어 10개에 21이었어요. 기준을 넘었기 때문에 에이전트를 띄우지 않고 혼자서 차례로 쓰고 있습니다.
비슷한 환경에서 에이전트를 여러 개 쓰고 있다면
먼저, 에이전트 수는 할 일 목록이 아니라 남은 자원에 맞추는 게 좋습니다. 띄우기 직전에 부하와 메모리를 재 보면 됩니다. 10월 4일에 확인했듯이, 자원보다 많이 띄우면 전체가 하나씩 돌릴 때보다 느려집니다.
다음으로, 제한은 세션이 아니라 맥북에 거세요. 세션마다 "빌드는 한 번에 하나"라고 정해도, 세션이 다섯 개면 빌드는 다섯 개가 돕니다. 자리 수는 모든 세션이 같이 보는 곳에 둬야 합니다.
마지막으로, 예약을 쓴다면 이미 쓴 공간과 앞으로 쓸 공간을 나눠서 세는지 확인해 보세요. 비어 있는 공간 숫자에 이미 반영된 것을 또 빼면, 디스크가 넉넉한데도 일이 멈춥니다.
마치며
어려웠던 점은 원인을 찾는 일이었습니다. 각 세션의 기록을 보면 다들 할 일을 했을 뿐이라, 한 세션만 봐서는 잘못된 곳이 보이지 않았거든요. 맥북 전체의 부하를 시간순으로 놓고 나서야 겹친 지점이 보였습니다.
정리하면서 알게 된 것도 하나 있습니다. 에이전트를 더 띄우는 것보다 줄을 잘 세우는 쪽이 빨랐습니다. 9월 12일에도 코드 작업은 20분 만에 끝났고, 나머지 두 시간은 서로를 기다리는 데 썼으니까요.
다음 글에서는 에이전트가 스튜디오를 운영할 때 가장 많이 쓰는 도구인 사내 CLI를 다룹니다. 에이전트 여러 개를 한 컴퓨터에서 돌리고 있다면 조금이나마 도움이 되었으면 합니다.
난테 스튜디오 아키텍처 시리즈의 2편입니다. 전체 목록은 1편에 있습니다.