클로드가 작성한 sqlite-utils 4.0rc2 (약 149달러)
Simon Willison은 Claude AI를 적극 활용하여 데이터베이스 트랜잭션 처리 과정의 치명적인 버그를 발견하고 수정하는 등 sqlite-utils 4.0rc2의 핵심 코드를 작성했습니다. 개발자는 휴대폰으로 AI 에이전트에게 프롬프트를 지시하는 동안 휴식을 취할 수 있었으며, 최종적으로 약 149달러의 API 비용으로 30개 파일에 달하는 대규모 코드 수정 및 설계 개선을 완료했습니다. 이는 현업 소프트웨어 개발에서 코딩 에이전트가 얼마나 강력하고 실용적인 도구가 되었는지를 보여주는 훌륭한 사례입니다.
Simon Willison의 웹로그 구독 후원: Sonar — 가트너(Gartner)가 기술 부채 관리 도구 부문에서 Sonar를 2026 매직 쿼드런트™ 리더로 선정했습니다. 보고서를 읽고 코드베이스 전체의 기술 부채를 측정하고 해결하는 방법을 알아보세요. 보고서 받기
sqlite-utils 4.0rc2, 주로 Claude Fable이 작성함 (약 $149.25 소요) 2026년 7월 5일
저는 몇 주 전에 sqlite-utils 4.0rc1 릴리스에 대해 글을 썼습니다. Max 구독에서 Claude Fable을 사용할 수 있는 기간이 며칠밖에 남지 않았기 때문에, 이를 활용해 제가 정말 안심할 수 있는 안정적인 4.0 버전을 릴리스할 수 있을지 확인해보기로 했습니다. 저는 시맨틱 버저닝(SemVer)을 준수하려고 노력하며, 하위 호환성이 깨지는 메이저 버전 업데이트는 최대한 드물게 가져가는 것을 선호합니다.
저는 아이폰의 Claude Code for web에서 다음과 같은 프롬프트로 시작했습니다. "안정적인 4.0 릴리스를 배포하기 전 최종 리뷰 - 나중에 수정하면 호환성이 깨지는 breaking change가 될 수 있는 마지막 순간의 문제들을 찾는 것이 매우 중요합니다."
여기 AI가 작성한 최초의 리포트가 있습니다. 저도 아직 마주치지 못했던 심각한 문제들이 있었으며, Fable은 그중 5개를 "릴리스 차단 요소(Release blockers)"로 분류했습니다. 다음은 그중 가장 심각했던 문제입니다:
- delete_where()가 커밋을 하지 않고 연결을 망가뜨림 (데이터 손실) Table.delete_where() (sqlite_utils/db.py:2948)는 래핑 없이 self.db.execute()로만 DELETE를 실행하므로 연결이 in_transaction=True 상태로 남게 됩니다(Table.delete()는 올바르게 래핑하는 db.py:2944와 비교해 보세요). 결과적으로 이후의 모든 atomic() 호출이 세이브포인트 분기를 타게 되어 커밋이 전혀 이루어지지 않습니다. 재현 시나리오: db = sqlite_utils . Database ( "dw.db" ) db [ "t" ] . insert_all ([{ "id" : i } for i in range ( 3 )], pk = "id" ) db [ "t" ] . delete_where ( "id = ?" , [ 0 ]) # 이제 conn.in_transaction이 True가 됨 db [ "t" ] . insert ({ "id" : 50 }) db [ "u" ] . insert ({ "a" : 1 }) db . close ()
다시 열어보면: 행은 [0, 1, 2]입니다 — 삭제 명령, 행 50, 그리고 테이블 u가 모두 사라집니다.
이것은 정말 끔찍한 버그입니다! 이런 버그를 배포하지 않게 되어 정말 기쁩니다. 비록 이 버그가 나중에라도 4.0.1 패치 릴리스로 수정할 수 있는 버그였을지언정, 5.0으로 가야만 하는 설계 결함은 아니었을 것입니다.
총 37개의 프롬프트, 34번의 커밋, 30개의 개별 파일에 걸친 +1,321 / -190 라인의 코드 변경을 거치면서, 우리는 피드백 전체를 차례대로 해결했으며 과정 중에 몇 가지 다른 설계 개선사항도 함께 적용했습니다.
코딩 에이전트의 이상한 점은 에이전트가 새 작업을 처리하는 데 가끔 10~15분 정도 소요되기 때문에, 이런 까다로운 작업일수록 오히려 다른 일을 할 기회가 더 많아진다는 것입니다. 저는 하프문 베이(Half Moon Bay) 7월 4일 퍼레이드를 즐기러 나가서, 가끔씩 확인하며 휴대폰으로 Fable에게 다음 단계를 지시했습니다. 자세한 내용은 PR(Pull Request)과 이 공유된 대화 기록에서 확인할 수 있습니다.
저는 최종 리뷰를 위해 노트북으로 전환했으며, GitHub의 PR 인터페이스를 통해 리뷰를 진행했습니다. 가장 중요한 변경 사항은 트랜잭션 처리와 관련된 것으로, 이는 이전 RC 버전의 핵심적인 새 기능이었습니다. 새로운 RC 버전에는 새로운 트랜잭션 모델에 대한 포괄적인 문서가 포함되어 있으며, 그 서론을 여기에 전체 인용하겠습니다:
데이터베이스에 쓰기를 수행하는 이 라이브러리의 모든 메서드(insert(), upsert(), update(), delete(), delete_where(), transform(), create_table(), create_index(), enable_fts() 등)는 자체 트랜잭션 내부에서 실행되며, 반환되기 전에 커밋됩니다. 즉, 메서드 호출이 완료되는 즉시 변경 사항이 디스크에 저장됩니다: db = Database ( "data.db" ) db . table ( "news" ). insert ({ "headline" : "Dog wins award" })
새 행이 이미 저장됨 - commit() 필요 없음
db.execute()로 실행되는 원시 SQL에도 동일하게 적용됩니다 — 쓰기 명령문은 실행되자마자 커밋됩니다. 사용자는 결코 commit()을 호출할 필요가 없으며, 변경 사항을 저장하기 위해 데이터베이스를 닫을 필요도 없습니다. 트랜잭션에 대해 고려해야 하는 정확히 두 가지 상황은 다음과 같습니다:
- 여러 쓰기 작업을 그룹화하여 모두 성공하거나 모두 실패하게 만들려면 db.atomic()을 사용하세요.
- db.begin()으로 트랜잭션을 직접 관리하는 경우, 이때는 사용자가 커밋(또는 롤백)을 실행하기 전까지는 어떠한 내용도 커밋되지 않습니다.