그럴듯한 코드를 쓰는 LLM과 그 이면
최근 개발자 커뮤니티에서 LLM이 생성하는 코드의 품질을 두고 매우 흥미로운 논쟁이 벌어졌다. '너의 LLM은 정확한 코드가 아니라 그럴듯한 코드를 쓴다’는 제목의 글이 큰 화제가 되었는데, 한 개발자가 Rust로 SQLite를 처음부터 끝까지 재구현한 프로젝트를 분석한 내용이다. 겉으로 보기에는 수십만 줄의 방대한 코드가 완벽한 구조를 갖추고 테스트를 통과하며 실제 SQLite 파일 포맷을 읽고 쓰는 것처럼 보이지만, 성능 면에서는 원본 SQLite보다 2만 배나 느리다는 충격적인 결과가 나왔다. 이는 LLM이 아키텍처나 모듈의 이름 같은 외형은 그럴듯하게 흉내 내지만, 성능을 결정짓는 핵심적인 최적화 로직이나 불변식은 놓치기 쉽다는 점을 시사한다.
이 글을 쓴 저자는 단순히 비판에 그치지 않고 구체적인 코드를 분석하며 문제의 원인을 짚어냈다. 가장 결정적인 문제는 쿼리 플래너의 버그였다. SQLite에서는 정수 기본 키(Integer Primary Key)를 조회할 때 B-Tree를 직접 탐색하여 로그 시간 복잡도를 가지도록 설계되어 있다. 하지만 LLM이 생성한 Rust 코드에서는 정수 기본 키를 일반 컬럼으로 처리했고, 결국 쿼리 플래너는 모든 행을 하나하나 비교하는 풀 테이블 스캔 방식을 선택했다. 100개의 행을 조회할 때 100번의 B-Tree 탐색 대신 1만 번의 비교 연산이 일어난 셈이다. 이 4줄짜리 함수 하나가 성능 차이의 주범 중 하나였다.
또 다른 심각한 문제는 INSERT 성능에서 발견되었다. 트랜잭션 외부에서 실행되는 단일 삽입 연산마다 매번 fsync를 호출하여 디스크 동기화를 수행하는 로직이 들어있었다. SQLite는 시스템 호출 비용을 줄이기 위해 최적화된 경로를 사용하고 필요할 때만 fdatasync를 사용하지만, LLM이 짠 코드는 안전을 위해 가장 보수적이고 느린 방식을 모든 시점에 적용했다. 그뿐만 아니라 매 요청마다 AST를 복제하고 스키마를 다시 로드하는 등, 실제 상용 데이터베이스라면 절대 하지 않을 비효율적인 선택들이 '안전한 기본값’이라는 명목 아래 수만 줄의 코드 곳곳에 산재해 있었다.
저자는 두 번째 사례로 디스크 정리 도구 프로젝트를 들었다. 단순히 빌드 결과물을 지우는 크론 잡 한 줄이면 해결될 문제를 위해 LLM은 8만 줄이 넘는 복잡한 Rust 코드를 만들어냈다. 베이지안 확률 기반의 예측 엔진이나 현란한 터미널 대시보드를 갖춘 이 도구는 '지능적인 디스크 관리’라는 프롬프트에는 완벽히 부합하지만, 실제 사용자가 겪는 디스크 부족 문제를 해결하는 데는 과도하게 비효율적인 방식이었다. 이것이 바로 저자가 말하는 'LLM의 실패 모드’다. LLM은 사용자의 의도를 흉내 내는 데는 뛰어나지만, 상황에 정말로 필요한 최적의 해결책을 제시하지는 못한다는 것이다.
이러한 현상의 근저에는 AI 정렬 연구에서 말하는 'Sycophancy(아첨)'라는 개념이 있다. LLM은 사용자가 듣고 싶어 하는 말, 혹은 사용자가 기대하는 형태의 결과물을 제공하려는 경향이 강하다는 것이다. 앤스로픽이나 구글의 연구에 따르면 모델들은 정답보다 사용자의 의견에 일치하는 답변을 선호하도록 학습되는 경우가 많다. 코딩에서도 마찬가지다. 'SQLite를 구현해줘’라고 하면 SQLite의 구조를 닮은 거대한 코드 뭉치를 만들어주지만, 그 코드가 실제 운영 환경에서 2만 배 느릴 수 있다는 사실은 모델의 우선순위가 아니다. 모델에게는 '구현된 것처럼 보이는 것’이 '실제로 효율적으로 작동하는 것’보다 더 큰 보상을 받기 때문이다.
하지만 나는 이 글의 논지에 대해 몇 가지 각도에서 반박해보고자 한다. 이 글은 매우 논리적인 것처럼 보이지만 실제로는 몇 가지 치명적인 오류와 편향을 담고 있다.
첫 번째 반박은 샘플 편향이다. 저자는 한 명의 개발자가 만든 두 개의 프로젝트만을 분석하여 'LLM이 만드는 코드는 그럴듯하지만 틀렸다’는 결론을 성급하게 일반화했다. 이는 전형적인 체리피킹의 사례다. 57만 줄짜리 SQLite 재구현 프로젝트에서 특정 체크 로직 하나가 빠진 것은 분명한 문제지만, 이것이 LLM 자체의 한계인지 아니면 해당 개발자가 적절한 벤치마크나 검증 없이 코드를 수용한 개발 방법론의 실패인지 구분하지 않았다. 사람이 짠 코드에서도 N제곱 알고리즘 버그는 흔하게 발생한다. 도구의 실패와 사용자의 검증 부재를 혼동해서는 안 된다.
두 번째로, 저자는 METR의 연구를 잘못 인용했다. 글에서는 2025년 연구를 근거로 'AI가 개발자를 19% 느리게 만든다’고 주장하지만, 정작 METR 측은 2026년 2월에 해당 연구의 방법론적 결함을 공식적으로 인정했다. 연구 참가자들의 상당수가 어려운 과제를 의도적으로 제외하는 선택 편향을 보였고, AI 없이는 작업하기를 거부하는 참가자들 때문에 표본이 심하게 왜곡되었다는 사실이 밝혀졌다. METR은 초기 데이터가 AI의 실제 속도 향상 능력을 심각하게 과소평가했을 가능성이 크다고 정정했다. 저자는 이미 반박된 연구 결과를 마치 확정된 진실처럼 제시하며 자신의 주장을 강화하는 데 사용했다.
세 번째 반박은 저자의 논지가 가진 자기모순이다. 저자는 글의 말미에 'LLM은 사용자가 무엇이 정답인지 알고 있을 때 매우 생산적인 도구다’라고 썼다. 그렇다면 이 글의 진짜 비판 대상은 '그럴듯한 코드를 쓰는 LLM’이 아니라 '검증 없이 결과물을 맹신하는 개발자’가 되어야 한다. 제목은 LLM의 기술적 결함을 공격하는 것처럼 뽑아놓고, 실제 해법으로는 사용자의 역량 향상을 말하고 있다. 이는 망치가 나쁜 게 아니라 설계도 없이 집을 짓는 목수가 문제라는 말과 다를 바 없다. 저자가 진정으로 비판하고 싶은 것은 'Vibe Coding’이라는 방법론이지 LLM 자체의 성능이 아니다.
네 번째로 '2만 배 느리다’는 자극적인 수치에 담긴 맥락을 살펴야 한다. 앞서 언급한 쿼리 플래너의 정수 키 체크 누락은 단 4줄의 함수 수정으로 해결할 수 있는 버그다. 이 버그는 LLM이라서 발생한 고유의 결함이라기보다, 쿼리 플래너를 처음 작성하는 인간 개발자도 충분히 저지를 수 있는 실수다. SQLite가 지금의 성능을 내기까지 26년 동안 수천 번의 수정을 거쳤다는 사실을 간과해서는 안 된다. 이 버그를 발견한 뒤 LLM에게 수정을 요청하면 단 몇 초 만에 고칠 수 있다. 즉, 성능 저하의 폭은 컸지만 이를 수정하는 비용은 극히 낮다. 최악의 사례(Worst-case) 하나를 가져와 전체 코드 품질의 척도로 삼는 것은 공정하지 않다.
다섯 번째로 저자는 LLM의 긍정적인 생산성 지표들을 의도적으로 무시했다. MIT, 하버드, 마이크로소프트가 공동으로 진행한 대규모 현장 실험에서는 AI 코딩 도구가 개발자의 생산성을 유의미하게 향상시킨다는 결과가 반복적으로 나왔다. 스택 오버플로우의 최신 설문조사에서도 과반수의 개발자가 AI를 통해 실질적인 도움을 얻고 있다고 답했다. LLM의 pass@1 정확도가 낮다는 지표 역시, 개발 과정이 단 한 번의 생성으로 끝나는 것이 아니라 지속적인 리뷰와 수정을 포함한다는 실무의 맥락을 제외한 수치다.
결국 이 논쟁의 핵심은 도구의 결함이 아니라 도구를 대하는 우리의 태도에 있다. LLM은 수십 년의 역사가 담긴 최적화 불변식을 스스로 창조해내지는 못한다. 하지만 그 불변식을 알고 있는 개발자가 LLM을 가이드하며 코드를 작성할 때의 속도는 비교할 수 없을 만큼 빠르다. 저자가 지적한 것처럼 '그럴듯함’에 매몰되어 검증을 소홀히 하는 문화는 경계해야 마땅하다. 특히 성능이 중요한 데이터베이스나 백엔드 시스템을 구축할 때 벤치마크 없는 배포는 재앙을 부를 수 있다.
나는 LLM이 소프트웨어 공학의 종말을 가져오는 것이 아니라, 오히려 검증과 설계의 중요성을 더 부각하고 있다고 생각한다. 코드를 한 줄씩 타이핑하는 노동의 가치는 줄어들겠지만, 전체 시스템의 흐름을 파악하고 병목 지점을 찾아내어 AI에게 올바른 수정을 지시하는 통찰력의 가치는 더욱 커질 것이다. 2만 배 느린 코드가 생성되었다면 그것을 발견해낸 저자의 분석력이야말로 앞으로의 개발자에게 필요한 핵심 역량이다. 문제는 도구가 아니라 그 도구를 다루는 방식이며, 우리는 ‘그럴듯한’ 환상 뒤에 숨은 실질적인 수치와 원칙을 놓치지 말아야 한다.
관련 글
그럴듯한 코드를 쓰는 LLM과 그 이면