
내 AI 에이전트들이 13분짜리 작업을 위해 4일을 보냈다. 비용은 내가 다 냈다.
요약
SQLite에서 PostgreSQL로의 데이터 마이그레이션을 위해 AI 에이전트를 활용했으나, 13분의 작업을 위해 4일간 6만 줄 이상의 코드를 생성하며 막대한 비용과 시간이 소요된 사례를 다룹니다.
핵심 포인트
- AI 에이전트의 과도한 코드 생성으로 인한 비용 및 시간 낭비 문제
- 데이터 마이그레이션을 위한 복잡한 인프라 및 안정성 코드 자동 생성
- 실제 데이터 이동 시간(13분) 대비 준비 과정(4일)의 극심한 불균형
- AI 에이전트 활용 시 발생하는 제어력 상실과 비용 관리의 중요성
AI가 4일 내내 나를 초보자처럼 다루었다.
데이터 볼륨이 감당할 수 없을 정도로 커지고 있어서 앱을 SQLite에서 PostgreSQL로 옮겨야 했다. 새로운 비동기 추상화 (async abstraction), 라이터 (writers) 재작성, 동시성 (concurrency), 텔레메트리 (telemetry), 점진적 마이그레이션 (progressive migration), 100명의 사용자를 대상으로 한 부하 테스트 (load testing), PITR을 포함한 백업, 자동 전환 (automated cutover).
그 과정에서 크래시 (crash) 상황에서도 살아남는 재개 가능한 오케스트레이터 (resumable orchestrator), 내구성이 있는 단계별 저널 (durable per-phase journal), 그리고 10개의 서명된 Ed25519 증명 (proofs)을 포함하여 7가지 사항이 더 나타났다. 단 1바이트도 옮기기 전에 60,000줄 이상의 코드와 테스트, 문서가 작성되었다. 겨우 6.47 GB를 옮기기 위해서 말이다.
모든 추가 사항에는 정당한 이유가 있었고, 그 모든 이유들은 각각 충분히 타당했다. 마이그레이션 보고서의 무결성을 보장하는 서명 체인 (signature chain)은 거절할 수 없다. SIGKILL 상황에서도 살아남는 오케스트레이터도 마찬가지다. 그래서 당신은 '예'라고 답하고, 또 '예'라고 답하며, 다시 '예'라고 답하게 된다. 그리고 그것들이 하나씩 완성되어 나타날 때는 전혀 미친 짓처럼 보이지 않는다.
청구서: 4일, 자정을 넘긴 전환 작업, 운영 환경에서 발견된 버그들, 그리고 7월 29일에는 요청하지도 않았던 서명 체인의 5,433줄을 삭제하는 커밋 (commit)이 발생했다. 또한 작업 중간에 다른 AI 구독을 추가로 결제해야 했는데, 이 녀석이 내 Claude 20배 요금제를 아무것도 아닌 것처럼 써버렸기 때문이다. 그 4일 동안의 비용을 지불한 사람은 아무도 없었다.
이 모든 과정이 고작 13분 걸린 마이그레이션을 위한 것이었다....
마이그레이션 자체는 13분이 걸렸다
문제의 실제 규모는 다음과 같다: 2,120,930개의 제품, 39개 테이블에 분산된 1,954만 개의 행 (rows), 디스크 상의 6.47 GB.
이것은 작은 규모다. PostgreSQL 입장에서는 반올림 오차 수준이며, 이 점을 명확히 말하고 싶다. 왜냐하면 매일 데이터베이스를 다루지 않는다면 1,900만 개의 행은 인상적으로 들릴 수 있기 때문이다. 노트북으로도 처리 가능한 수준이다.
측정된 소요 시간 순서:
- 검증된 스냅샷 (Verified snapshot): 20분 40초
- 파일럿에서 25만 개 슬라이스, 그리고 전체 카탈로그로 이어지는 전체 캐스케이드 (Full cascade): 1시간 33분
- 전체 카탈로그 COPY 작업 자체: 13분
- 스테이징에서 운영 환경으로의 최종 관문: 7분, 44개의 애플리케이션 레벨 작업 검증, 실패 0건
이제 실제로 고통스러운 타임라인입니다. 작업은 7월 26일 11:04에 시작되었습니다. 7월 28일 12:24까지도 전체 카탈로그 임포트(full-catalog import)는 시작되지 않았고, 라이브 게이트(live gates)는 아직 존재하지도 않았습니다. 첫 번째 실제 연쇄 작업(cascades)은 28일 19:00경에 실행되었습니다. 게이트는 29일 00:04에 녹색(green)으로 변했습니다. 컷오버(Cutover)는 00:11에 이루어졌습니다.
그 61시간 중 대부분은 첫 번째 바이트가 이동하기도 전에 흘러가 버렸습니다.
여기에는 명백한 이의 제기가 있을 것이며, 유능한 독자라면 벌써 준비가 되어 있을 것입니다. 13분짜리 COPY 작업과 4일짜리 프로젝트를 비교하는 것은 서로 다른 두 가지를 섞는 것이며, 그 혼합 자체가 모든 문제입니다. 애플리케이션을 PostgreSQL 호환(PostgreSQL-compatible)으로 만드는 것은 소프트웨어 작업이며 정당한 소요 시간이 있습니다. 데이터를 마이그레이션(Migrating)하는 것은 짧고 측정 가능한 런북(runbook)입니다. 이 두 가지가 하나의 종료 기준을 가진 하나의 프로젝트로 묶이면서, 두 번째 작업이 어느 단계에 있는지 말할 방법이 사라져 버렸습니다.
통제 변수(Control number): 이제 런타임(runtime)이 호환되므로, 이 마이그레이션을 다시 수행한다면 2~3시간 정도가 소요될 것입니다.
6GB를 옮기기 위한 60,000줄의 코드
마이그레이션 빙산: 13분의 이동 vs 61시간의 구축
범위(scope)는 2일 차쯤 어디에선가 데이터베이스 마이그레이션이 아니게 되었습니다. 그것은 1개의 작업으로 배포되는 8개의 별도 소프트웨어 산출물(deliverables)이 되었고, 나중에는 15개가 되었습니다.
저와 함께 이 7가지 추가 사항을 살펴보십시오. 여기서 여러분은 제가 바보였는지 아닌지를 결정하게 될 것입니다.
- SQLite 용량 경계(capacity boundary): 앱이 기존 엔진의 한계를 넘어섰을 때를 알 수 있도록 함
- 라이터(writers)에 대한 정적 분석기(static analyzer): 아직 변환되지 않은 라이터를 잡아내기 위함
- 재개 가능한 오케스트레이터(resumable orchestrator): 3시간째에 충돌(crash)이 발생하면 처음부터 다시 시작해야 하고 하룻밤을 허비하게 되기 때문
- 10개의 Ed25519 증명(proofs): 마이그레이션 보고서가 사후에 몰래 수정될 수 없도록 함
- 내구성이 있는 단계별 저널(Durable per-phase journals)
- PITR(Point-In-Time Recovery) 복구가 가능한 SIGKILL 시나리오
- 모든 작업에 연결된 증거 수집기(Evidence collectors)
그 목록을 다시 읽어보며, 당신이라면 거절했을 법한 항목을 찾아보세요. 저 역시 찾을 수 없었습니다.
노력의 분배(effort split)가 여전히 저를 괴롭히는 부분입니다. 증거 체인(proof chain)의 무게는 마이그레이션 메커니즘 자체만큼이나 무거웠고, 실제로 런타임(runtime)에 전달되는 유일한 구성 요소인 애플리케이션 어댑터(application adapter)보다 거의 3배나 더 컸습니다. 실행되는 것을 실행하는 데 소비된 시간보다, 실행되는 것을 감시하는 것을 감시하는 데 소비된 시간이 매 시간마다 더 많았습니다.
그 위에 체인화된 증명(chained attestations), go-token, 그리고 코드 트리의 다이제스트(digest)와 자체 런처의 SHA-256을 검증하는 루트 전용 엔트리포인트(root-only entrypoint)가 얹혀 있었습니다. 전체 시스템은 레이저 격지가 설치된 포트 녹스(Fort Knox, 미국 국립 금고) 같았지만, 정작 지키고 있는 것은 비스킷 통이었습니다.
어떤 키도 프로비저닝(provisioned)되지 않았고, 아무것도 커미션(commissioned)되지 않았습니다. 커밋(Commit) 7c77ca7은 전환(cutover) 바로 다음 날 5,433줄의 코드를 삭제했습니다. 😬
그러니 규모(sizing)를 있는 그대로 명명해 봅시다. 이것은 단 1명, 즉 저에 의해 운영되는 200만 행 규모의 데이터베이스에서 마이그레이션 보고서의 내부 위조를 방지하기 위해 설계된 장치입니다.
위조자와 감사자(auditor)가 동일 인물이었던 것이죠.
(/docs 어딘가에는 현재 PHASE_4_EVIDENCE_PROTOCOL.md와 같은 이름을 가진 14개의 마크다운(markdown) 파일이 있습니다. 제가 그것들을 생성한 커밋을 작성했기에 존재한다는 것을 압니다. 저는 단 한 번도 그것들을 열어본 적이 없습니다. 앞으로도 열어보지 않을 것입니다.)
문제는 카테고리가 아니라 규모입니다. 10배 더 큰 데이터베이스에서 여러 운영자가 있거나, 사람이 없는 상태에서 재실행(replayed)되어야 하는 프로세스라면, 이 장치의 상당 부분은 그 존재 가치를 인정받을 것입니다. 서명된 증명(Signed proofs)과 복구 저널(recovery journals)은 실제 맥락(real contexts)에서 필요하기 때문에 존재하는 것입니다. 이번 사례는 그렇지 않았습니다.
리뷰에는 자연스러운 중단 조건이 없다
매 리뷰 라운드마다 기대되는 안전 수준이 소리 없이 재정의되었습니다. 배치(batch) 작업이 통과(green)되면 새로운 리뷰가 트리거되었고, 이는 새로운 조건을 발견했으며, 다시 새로운 수정 사항과 새로운 사이클을 열었습니다. 모든 '그린(green)' 배치는 두 번째 체력 바를 가진 보스(boss)와 같았습니다. 체력 바가 올라간 것은 상황이 악화되었기 때문이 아니라, 리뷰가 수행되었기 때문이었습니다.
이것은 우연이 아니라 구조적인 문제입니다. 충분히 큰 시스템에서 약점을 찾도록 임무를 부여받은 에이전트(agent)는 반드시 약점을 찾아낼 것입니다. 왜냐하면 약점은 항상 존재하기 때문입니다. 처리되지 않은 또 다른 예외 상황(edge case), 더 타이트하게 조절할 수 있는 또 다른 타임아웃(timeout), 아직 발견되지 않은 또 다른 실패 모드(failure mode)가 항상 존재합니다. "이 정도면 충분하다"라는 판결은 코드의 관찰 가능한 속성이 아닙니다. 그것은 남겨진 리스크(risk)와 작업을 계속하는 데 드는 비용 사이의 절충(trade)이며, 그 절충은 그 결과를 책임지는 사람의 몫입니다. 에이전트는 그러한 정보에 접근할 수 없습니다. 에이전트는 지연이 나에게 어떤 비용을 초래하는지 모르고, 데이터가 얼마만큼의 가치가 있는지 모르며, 이 일을 하는 사람이 나뿐이고 4일째 되는 날에는 비용이 발생한다는 사실도 모릅니다. 따라서 당신이 그 절충안을 고정된 기준(frozen criterion)으로 전달하지 않는다면, 에이전트는 자신이 할 줄 아는 일, 즉 계속해서 찾는 일을 수행합니다. 실패는 엄격함(rigor)의 부족이 아니라, 감사(audit) 및 수정(fix) 루프에서 발생했습니다. 엄격함 자체가 문제였습니다.
앱을 온라인에 올리기 전에 에이전트에게 리뷰를 요청하면 똑같은 일이 발생합니다. 에이전트는 무언가를 찾아냅니다. 항상 찾아내며, "그만해, 이 정도면 좋아"라는 판결은 결코 내리지 않습니다.
2026년 7월 24일, Anthropic은 Claude Code 팀이 Opus 5 및 Fable 5 세대의 시스템 프롬프트(system prompt)를 80% 이상 제거했음에도 불구하고, 코딩 평가(coding evals)에서 측정 가능한 손실이 없었던 이유를 발표했습니다. 규모는 다르지만 동일한 현상입니다. 보호 계층(protective layer)은 항상 특정 사건에 대한 반응으로 작성되며, 그 사건이 잊히고 나면 프로세스 내에서 다시 읽기를 예약하는 장치는 아무것도 없습니다. 계층은 쌓여만 가고, 각 계층은 추가된 당일에는 모두 정당화될 수 있지만, 지침(instructions)을 쌓아 올리는 비용은 누군가가 마침내 그 수를 세기 시작할 때 비로소 눈에 보이게 됩니다.
저는 단일 설정 파일 내에서 지침을 쌓는 것이 실제로 어떤 비용을 초래하는지를 살펴보았을 때, 훨씬 작은 규모에서 동일한 문제를 겪었습니다.
현재 Dex Horthy의 "Why Software Factories Fail"에서 가장 날카롭게 제기된 반론이 돌고 있습니다. 즉, 엔지니어링을 활용하는 것(harness engineering)만으로는 충분하지 않다는 것입니다. 왜냐하면 모델 자체가 시간이 지남에 따라 코드베이스를 저하시키며, 루프(loop) 주변에 아무리 많은 도구를 둘러싸더라도 루프를 구성하는 요소 자체의 결함은 고칠 수 없기 때문입니다. 그의 해답은 사전에 더 철저한 계획을 세우는 것입니다. 이는 타당한 지적이며 제가 목격한 많은 실패 사례를 포괄합니다. 하지만 제 생각에 이 사례는 그 설명만으로는 부족합니다. 계획은 완료되었고, 아키텍처(architecture)는 유지되었으며, 부족했던 것은 루프가 언제 종료되어야 하는지를 알려주는 숫자였습니다.
2개의 에이전트가 루프를 놓쳤다
작업을 주도하던 에이전트는 L8/L9 단계에서 멈췄습니다.
정확히 그 순간, 보드(board)에는 남은 작업이 0개의 P0, 2개의 P1이었고, 타겟 테스트 99개는 통과(green) 상태였으며, TypeScript도 통과(green) 상태였습니다. 리스크 종료 결정을 위한 모든 조건이 충족되었고 문서화되었습니다. 하지만 에이전트는 마이그레이션(migration)을 실행하는 대신 사이클을 재시작했습니다.
저는 작업이 진행되도록 여러 번 요청했습니다. 하지만 진행되지 않았습니다. 마이그레이션이 점점 더 많은 시간을 잡아먹는 것을 보고, 저는 별도의 세션에서 Claude Fable 5를 열어 무슨 일이 일어나고 있는지 살펴보라고 요청했습니다.
Fable 역시 감사 루프(audit loop)를 찾아내지 못했습니다.
2개의 에이전트가 이 프로젝트를 살펴보았지만, 그들 중 누구도 문제를 지적하지 않았습니다. 각 에이전트는 질문이 던져진 시점의 작업 상태만을 검토하고 있었을 뿐, 그 상태를 만들어내는 프로세스의 형태를 보고 있지 않았습니다. 이는 악의가 빠진 HAL 9000의 답변과 같습니다. "미션은 정상적으로 진행 중이며, 모든 시스템은 정상입니다. 다음 검증 라운드를 위해 대기해 주십시오."
결국 제가 직접 모든 것을 중단시켰습니다.
저는 이것을 하나의 방법론으로 미화하려는 것이 아닙니다. 저는 루프를 진단한 것이 아닙니다. 저는 화가 났습니다.
평정심을 잃는 것은 절차(procedure)가 아니라 증상(symptom)입니다. 또한 제가 이제는 제 인내심을 게이트(gate)로 믿는 대신, 에이전트가 시작하기 전에 동결하는 계약(the contract I freeze before the agent starts)을 작성하는 이유이기도 합니다.
실제 데이터는 2일 늦게 나타났다
전환(cutover) 당일 저녁, 약 한 시간 사이에 7개의 결함(defects)이 드러났습니다. 이 결함들은 모두 증명 체인(proof chain)의 한 줄도 존재하기 전인 7월 26일, 실제 스냅샷(snapshot) 상에서 찾아낼 수 있었던 것들입니다:
- 새로운 SQLite 용량 경계(capacity boundary)로 인해 작동이 중단된 export 및 compare CLI
- 스키마(schema)에 남아 있는 잔류 Litestream 테이블들
- 2개의 고아 외래 키(orphaned foreign keys)
- 잘못된 세션으로 인증을 시도하는 comparator
- 카테고리가 없는 3개의 제품
- 너무 짧게 설정된 COPY 및 체크섬(checksum) 타임아웃
- 비교(compare) 도중에 발생하는 연결 재사용(connection recycling)
대신 이 문제들은 대부분 합성 테스트(synthetic testing)를 진행한 2일이 지난 후, 자정에 나타났습니다.
7개 중 4개는 동일한 버그가 4번 반복된 것이었으며, 저는 4번 모두 이를 놓쳤습니다. psql COPY 프로세스의 타임아웃은 13분 동안 실행되는 COPY 작업에 대해 120초로 설정되어 있었습니다. queryTimeoutMs는 60초로 제한되어 있었는데, 이는 패리티 체크섬(parity checksums)을 수행하기에는 유지 불가능한 수치였습니다. maxLifetimeSeconds는 300초로 설정되어 비교 도중에 연결을 재사용했습니다. 그리고 전체 카탈로그(full-catalog) 카운트에는 어차피 export가 선택하고 있던 제품들이 제외되어 있었습니다.
이 모든 경계값(bounds)은 각 단계가 13ms에서 646ms 사이로 측정되는 축소된 데이터셋(reduced datasets)을 기준으로 조정된 것이었습니다.
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기