목록으로
5 분 소요 2026

LLM이 작성한 57만 줄의 Rust 코드는 컴파일됐습니다. SQLite보다 20,171배 느렸습니다.

LLM이 작성한 Rust SQLite 재구현체를 벤치마킹한 결과를 살펴봅니다. 올바르게 보이는 코드와 실제로 올바른 코드 사이의 간극이 다섯 자릿수 차이로 나타났습니다.

LLM이 전부 작성한 SQLite Rust 재구현체가 최근 벤치마킹됐습니다. 코드는 컴파일됐고, 테스트도 통과했습니다. 코드는 깔끔하고 구조적으로 잘 정리된 관용적 Rust였습니다. 그런데 기본적인 primary key 조회 하나에서 SQLite보다 20,171배 느린 결과가 나왔습니다.

이 숫자 앞에서 멈칫했습니다. LLM이 생성한 코드가 느리다는 사실이 놀라워서가 아니라, 그 느림이 어디서 비롯됐는지가 충격이었습니다. 컴파일러나 테스트 스위트가 잡아낼 수 있는 종류의 잘못이 코드에는 없었습니다. B-tree는 올바르게 구현됐습니다. query planner도 존재했습니다. 스토리지 엔진도 작동했습니다. 각 부분은 개별적으로 보면 모두 타당했습니다. 그런데 시스템 전체는 거의 사용 불가능한 수준이었습니다.

벤치마크 분석과 소스 코드를 직접 읽어보는 데 시간을 할애했습니다. 발견한 패턴들은 LLM이 생성한 프로젝트에서 반복적으로 등장하며, 이 모델들이 코드를 작성하는 방식의 근본적인 특성을 가리키고 있다고 생각합니다.

B-tree는 존재했지만, query planner는 그것을 무시했습니다

SQLite에서 PRIMARY KEY 조회는 B-tree 경로를 타고 O(log n) 시간에 끝납니다. where.c의 네 줄이 iPKey를 확인하고 쿼리를 트리로 직접 라우팅합니다. 이것은 전체 시스템이 어떻게 맞물려 돌아가는지를 이해할 때만 의미가 있는 종류의 미시적 최적화입니다.

LLM이 생성한 버전에도 B-tree 구현체가 있었습니다. 독립적으로 테스트하면 정확히 작동했습니다. 문제는 query planner가 primary key 조회에 그것을 전혀 호출하지 않았다는 점입니다. is_rowid_ref() 함수는 딱 세 가지 리터럴 문자열, 즉 “rowid”, “rowid”, “oid”만 인식했습니다. id INTEGER PRIMARY KEY처럼 컬럼을 선언하면 planner는 그것이 rowid 별칭임을 인식하지 못했습니다. 모든 쿼리가 full table scan으로 처리됐습니다.

수치로 보면 그 결과는 가혹합니다. 100개 행을 100회 조회할 때, B-tree 경로는 약 700번의 비교 연산을 수행합니다. full scan 경로는 10,000회가 넘습니다. 그러나 진짜 피해는 알고리즘 복잡도에서 옵니다. 조회당 O(log n)이 O(n)이 되고, 전체 벤치마크 스위트에 걸쳐 그것이 누적되면서 20,171배라는 격차로 이어졌습니다.

이것은 직접 벤치마크를 작성하지 않는 한 어떤 단위 테스트도 잡아내지 못하는 종류의 버그입니다. B-tree는 작동합니다. scan도 작동합니다. planner가 잘못된 쪽을 선택합니다. 모든 테스트는 통과합니다.

안전한 기본값은 이자처럼 복리로 쌓입니다

이 사례가 단순한 라우팅 버그보다 더 흥미로웠던 이유가 있습니다. query planner 문제를 감안하더라도 재구현체는 여전히 약 2,900배 느렸습니다. 나머지 격차는 각각 합리적으로 보이는 결정들이 쌓인 결과였습니다.

모든 쿼리 실행 시 전체 AST를 복제하고 bytecode로 재컴파일했습니다. SQLite는 prepared statement 핸들을 재사용합니다. 두 방식 모두 유효하지만, 실행마다 AST를 복제하는 것은 규모가 커질수록 비용이 급증합니다.

모든 페이지 읽기 시 힙에 새로운 4KB 버퍼를 할당했습니다. SQLite의 page cache는 이미 로드된 메모리에 대한 직접 포인터를 반환합니다. LLM 버전은 안전하고 명백한 경로를 택했습니다. 할당하고, 읽고, 반환합니다. 작동은 합니다. 쿼리당 수천 페이지를 읽을 때는 몇 자릿수 차이가 납니다.

모든 커밋마다 전체 스키마를 처음부터 재구성했습니다. SQLite는 단일 정수 cookie 값을 비교합니다. cookie가 변경되지 않았다면 스키마는 여전히 유효합니다. 재구현체에는 이 개념이 없어서 매번 전체 작업을 수행했습니다.

모든 statement가 sync_all() 호출로 파일 메타데이터 전체를 디스크에 플러시했습니다. SQLite는 파일 데이터만 플러시하고 메타데이터 동기화를 건너뛰는 fdatasync()를 사용합니다. 이 차이는 쓰기가 많은 워크로드에서 엄청난 영향을 미칩니다.

이것을 방어적 기본값의 복합 효과라고 부르고 싶습니다. 각 선택은 독립적으로는 합당한 근거가 있습니다. AST를 복제하면 Rust의 소유권 복잡성을 피할 수 있습니다. 새 버퍼를 할당하면 use-after-free 버그를 방지합니다. 스키마를 재구성하면 캐시 불일치를 피합니다. sync_all()을 호출하면 가장 강력한 내구성 보장을 제공합니다.

