발견 출처 dev.to/발행

Was bash the wrong language for my agent?

내 에이전트에게 bash는 잘못된 언어였을까? — 2,150줄 bash를 Python으로 옮기며 배운 것

한 번에 하나의 작업을 처리하는 소규모 AI 에이전트를 2,150줄의 bash에서 Python으로 하루 만에 포팅한 개발자의 회고입니다. 결론은 단순한 'bash가 틀렸다'가 아니라, 리라이트의 비용은 시작 전에 이미 결정되어 있으며, 표현력 부족이 성능보다 먼저 설계를 지배한다는 분석입니다. 포팅 결과 코드는 40% 줄었지만 총 라인 수는 주석 때문에 오히려 늘었다는 역설적인 수치도 담겨 있습니다.

본문

루틴 작업을 하나씩 골라 계획을 보여주고 승인하면 실행하는 소규모 AI 에이전트를 다루는 글입니다. 내부적으로는 커맨드라인 도구들을 구동하고, 모델을 호출하고, JSON을 재가공해 읽기 쉬운 요약을 출력하는 '글루(glue)' 프로그램입니다. 저자는 이 프로그램을 7개 파일 2,150줄의 bash로 유지하다가 Python으로 포팅했지만, 그렇다고 bash가 처음부터 잘못된 선택이었다고는 보지 않으며, 왜 그런지를 정리하는 것이 이 글의 목적입니다.

'아직 bash인 이유'가 무너진 순간

저자는 이전에도 같은 질문을 검토해 bash 유지를 결정했고, 그 근거를 README의 'Why this is still bash' 섹션으로 남길 만큼 신중했습니다. 프로그램의 90%가 서브프로세스 오케스트레이션이라 bash의 본진이었고, 테스트 없는 2,000줄 리라이트는 동작을 조용히 잃는 표준적인 방법이었으며, 버그 장부상 비용이 컸던 버그들은 어떤 언어로 썼어도 발생했을 설계 오류였기 때문입니다. 저자는 이 추론 자체는 지금도 옳다고 봅니다. 움직인 것은 요구사항이었습니다. '빌드 단계가 없어야 한다'를 엄격한 규칙으로 해석해 'python3는 이미 설치되어 있다'가 핵심 논거였는데, 요구사항이 '규칙이 아니라 시작이 복잡하지 않으면 된다'로 명확해지면서 최선의 논거가 무너졌습니다. 실측 결과 Python과 표준 라이브러리 기동 비용은 89ms, bash는 3ms로 86ms 차이였지만, 이 프로그램은 모델 응답을 30~100초 기다리는 프로그램이었습니다. 저자는 추론은 유효했으나 입력값이 틀렸고, 입력값 검증에는 거의 노력을 들이지 않았다며 그 비율이 뒤집혀 있었다고 반성합니다.

성능이 아니라 표현력의 문제

bash에서 발생한 162회의 fork는 1분의 모델 지연 앞에서는 아무것도 아니었습니다. 진짜 비용은 표현력이었습니다. 프로그램의 모든 구조가 한 줄짜리 jq 표현식에 들어가거나 세 조각으로 쪼개졌고, 작성한 것은 원하는 구조가 아니라 jq가 한 줄로 표현할 수 있는 구조였습니다. 이 비용은 개별 라인에서는 보이지 않고 '고려조차 하지 않게 되는 설계'에서 드러난다고 지적합니다. 예로 변경이 실제 건드린 파일 목록, 에이전트가 직접 쓴 파일 목록, 사전 승인한 파일 목록 사이의 차집합 두 개가 필요했는데, jq로는 여러 번 시도 끝에 겨우 맞췄지만 Python 버전은 첫 시도에 쓸 수 있는 코드였습니다. 또한 'line 567: 1: command not found'로 죽은 실행이 있었는데 원인을 끝내 찾지 못했고, 무관한 이유로 해당 구간을 재작성하면서 사라졌다고 합니다. 사후에 찾을 수 없는 버그는 수정되지 않을 뿐 아니라, 같은 종류의 다음 버그도 못 찾을 것이라는 신호라는 것입니다.

라인 수의 역설 — 코드 40% 감소, 총량은 증가

