
AI에게 코드를 작성하게 하는 글은 넘쳐나는데, '정확성 보증'에 관한 이야기는 너무 적다
요약
AI를 활용한 코드 생성 기술이 급격히 발전하고 있으나, 생성된 코드의 정확성과 품질을 보증하는 검증 메커니즘에 대한 논의는 부족한 실정입니다. 본 글은 코드 생성 파이프라인(Harness) 구축 시 반드시 병행되어야 할 품질 관리와 검증 설계의 중요성을 강조합니다.
핵심 포인트
- AI 코드 생성 능력의 확대만큼 검증 메커니즘 설계가 필수적임
- 코딩 규약 준수, 설계 사양 일치 여부, 아키텍처 위반 확인 필요
- 형식적인 테스트 코드가 아닌 실질적인 경계값 검증이 중요함
- 생산 기술의 진화는 항상 검사 및 품질 관리 기술과 함께 발전함
AI는 대량의 코드를 작성할 수 있게 되었다. 그렇다면, 누가 그것을 보증하는가
Claude Code나 에이전트에게 코드를 통째로 AI 생성시키는 것——이제는 드문 일이 아니게 되었습니다. 설계서나 프롬프트(Prompt)를 입력값으로 하여 코드와 테스트를 자동 생성하는 일련의 메커니즘은 '하네스(Harness)'라고 불리기도 합니다(요컨대, LLM을 포함한 코드 생성 파이프라인을 의미합니다. 본 기사에서도 이후 이 용어를 사용하겠습니다).
코드를 작성하게 만드는 방법에 대한 글은 매일 양산되고 있습니다. 이렇게 지시했다, 이렇게 프롬프트를 구성했다, 이런 하네스를 만들었다. 과거 프로그래밍 붐 시절에 "Rails로 블로그를 만들어 보았다"가 넘쳐났던 것과 같은 광경입니다.
하지만, 여기서 잠시 멈춰 서 주셨으면 합니다. 생성된 코드가 올바르다는 것을 어떻게 보증할 것인가?
'보증'이라는 것은 구체적으로 다음과 같은 질문입니다.
- 생성된 코드가 코딩 규약(Coding Convention)을 모두 준수하고 있음을 어떻게 보증할 것인가?
- 설계서에 적힌 사양(임계값, 계산식, 처리 순서, 예외 타입)이 누락 없이 구현되었음을 어떻게 보증할 것인가?
- 반대로, 설계서에 적혀 있지 않은 불필요한 처리가 섞여 들어가지 않았는가?
- 아키텍처 규약(레이어 구성, 의존 방향)을 위반하지 않았는가?
- 함께 생성된 테스트 코드가 경계값이나 예외 케이스를 정말로 검증하고 있는가? (assert가 비어 있는 '형식적인 테스트'가 되어 있지는 않은가?)
인간이 작성한 코드라면 코드 리뷰(Code Review)와 테스트가 이 질문에 답해 왔습니다. 하지만 LLM이 하루에 수만 행을 생성하는 세상에서, 모든 행을 인간이 리뷰하는 것은 비현실적입니다. "아마 괜찮겠지", "돌아가니까 됐어"로 끝낼 것인가, 아니면 보증 메커니즘을 설계할 것인가——이 지점이 갈림길입니다.
그리고 이 검증 측면의 이야기는 생성 측면의 열기에 비해 압도적으로 부족합니다.
조립 로봇 옆에는 언제나 검사 공정이 함께 달리고 있었다
우리는 비슷한 구도를 한 번 본 적이 있습니다. 바로 공장 자동화의 역사입니다.
세상의 기억에 남는 것은 언제나 '만드는 쪽'의 기계입니다. 용접 로봇, 자동 조립 라인, 무인 운반차. 화려하고, 사진 찍기 좋으며, 생산성 수치가 눈에 띄게 뛰어오르기 때문입니다.
하지만 자동화의 역사를 자세히 살펴보면, 생산 기술의 진화는 언제나 검사와 품질 관리의 진화와 세트로 진행되어 왔습니다. 검사 공정, 한도 견본, 계기 교정, 품질 관리 부서. 만드는 속도가 올라갈수록 불량을 놓쳤을 때의 피해도 빠르고 커집니다——그렇기에 고속으로 만드는 메커니즘에는 반드시 고속으로 보증하는 메커니즘이 병행되어 왔던 것입니다. "만들 수 있는가"와 "만든 것이 양품임을 어떻게 보증할 것인가"는 항상 한 세트였습니다. 한쪽만 손에 넣은 공장은 불량품을 고속으로 양산하는 장치를 손에 넣은 셈이 됩니다.
지금의 LLM 코드 생성은 이 '한쪽만 있는' 상태로 보입니다. 생성 능력은 극적으로 확대되었지만, 검증 능력에 대한 논의가 따라가지 못하고 있습니다.
정확히 말하면 검증 이야기가 아예 없는 것은 아닙니다. 해외에서는 LLM 애플리케이션의 평가(evals) 방법론이 급속히 확립되고 있으며, 학술계에는 LLM 생성 코드의 품질을 다루는 연구도 있습니다. 다만 그 상당수는 RAG나 챗봇 같은 'AI 프로덕트의 평가'가 중심이거나, 혹은 벤치마크 연구입니다. 현장의 엔지니어가 코드 생성의 품질 보증을 실제 프로젝트에서 어떻게 설계하고, 어디에서 타협했는지를 공개하는 글은 일본어든 영어든 아직 적습니다.
이 시리즈는 그 공백을 메우려는 시도입니다. 주제는 제가 참여하고 있는 레거시 언어에서 Java로의 이행 R&D입니다. 설계서로부터 Java 코드와 JUnit 테스트를 생성하는 하네스에 대해, 평가계를 처음부터 설계했습니다. 본 기사에서는 그 출발점이 된 첫 번째 질문부터 시작하겠습니다.
첫 번째 질문: 이 평가는 '걸러내기 위한 것'인가 '키우기 위한 것'인가
평가계를 설계하기 시작했을 때 가장 먼저 부딪힌 질문은 이것이었습니다.
평가는 최소한의 기준을 정해 안 좋은 것을 걸러내기 위해 사용하는 것인가?
아니면 하네스의 질을 높이기 위해 사용하는 것인가?
걸러내기 위해서라면 명확한 Fail 기준을 정하면 됩니다. 키우기 위해서라면 점수를 매기고, 점수가 올라가도록 개선하는 접근 방식이 됩니다.
언뜻 보면 둘 중 하나를 선택하는 문제처럼 보입니다. 하지만 검토하다 보면 이것은 이지선다가 아니라, 평가 대상이 두 가지 있다는 사실을 깨닫게 됩니다.
- 성과물(개별 생성 코드)——이번에 생성된 이 Java는 출하해도 되는가?
- 하네스 자체(생성 메커니즘)——이 하네스는 어떤 설계서가 와도 제대로 된 코드를 내놓을 수 있는가?
그리고 이 두 가지에는 서로 다른 도구가 필요합니다.
성과물에는 Gate(합불)
개별 생성물에 대한 평가는 Pass/Fail의 이진(binary) 값이어야 합니다. 기능이 충족되지 않았거나, 아키텍처 규약(architecture convention)을 위반하는 등의 결함에 대해 "78점"이라는 숫자를 부여해 봤자 의사결정에 사용할 수 없습니다. 출하할 것인가 말 것인가, 재생성할 것인가 아니면 인간에게 에스컬레이션(escalation)할 것인가. 필요한 것은 문지기이며, 문지기의 판정은 흑백이 분명해야 합니다.
하네스(Harness)에는 Score(집계 지표)
반면, "하네스가 좋아졌는가"는 단 한 번의 합불 여부로는 알 수 없습니다. LLM은 비결정적(non-deterministic)이기 때문에, 동일한 설계서로부터 실행할 때마다 다른 코드가 나옵니다. 1건의 Pass는 실력일 수도 있고, 요행일 수도 있습니다.
여기서 필요해지는 것이 머신러닝 개발에서 말하는 eval의 개념입니다. 하는 일은 "학력 테스트"와 유사한 이미지입니다.
문제집 만들기: 성격이 다른 설계서를 10~20건 정도 모아 고정된 문제집으로 만듭니다(이를 golden set이라고 부릅니다). 단순한 처리의 설계서, 분기가 많은 것, 예외 처리가 복잡한 것 등으로 다양하게 구성하는 것이 포인트입니다. 편향된 문제집이라면 "특정 경향에만 강한 하네스"로 자라나기 때문입니다 -
개조할 때마다 전 문항 풀게 하기: 프롬프트나 공정을 변경했다면, 문제집을 처음부터 끝까지 일괄적으로 풀게 하여 Gate 통과 수를 셉니다. "15/20 → 17/20"과 같이 개선 사항이 숫자로 보입니다 -
같은 문제를 여러 번 풀게 하기: LLM은 같은 문제라도 풀 수도 있고 못 풀 수도 있습니다. 따라서 1회의 결과만으로는 실력을 알 수 없습니다. 예를 들어 각 설계서를 3회씩 실행하여 "3회 모두 통과한 비율"(pass^k라고 부릅니다)로 측정하면, 요행과 실력을 구분할 수 있습니다 -
점수 외의 것도 기록하기: 통과율뿐만 아니라 위반 건수, 소요된 토큰(=비용), 실행 시간도 함께 집계해 두면, "똑똑해졌지만 비용은 3배로 늘었다"와 같은 변화도 확인할 수 있습니다
즉, Gate를 돌린 결과를 집계한 것이 그대로 Score가 됩니다. 이중적인 구조를 만들 필요 없이, 문지기의 기록부가 성적표를 겸하는 구조로 만드는 것입니다.
이 정리를 마친 순간, 서두의 질문은 풀립니다. "걸러내기 위한 것"과 "키우기 위한 것"은 대립하지 않습니다. 걸러낸 기록으로 키우는 것입니다.
흔히 저지르는 두 가지 실수
이 구조를 갖춰 두면, 하네스 개선 과정에서 저지르기 쉬운 실수를 시스템적으로 방지할 수 있습니다.
실수 1: 1건만 보고 수정한다. 설계서 A에서 실패했기에 프롬프트를 수정했더니, B와 C가 망가졌다——LLM 개발에서는 일상다반사입니다. 개선의 판단은 반드시 golden set 전 건의 집계로 수행해야 합니다.
실수 2: 변동성을 개선으로 오인한다. 1회의 실행에서 "통과했다! 개선이다!"라고 기뻐하는 것은, 주사위를 던져 6이 나온 것을 실력이라고 부르는 것과 비슷합니다. 여러 번 실행하는 pass^k를 확인하는 습관이 이를 방지합니다.
설계의 기본 사상: 비결정적인 생성을 결정적인 판정으로 받아내기
Gate/Score의 정리와 더불어, 평가계 전체를 관통하는 기본 사상을 먼저 정했습니다.
"비결정론적인 생성물을 가능한 한 결정론적인 판정으로 품질 보증한다"
LLM의 출력은 어디까지나 추론(inference)입니다. 동일한 입력이라도 실행할 때마다 흔들리고, 그럴듯한 오류를 자신만만하게 내놓기도 합니다. 이러한 성질은 생성 측면에서는 받아들일 수밖에 없습니다(그렇기에 생산성이 나오는 것이기도 합니다). 하지만, 판정 측면까지 추론으로 만들어 버리면 불확실한 것을 불확실한 것으로 측정하게 되어, 보증은 어디에도 존재하지 않게 됩니다. 흔들리는 자로 흔들리는 막대를 측정해 봤자 얻을 수 있는 것은 분위기뿐입니다.
따라서 평가계는 판정 수단을 "결정성이 높은 순서"대로 나열하고, 위에서부터 사용할 수 있는 만큼 사용하는 구조로 만들었습니다.
포인트는 4번의 처리입니다. "설계서의 내용이 구현되었는가"와 같은 의미론적 대조는 기계가 판정할 수 없기 때문에 LLM에 의존할 수밖에 없습니다. 하지만 그것은 "어쩔 수 없이, 최소한의 범위 내에서, 제어하면서" 사용하는 것이지, 편리하다고 해서 무엇이든 LLM에게 묻는 것이 아닙니다. judge에 전달할 기준은 고정된 문서에 닻을 내리고(anchored), 판정 절차를 고정하며, 출력을 구조화합니다——추론의 자유도를 깎을 수 있는 만큼 깎아서, 결정적인 판정에 가까워지게 만드는 것입니다.
이 사상은 이후의 모든 설계 판단에 영향을 미칩니다. "LLM에게 유연하게 평가하게 하면 되지 않을까?"라는 유혹은 개발 중에 몇 번이고 나타나지만(실제로 저도 몇 번이나 흔들렸습니다), 그때마다 이 원칙으로 되돌아오게 되었습니다. AI는 추론일 뿐입니다. 보증은 추론 위에 세워질 수 없습니다. 세운다면 결정적인 지반 위에 세워야 합니다.
또 다른 원칙: 생성한 본인에게 채점시키지 않는다
Gate/Score 다음으로 결정한 것이 채점의 독립성입니다.
LLM에게 코드를 생성하게 하고, 그 LLM에게 "이 코드, 설계서를 충족하고 있어?"라고 물으면, 높은 확률로 "충족하고 있습니다"라는 답변이 돌아옵니다. 생성과 검증을 동일한 추론(Inference) 과정에서 수행하게 하면, 동일한 오해는 동일한 오해인 채로 그대로 통과해 버립니다. 이는 사람이라도 자신이 작성한 코드의 버그는 스스로 찾아내기 어려운 것과 같은 구조입니다.
그래서 평가 체계는 생성 측과는 독립된 정답 계통을 갖도록 설계했습니다.
- 기능의 정답: 설계서로부터 별도의 계통으로 만드는 참조 테스트(Reference Test) (생성된 테스트가 아님). 클래스나 메서드의 인터페이스(Interface)를 사전에 공통화하여 "이름이 달라 대조할 수 없는 상황"을 방지합니다.
- 아키텍처의 정답: "비즈니스 로직 계층은 화면 계층에 의존해서는 안 된다"와 같은 구조적 규칙은 사실 테스트 코드(Test Code)로 작성할 수 있습니다 (이를 위한 테스트 라이브러리가 Java에는 있습니다). 설계서의 아키텍처 기술로부터 이 테스트를 만들어 두고, 생성된 코드에 대해 실행합니다. 위반 사항이 있으면 테스트가 실패(Fail)하므로, 기계적으로 실패 처리를 할 수 있습니다.
- 코드 품질의 정답: SonarQube의 Quality Gate (자체적인 기준을 발명하지 않고, 업계 표준에 책임을 맡깁니다).
공통점은 정답을 먼저 고정한 다음, 생성물을 대조한다는 순서입니다. 생성물을 보고 나서 기준을 만들면, 기준이 생성물에 끌려가게 됩니다. 시험 문제는 답안을 보기 전에 만든다는 당연한 사실을, LLM이 개입하면 의외로 잊게 됩니다.
생성 측과 채점 측은 동일한 설계서를 근원으로 삼으면서도 서로 다른 길을 거쳐 합류합니다. 이러한 분리가 있기에 대조 결과에 의미가 생겨납니다.
요약
- LLM은 대량의 코드를 작성할 수 있지만, 그 정확성을 보증하는 검증 측면의 논의는 아직 부족하다.
- 평가에는 두 가지 대상이 있다. 결과물에는 Gate(합격/불합격), 하네스(Harness)에는 Score(집계 지표) —— 그리고 Gate의 기록을 집계한 것이 Score가 된다.
- 기본 사상은 "비결정적인(Non-deterministic) 생성물을 가능한 한 결정적인(Deterministic) 판정으로 보증한다". LLM에 의한 판정은 기계로 판정할 수 없는 것에 한정한다.
- 정답은 생성 측으로부터 독립시키고, 생성물을 보기 전에 고정한다.
- 다음에는 "생성된 테스트 코드의 품질을 어떻게 측정할 것인가", 그다음에는 "LLM judge 자체를 어떻게 검증할 것인가(평가기의 평가)\
AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기