LLM 코딩, 실행 가능한 검증자로 자유도를 제거하다
John Regehr, 2026년 3월 26일
우리는 이 도구들을 믿을 수 없다
Claude나 Codex 같은 LLM 기반 코딩 에이전트를 써본 개발자라면 누구나 느꼈을 겁니다. 이 도구들은 특정 조건이 갖춰지면 정말 놀라운 속도로 좋은 결과를 만들어냅니다. LLVM IR 같은 복잡한 API를 다루는 것도 잘하고, 실제 소프트웨어의 까다로운 버그를 그대로 적용할 수 있는 수준으로 고쳐주기도 합니다.
그런데 문제가 있습니다. 같은 도구가 정말 이상한 방식으로 망가지곤 합니다. 엉뚱하거나 말도 안 되는 코드를 뱉어내는 거죠.
LLM에게 "나쁜 선택"을 할 자유가 있으면, 우리는 그것이 옳은 선택을 할 것이라 믿을 수 없습니다. 그렇다면 해결책은 간단합니다. 나쁜 결과를 만들 수 있는 자유를 빼앗아버리면 되는 것입니다.
이걸 도와줄 도구가 바로 "실행 가능한 검증자(executable oracle)"입니다.
실행 가능한 검증자란?
가장 단순한 형태의 실행 가능한 검증자는 테스트 케이스입니다. 하지만 테스트 케이스만으로는 부족합니다. 아무리 많아도 말이죠.
제가 예전에 다룬 Claude's C Compiler 사례를 생각해보죠. GCC의 "torture test suite"를 포함해 여러 테스트를 통과했음에도 불구하고, 34개의 심각한 컴파일 오류가 여전히 숨어 있었습니다.
그런데 만약 Csmith나 YARPGen 같은 도구가 테스트 루프에 포함되었다면 어땠을까요? 이런 도구들이 더 나은 검증자인 이유는, 각각이 내부적으로 엄청난 양의 테스트 케이스를 내재하고 있기 때문입니다.
이 글의 핵심 아이디어는 이겁니다: 실패를 만들 수 있는 자유도를 최대한 없애자는 것입니다. 완벽한 영점(zero degrees of freedom)은 이상적이지만, 그것을 향해 나아가는 것만 해도 큰 진전입니다.
실제 사례들
사례 1: 코드 최적화
Claude's C Compiler의 또 다른 문제는 생성된 코드의 품질이었습니다. 컴파일러 안에 정교해 보이는 최적화 패스들이 많이 들어있었지만, 실제 성능 개선에는 거의 도움이 되지 않았거든요.
만약 개발자가 "코드 품질"을 검증하는 실행 가능한 검증자를 테스트 루프에 포함했다면 어땠을까요? 추측이긴 하지만, Claude가 그 피드백을 받고 훨씬 더 나은 최적화를 해냈을 거라고 봅니다.
어떤 형태의 검증자면 될까요? 간단하게 유지하되—예를 들어 컴파일러 출력을 실행했을 때 CPU가 실행하는 명령어 개수를 세는 식이면 됩니다. 기준점으로 gcc -O0을 제시하면, LLM도 개선의 여지가 어디인지 알 수 있겠죠.
정리하면: LLM이 코드 품질에 대해 자유로운 선택권을 갖고 있었기에 형편없는 결과를 냈습니다.
사례 2: 데이터플로우 분석
우리 팀은 LLVM의 "known bits" 분석 같은 데이터플로우 전이 함수를 자동 합성하는 연구를 하고 있습니다. 원래는 무작위 합성 기법을 썼는데, 최근 Codex에게 직접 전이 함수를 작성하도록 해봤습니다.
혼자 놔두면 나쁘지 않지만, 특별히 좋은 것도 아니었습니다. 그런데 우리 명령줄 도구들—함수의 정확성을 평가하고 정당성을 검증하는 도구들—에 접근하게 하자, Codex는 LLVM 같은 실제 컴파일러나 우리의 무작위 합성 결과보다도 나은 결과물을 만들어냈습니다.
한 가지 남은 자유도는 코드 크기였는데, 이것 때문에 Codex가 좀 큰 함수들을 만들곤 했습니다. 하지만 이건 제한하기 가장 쉬운 부분이었죠.
정리하면: 정당성과 정확성이라는 두 개의 상대적인 검증자 사이에 LLM의 결과물을 "끼워넣자" 데이터플로우 함수 합성이 정말 잘 작동했습니다.
사례 3: HTML 파서
JustHTML은 Python으로 새로 작성된 HTML5 파서입니다. Claude's C Compiler처럼 대규모 기존 테스트 스위트를 활용해서 만들어졌습니다.
작성자의 블로그 포스트를 읽어보면, 코딩 에이전트가 스스로를 구석에 몰아 형편없는 소프트웨어 아키텍처를 만들었다고 합니다. 아키텍처 선택에 실행 가능한 검증자를 달기는 어려운데, 작성자는 이 부분에 시도하지 않았습니다. 대신 LLM을 수동으로 몇 가지 리팩터링 작업으로 이끌어, 결국 더 나은 아키텍처에 도달했습니다.
나중에 그는 정확성(퍼저 사용)과 성능을 위한 실행 가능한 검증자도 추가했습니다.
정리하면: 기존 검증자와 새로운 검증자를 창의적으로 조합하고, 아키텍처 부분에 필요한 수동 개입을 함께 한 것이 인상적인 결과를 만들었습니다.
검증자는 어디서 찾을까?
소프트웨어 테스팅을 가르칠 때, 저는 항상 강조합니다. 테스팅은 창의적인 활동이지, 기계적으로 입력과 출력을 짝짓는 활동이 아니라는 것입니다. 뛰어난 테스팅 사례들을 보면, 그 안에 항상 창의적이고 흥미로운 뭔가가 숨어 있습니다.