포팅은 하루 걸렸습니다. bash 2,150줄이 Python 2,258줄로 108줄 늘었지만, 내역을 보면 코드는 1,278줄로 40% 줄었고 주석·docstring이 980줄입니다. 증가분은 '각 안전 점검이 어떤 장애 사례를 막기 위해 존재하는지' 기록한 주석으로, 정작 잃을까 두려워했던 바로 그것이었다고 합니다. 총 라인 수로 평가하면 이 리라이트는 아무것도 달성하지 않은 셈이라고 단언합니다.

리라이트를 싸게 만든 두 개의 경계

저자가 가장 남기고 싶어 한 부분입니다. 첫째, 프롬프트는 문자열이 아니라 파일입니다. 모든 프롬프트가 별도 Markdown 파일에 있고 코드가 {{placeholder}}를 채우는 방식인데, 덕분에 마이그레이션에서 프롬프트 한 단어도 건드리지 않았습니다. 판단 기준과 순서 등 프로그램의 행동을 결정하는 것이 전부 그 파일에 있어서, 언어 교체는 그것들을 조립하는 글루만 바꾼 셈입니다. 둘째, 작업 유형마다 JSON으로 통신하는 별도 실행 파일입니다. argv로 동사를 받고 stdin으로 JSON을 받아 stdout으로 JSON을 출력하는데, bash에는 모듈이 없어 프로세스 경계를 두는 게 변수 오염보다 나았기 때문입니다. 이 경계 덕분에 빅뱅 리라이트가 필요 없었고, 오케스트레이터는 Python인 채 작업 유형은 bash로 두거나 그 반대도 가능했습니다. 아는 한 가장 큰 bash 에이전트가 cat src/*.sh로 조립한 단일 파일 4,700줄에 도달했을 때 깨진 것은 정확성이 아니라 모듈 구조였다는 사례도 덧붙입니다. 두 경계 모두 이식성을 염두에 둔 것이 아니라 당시 불편함을 해결하려 만든 것인데, '리라이트가 쌀지는 시작 전에 결정된다'는 결론으로 이어집니다. 포팅 후에도 프로세스 경계를 유지했는데, Python import로 접으면 JSON 왕복 하나를 아끼겠지만 이 경계야말로 프로젝트 최고의 구조적 결정이라고 평가합니다.

표준 라이브러리만 — 새로운 규칙

shebang과 표준 라이브러리만으로 이전과 똑같이 호출되며 virtualenv도 설치도 필요 없습니다. 다만 Python은 의존성을 부르는 언어라, urllib가 있는데 requests를, 모델 CLI가 스키마를 강제하는데 검증기를, 25줄짜리 파싱에 인자 파서를, 존재하는 출력 계층에 포매팅 라이브러리를 붙이면 '빠르게 시작 가능'이라는 장점이 사라진다고 경계합니다. 그래서 README에 '표준 라이브러리만, 추가하려면 왜 그것으로 안 되는지 먼저 설명할 것'이라는 규칙을 둡니다. 프로그램 전체가 표준 라이브러리 모듈 6개만 필요합니다.

검증과 남은 리스크

포팅 후 모든 경로를 시험했고, 격리된 체크아웃에서 모델이 코드를 쓰고 포매터·vet·빌드·테스트·커밋까지 진행하는 고비용 경로도 push 직전까지 전체 드라이런을 돌렸습니다. 순수 함수 15개 테스트는 통과합니다. 다만 브랜치를 push하고 변경 요청을 여는 두 줄은 실제로 여는 것이므로 실행된 적이 없고, 작업 유형 안의 안전 점검들은 하나씩 수작업으로 옮겼습니다. 옳게 옮겼다고 믿지만 이를 증명할 테스트는 여전히 없다고 솔직하게 적습니다. 결론적으로 bash는 잘못된 선택이 아니었습니다. 2,150줄까지 잘 버텼고 그동안 언어와 싸우는 대신 판단 로직을 바꾸는 데 시간을 썼습니다. bash의 문제는 일을 못 한 게 아니라, 표현력이 어느 순간부터 설계를 대신 결정하기 시작했다는 것이라고 정리합니다.

dev.to 반응

아직 등록된 댓글이 없습니다.

원문: dev.to / 번역·요약: Trawling