그러나 성능 비용은 덧셈이 아닌 곱셈으로 누적됩니다. 10배 패널티 네 개가 쌓이면 40배가 아니라 10,000배가 됩니다. LLM은 각 함수를 비교적 독립적으로 생성하기 때문에 이 복리 효과를 추론하지 못합니다. 지역적으로 최적화하고, 전체적으로 대가를 치릅니다.

크론 한 줄짜리를 대체하는 데 82,000줄이 필요했습니다

같은 개발자의 다른 LLM 생성 프로젝트도 동일한 패턴을 다른 방식으로 보여줬습니다. 문제는 이것이었습니다. Rust의 target/ 디렉토리에 있는 빌드 아티팩트가 시간이 지남에 따라 디스크 공간을 잠식합니다. LLM의 해결책은 일곱 개의 대시보드와 어떤 아티팩트를 정리할지 결정하는 베이즈 점수 엔진을 갖춘 82,000줄의 Rust 데몬이었습니다.

기존 해결책은 find ./target -type f -atime +30 -delete, cron 작업의 한 줄입니다. 의존성이 없습니다. 아니면 엣지 케이스까지 이미 처리하는 공식 커뮤니티 도구인 cargo-sweep을 쓰면 됩니다.

LLM이 생성한 프로젝트는 192개의 의존성을 끌어들였습니다. 참고로 Rust 생태계에서 가장 정교한 검색 도구 중 하나인 ripgrep은 61개를 사용합니다.

이것은 반복해서 보이는 패턴입니다. LLM은 당신이 필요로 하는 것이 아닌 당신이 요청한 것을 만듭니다. “모니터링과 점수 체계를 갖춰 Rust 빌드 아티팩트를 지능적으로 관리하는 시스템을 만들어라”라고 프롬프트하면 정확히 그것이 나옵니다. 모델은 뒤로 물러서서 문제가 시스템을 필요로 하는지 물어볼 메커니즘이 없습니다. target/ 디렉토리 크기가 이미 잘 알려진 해결책이 있는 Rust 커뮤니티의 오랜 불만이라는 것을 알지 못합니다. 의존성 0개 대비 192개의 유지보수 비용을 고려하지 않습니다.

연구 결과도 같은 방향을 가리킵니다

이 두 프로젝트가 이상치인지 궁금해서 더 넓은 연구를 살펴봤습니다. 그렇지 않았습니다.

METR은 16명의 숙련된 오픈소스 개발자를 대상으로 무작위 대조 시험을 진행했습니다. AI 도구를 사용한 그룹은 대조군보다 19% 느리게 작업을 완료했습니다. 가장 인상적이었던 부분은 이것입니다. 실험이 끝난 후, AI 사용 그룹은 자신들이 20% 더 빨랐다고 믿었습니다. 생산성에 대한 주관적 경험이 측정된 현실과 정반대였습니다.

GitClear는 2억 1천만 줄의 코드를 분석했고, 복사-붙여넣기된 코드가 처음으로 리팩토링된 코드를 앞질렀음을 발견했습니다. 이 추세는 AI 코딩 도구 도입과 직접적인 상관관계가 있습니다. 코드가 개선되는 속도보다 추가되는 속도가 더 빠릅니다.

Google의 DORA 2024 보고서는 AI 도입이 25% 증가할 때 배포 안정성이 7.2% 하락하는 상관관계를 발견했습니다. 프로덕션에 들어가는 AI 생성 코드가 늘어날수록, 인시던트도 늘어납니다.

NeurIPS 2024의 Mercury 벤치마크는 표준 코딩 벤치마크에 효율성 지표를 추가했습니다. “올바른 출력을 생성하는가”뿐 아니라 “자원을 낭비하지 않고 올바른 출력을 생성하는가”를 측정하자, 합격률이 50% 미만으로 떨어졌습니다.

이것이 LLM이 코딩에 무용하다는 의미는 아닙니다. 저도 항상 사용합니다. 그러나 “컴파일되고 테스트를 통과한다”는 것이 위험할 정도로 낮은 기준임을 의미합니다. 그럴듯한 코드와 올바른 코드 사이의 간극이 바로 실제 엔지니어링이 일어나는 곳입니다.

이것이 개발자에게 실제로 요구하는 것

핵심 문제는 LLM이 나쁜 코드를 작성한다는 것이 아닙니다. 지역적으로는 일관되지만 전체적으로는 일관되지 않은 코드를 작성한다는 것입니다. 각 함수는 말이 됩니다. 시스템은 그렇지 않습니다. 이것이 전통적인 테스팅이 놓치는 정확한 실패 유형인데, 테스트는 지역적 동작을 검증하기 때문입니다.

필요한 것은 이 간극을 겨냥하는 평가입니다. 테스트만이 아닌 벤치마크. 정확성 검사만이 아닌 CI의 성능 예산. “이 모듈이 왜 존재하는가”를 먼저 묻는 아키텍처 리뷰. 솔루션의 복잡성을 문제의 복잡성과 비교하는 의존성 감사.

질문은 “이 코드가 올바르게 보이는가”가 아닙니다. “어떻게 이것이 올바름을 증명할 것인가”입니다. 그리고 증명에는 LLM이 현재 갖추지 못한 시스템 수준의 사고가 필요합니다.

요청한 것과 프로덕션이 요구하는 것 사이의 간극, 바로 거기에 엔지니어링 판단력이 존재합니다. 측정 없이 코드 생성은 그저 토큰 생성에 불과합니다.

뉴스레터 구독하기

최신 프로젝트, 아티클, AI와 웹 개발 실험에 대한 소식을 받아보세요.