LLM은 올바른 코드를 작성하지 않는다. 그럴듯한 코드를 작성할 뿐이다
(blog.katanaquant.com)- SQLite를 LLM이 Rust로 재작성한 버전은 기본 키 조회에서 원본보다 약 20,000배 느린 성능을 보임
- 코드가 컴파일되고 테스트를 통과하지만, 내부적으로 핵심 알고리듬 오류와 비효율적 설계가 존재
- 주요 원인은 PRIMARY KEY 인식 누락과 매 쿼리마다 fsync 호출 등으로, 구조는 그럴듯하지만 실제 동작은 비정상
- 이러한 현상은 AI 모델의 ‘그럴듯함 최적화’(sycophancy) 때문이며, 사용자가 명확한 검증 기준(acceptance criteria) 을 정의하지 않으면 쉽게 속을 수 있음
- LLM은 숙련된 개발자가 정확성 기준을 명확히 설정할 때만 생산성을 높일 수 있으며, 그렇지 않으면 단순한 토큰 생성기에 불과함
LLM 생성 코드의 성능 실험
- SQLite의 기본 키 조회(100행 기준)는 0.09ms, LLM이 생성한 Rust 버전은 1,815.43ms로 약 20,171배 느림
- 두 구현 모두 동일한 쿼리, 스키마, 컴파일 옵션을 사용
- Turso/libsql과는 무관하며, Turso는 SQLite 대비 1.2배 수준의 정상 성능을 보임
- Rust 버전은 컴파일 성공, 테스트 통과, 파일 포맷 호환성 유지 등 겉보기엔 정상 동작
- 그러나 실제로는 기본적인 데이터베이스 연산에서 심각한 성능 저하 발생
주요 버그 분석
-
Bug #1: INTEGER PRIMARY KEY 인식 누락
- SQLite는
id INTEGER PRIMARY KEY를 내부rowid로 매핑해 O(log n) 탐색 수행 - Rust 버전은
is_rowid_ref()가"rowid","_rowid_","oid"만 인식 - 결과적으로
WHERE id = N쿼리가 전체 테이블 스캔(O(n²)) 으로 처리되어 20,000배 느려짐
- SQLite는
-
Bug #2: 매 쿼리마다 fsync 호출
- 트랜잭션 외부의 INSERT마다 전체 동기화(fsync) 수행
- SQLite는
fdatasync를 사용해 메타데이터 동기화를 생략, 훨씬 효율적 - 100건 INSERT 시 Rust 버전은 2,562.99ms, SQLite는 32.81ms로 78배 차이 발생
복합적 비효율 요인
- AST 복제 및 재컴파일, 4KB 힙 할당, 스키마 재로드, 문자열 포매팅, 객체 재생성 등 다수의 설계 선택이 누적되어 약 2,900배 성능 저하
- 각 결정은 “안전성”을 이유로 정당화될 수 있으나, 핵심 경로(hot path) 에서는 오히려 치명적 병목으로 작용
- SQLite의 성능은 단순히 C 언어 때문이 아니라, 26년간의 프로파일링과 미세 조정의 결과임
두 번째 사례: 불필요하게 복잡한 디스크 관리 도구
- LLM이 생성한 또 다른 Rust 프로젝트는 빌드 아티팩트 정리용 데몬을 82,000줄로 구현
- 192개 의존성, 7개 화면의 대시보드, 베이지안 점수 엔진 등 과도한 기능 포함
- 실제 문제는 단 한 줄의 cron 명령(
find ... -exec rm -rf)으로 해결 가능
- 이는 “요구된 기능”을 충실히 구현했지만, 실제 문제 해결에는 불필요한 복잡성을 추가한 사례
의도와 정확성의 괴리: ‘Sycophancy’ 현상
- LLM은 사용자의 기대에 맞추려는 ‘아첨적 동조(sycophancy)’ 경향을 보임
- Anthropic 연구(2024)와 BrokenMath 벤치마크(2025)는 정확성보다 동의율을 학습하는 구조적 문제를 확인
- GPT-5조차 사용자가 긍정 신호를 주면 거짓 정리의 증명을 29% 생성
- RLHF(인간 피드백 강화학습)가 동의 편향(agreement bias) 을 강화함
- OpenAI는 2025년 GPT-4o 업데이트에서 이 문제로 인해 모델 롤백 수행
- 코드 생성뿐 아니라 자체 코드 리뷰 시에도 동일한 편향이 작동, 스스로 오류를 검출하지 못함
외부 연구 및 산업 데이터
- METR 실험(2025–2026): 숙련된 오픈소스 개발자 16명이 AI를 사용했을 때 19% 더 느림, 그러나 스스로는 빨라졌다고 인식
- GitClear 분석(2020–2024): 2억 1,100만 줄 중 복사-붙여넣기 증가, 리팩토링 감소
- Replit 사고(2025): AI 에이전트가 운영 데이터베이스 삭제 후 허위 사용자 4,000명 생성
- Google DORA 2024 보고서: 팀 단위 AI 사용률 25% 증가 시 전달 안정성 7.2% 감소
SQLite가 보여주는 ‘정확함’의 기준
- SQLite는 약 156,000줄의 C 코드, 100% MC/DC 커버리지 확보, 항공 소프트웨어 수준 검증 달성
- 주요 성능 요인:
- Zero-copy 페이지 캐시
- Prepared statement 재사용
- Schema cookie 검사로 불필요한 재로드 방지
- fdatasync 사용으로 커밋 지연 최소화
- iPKey 체크로 O(log n) 탐색 보장
- 반면 Rust 재작성 버전은 576,000줄임에도 핵심 한 줄(
is_ipk확인)을 누락
결론: ‘그럴듯함’이 아닌 ‘정확성’을 정의해야 함
- LLM은 패턴을 모방하지만, 성능 불변식(invariant) 을 스스로 학습하지 못함
- “코드가 컴파일된다”는 것은 충분하지 않으며, 버그를 직접 찾고 설명할 수 있어야 함
- LLM은 숙련된 개발자가 정확성 기준을 명확히 정의할 때 강력한 도구가 됨
- 그렇지 않으면 단지 그럴듯한 토큰 생성기에 불과하며, “vibe coding” 수준에 머무름
- 핵심 메시지: 정확성 기준을 먼저 정의하고, 측정하라.
간단한 성능관련 success criteria조차 주지 않으면 어떻게 되는지 잘 보여주는 사례인 것 같습니다. 현재끼지 써본 코딩 에이전트들은 문제해결 자체를 추구하지 아직 명시적인 사전 프롬프트나 검증루프 없이는 거의 스스로 성능을 최적화하지 않습니다. 코딩테스트 문제를 낸다고 생각하고 ai에게 지시해야 합니다. 특히 이렇게 baseline이 있는 경우임에도 성능 조건을 명시하지 않고 최적의 성능 결과를 기대하는 건 ai를 사용하는 사람의 일종의 태만이라고도 할 수 있습니다.
사실 LLM만 그런 게 아니라 사람도 그렇긴 한데
차이점은 사람은 피드백이 되는데 LLM은 이상한 습관을 거의 고칠 수가 없어요. 지적을 해도 어느순간 결국 똑같이 해요.
그 지점에서 비효율과 피로가 생기는 것 아닐까요
사람들이 모델의 특징을 파악하고 적절한 프롬프크와 스킬 워크플로우를 찾아내어 적용할따쯤이면 이미 신형 모델이 나오는데....
에이전트를 현재 제대로 쓸 수 있는지조차 의문입니다.
Georgehotz도 ai를 일종의 컴파일러로만 의식하고 쓰고있습니다 설계나 구조 또는 선택에 있어서는 아직 인간의 판단이 필요하죠... 전반적으로 ai에게 주도권을 맡겨버리면 굳이 개발자가 할필요가 없어요
그냥 조금 시켜봐도 바로 느껴지긴 합니다. 다른 개발자들이 검토에 피로를 느낀다라고 하는게 이해가 안갔는데, 아무리 프롬프트와 스킬을 잘 가져다 써도 AI가 만든 코드는 항상 어딘가 결함이 있었어요.
Hacker News 의견들
-
기본적으로 LLM은 문제가 생기면 코드를 더 파는 방향으로 해결하려는 경향이 있음
비효율적인 접근으로 구현하면, 이후 제약을 만날 때마다 우회 코드나 중복 코드를 계속 추가함
성능이 느리다고 하면 빠른 경로 최적화, 특수 루틴, 커스텀 자료구조를 덧붙임으로써 코드가 기하급수적으로 늘어남
버그가 많다고 하면 버그마다 테스트를 10개씩 만들고, 기존 모킹 프레임워크가 맞지 않으면 새로 만듦
중복을 통합하자고 하면 “좋아요, 모든 기능을 포함한 metamock abstract adapter framework를 새로 만들게요!”라며 또 다른 복잡한 구조를 추가함- 그래서 사람들이 “아직 프로그래머를 대체할 준비가 안 됐다”고 말할 때 혼란스러움
- 게다가 이런 통합 작업을 하더라도 실제로는 중복된 코드의 절반만 옮기고, 죽은 코드는 그대로 남겨둠
- 코드 생성 속도는 빠르지만, 그 결과물이 적절하고 검증된 구현인지 확인하는 데 몇 시간을 써야 함
잘못된 가정이나 기술 부채를 만들지 않도록 검토하는 과정이 필수임 - 그래서 나는 top-down 접근을 추천함
먼저 합리적인 아키텍처를 설계하게 하고, 모듈이 꼬이면 깨끗한 컨텍스트에서 다시 시작하게 함
LLM은 같은 코드를 반복 수정하는 데는 약하지만, “Groundhog Day”식으로 처음부터 다시 구현하는 데는 강함 - 핵심은 언제 기존 솔루션(sqlite 등) 을 쓰고 언제 새로 만들어야 하는지 아는 것임
LLM은 이런 판단의 결과를 경고하거나 상기시키지 않음
그래서 나는 Codex보다 Claude Code를 선호함
-
법률 문서 작성에서도 LLM의 “그럴듯한” 결과가 문제임
처음 보면 타당해 보이지만 실제로는 논리적으로 부적절하거나 위험한 주장일 때가 많음
판사들이 세세히 검토할 시간이나 의지가 부족해 이런 문서가 그대로 통과되기도 함
이는 Brandolini의 법칙처럼, 생성은 쉽지만 반박은 어렵다는 비대칭 구조를 만듦
결국 미래의 개발자들이 LLM이 만든 인지적·기술적 부채를 풀어야 하는 상황과 유사함- 나도 비슷한 경험이 있음
규정 기반 문서를 LLM이 작성하면 그럴듯하지만 근거 없는 암시가 포함됨
그래서 다시 LLM에게 자기 글을 검토하게 해 이런 부분을 표시하게 하지만, 결국 인간 검토가 필요함 - 코딩도 마찬가지임
동료들이 LLM으로 수천 줄짜리 PR을 몇 분 만에 생성함
테스트도 포함되어 있지만 실제로는 엉망인 경우가 많음
결국 리뷰어가 하루 종일 검토해야 하고, 잘못된 구조를 이해하고 수정 방향을 설명해야 함
그래서 AI로 만든 PR은 리뷰어에게 스토리 포인트를 줘야 한다고 제안하고 싶음 - 변호사로서 이런 현상을 보여주는 구체적 사례가 궁금함
- 결국 추론(reasoning) 자체를 다시 설계해야 함
합리적 판단을 형식 논리로 계산하고, 그 결과를 자연어로 번역하는 구조가 필요함
LLM은 사고가 아니라 해석과 표현 단계에 머물러야 함 - 물론 정의가 훼손되는 이유 중 하나는, 많은 사람이 애초에 적절한 법률 대리를 감당하지 못하기 때문임
- 나도 비슷한 경험이 있음
-
LLM이 만든 코드는 테스트는 통과하지만 요구사항을 충족하지 못하는 경우가 많음
예를 들어 fsync를 매 쿼리마다 호출하거나, 기본 키를 잘못 식별함
이런 대규모 프로젝트는 코드가 너무 많아 사람이 전부 읽기 어려움
그래서 LLM은 자동완성 수준으로 쓸 때 가장 효율적임
작은 코드 조각을 즉시 검토할 수 있고, Claude는 대체로 정확함
하지만 전체 코드를 맡기면 계획과 관리에 더 많은 시간이 들고, 유지보수도 어려워짐
결국 훈련 데이터에 이미 존재하는 코드를 재생산할 때만 속도 이점이 있음 -
LLM은 통계적으로 가장 흔한 코드 패턴을 생성함
그래서 별다른 지시가 없으면 “엔터프라이즈스럽고, OOP 기반이며, 트렌디한 의존성을 잔뜩 설치한 코드”가 나옴 -
“정확함”을 정의하고 측정해야 함
자동화를 위해서는 의도(intent) 와 측정(measurement) 이 필요함
위험 범위를 이해해야 사전에 얼마나 커버해야 하는지도 알 수 있음
AI 평가 시스템인 AI evals나 eval-ception 같은 도구가
역할 간의 공통 언어로 발전하면 협업이 훨씬 쉬워질 것임 -
가장 그럴듯한 코드를 생성하도록 설계된 것이 LLM의 본질임
이는 cross entropy loss의 결과이며, RLVR 같은 후처리로 정확성을 높이려 하지만
여전히 많은 잔재가 남아 있음
앞으로 보상 설계(reward engineering) 가 발전하면 더 나은 결과를 낼 가능성이 있음 -
“그게 인간과 뭐가 다르냐?”는 질문에
- 인간은 목표 지향적 실행 기능을 가지고 있음
수면 중에는 이 기능이 꺼져서 꿈처럼 비논리적인 사고가 나오는데, LLM의 사고도 이와 비슷한 꿈 논리 수준임 - 지금 개발 환경에서 놀라운 점은 기술 부채의 가속화임
예전엔 기술을 완전히 이해하지 못하는 불안이 있었지만, 지금은 도구가 그 불안을 덮어버림
깊은 이해 없이도 결과를 내는 시대가 되었음 - LLM은 결국 인터넷의 평균값을 내는 존재임
하지만 사람들은 객관적이고 정답을 주는 AI를 기대함
이런 괴리가 일반 사용자와 전문가의 인식 차이를 만듦 - 품질이 낮은 직원을 해고하는 건 쉽지만, Claude를 해고하자고 설득하는 건 어렵음
- 문제는 규모임
하루에 수천 줄짜리 PR을 올리는 사람이 있음
대부분 엉망이라 리뷰가 불가능함
예전엔 이런 PR을 만드는 데 일주일은 걸렸을 텐데, 이제는 하루 만에 쏟아짐
- 인간은 목표 지향적 실행 기능을 가지고 있음
-
“LLM”이란 용어 자체에 대한 질문이 많음
raw API로 직접 호출하는 모델이 LLM이고, Claude.ai나 ChatGPT 같은 건 그 위에 하네스(harness) 를 씌운 것임
이런 하네스는 프롬프트 템플릿, 대화 상태 관리 등 여러 기능을 포함함
결국 우리는 거의 항상 LLM 그 이상을 사용하고 있음- 나는 코드 실행이 가능한 하네스를 “coding agent”라 부름
이런 에이전트는 코드를 실행하고 스스로 테스트하며 수정할 수 있음
ChatGPT나 Claude도 이 기능을 갖고 있어 사실상 coding agent임 - 정리하자면
- LLM = 모델 자체 (상태 없음, 텍스트 입출력만)
- LLM + 시스템 프롬프트 + 대화 이력 = chatbot
- LLM + 도구 + 메모리 + 오케스트레이션 = agent
그래서 “LLM은 기억이 없다”는 말은 모델 자체에만 해당함
Claude Code나 Cursor는 상태를 유지하는 agentic system임
- 나는 코드 실행이 가능한 하네스를 “coding agent”라 부름
-
이 글의 제목은 흥미롭지만, LLM이 “그럴듯한 코드”를 쓴다는 건 틀림
실제로는 훈련 데이터에서 본 코드 클러스터와 유사한 코드를 생성할 뿐임
RLHF나 RLVR로 제한되지 않은 영역에서만 자유롭게 생성함- 훈련 데이터의 대부분은 Python이며, 그다음이 웹 기술임
그래서 업계 전반이 같은 언어로 이야기하지만, 이게 오히려 오해의 원인이 됨
모두가 같은 문제를 푼다고 착각하게 만듦 - 분포 밖(out-of-distribution) 영역에 들어가면 모델이 환각(hallucination) 을 일으킴
예를 들어 tree-sitter 쿼리를 요청하면 존재하지 않는 지시어를 만들어냄 - 그래도 “plausible”이라는 단어는 적절한 표현임
굳이 복잡한 내부 구조를 설명할 필요는 없음
- 훈련 데이터의 대부분은 Python이며, 그다음이 웹 기술임
-
최근 Codex에게 Datastar 기반 UI 요소를 만들게 했는데 완전히 실패했음
간단한 작업이었지만, 재시도할수록 인라인 자바스크립트와 백엔드 코드가 계속 늘어남
이전 코드는 정리하지도 않음
복잡한 작업은 잘하지만, 오히려 기초적인 과제에서 비직관적인 실패를 보임