5.9.2 코드 생성기: 생성된 코드의 구문 오류(Syntax Error) 및 컴파일 가능성 검증
Copilot이나 로우코드(Low-code) 플랫폼의 자연어 질의(NL2SQL, NL2Code) 기능을 테스트할 때, 모델이 생성한 코드의 정확성을 평가하는 것은 이메일 요약보다 훨씬 까다롭다. 코드는 띄어쓰기 하나, 괄호 하나만 누락되어도 시스템을 다운시키는 치명적인 결함이 되기 때문이다.
초보적인 개발자들은 LLM이 생성한 코드를 정답 텍스트와 1:1로 비교(assert result == expected_code)하는 실수를 저지른다. 하지만 LLM이 변수명을 user_id 대신 userId로 짓거나, 따옴표의 종류를 바꾸거나, 개행 문자를 조금만 다르게 출력해도 이 오라클은 무참히 실패(Flaky Test)하게 된다. 코드 생성기의 결정론적 오라클은 텍스트의 표면이 아니라, **‘코드의 구조적 무결성(Structural Integrity)’**을 채점해야 한다.
1. 정적 분석기(AST)를 활용한 Syntax Error 오라클
가장 가볍고 확실한 첫 번째 방어선은 파이썬 내장 라이브러리인 ast(Abstract Syntax Tree) 모듈이나 eslint 같은 Linter를 활용하여 코드가 컴파일 가능한 문법(Valid Syntax)을 띠고 있는지 유닛 테스트 레벨에서 파싱해 보는 것이다. 이 과정은 실제 코드를 메모리에서 실행(Eval)하지 않으므로 보안 위험이 전혀 없고 속도가 매우 빠르다.
import ast
import pytest
def test_python_code_generation(llm_code_generator):
prompt = "리스트에서 짝수만 필터링하는 파이썬 함수를 짜줘."
# LLM이 생성한 원시 텍스트 (마크다운 백틱 등을 제거한 순수 코드)
generated_code = llm_code_generator.generate_code(prompt)
# 결정론적 오라클: 구문 오류(Syntax Error)가 없는 올바른 파이썬 코드인가?
try:
ast.parse(generated_code)
is_syntax_valid = True
except SyntaxError as e:
is_syntax_valid = False
assert is_syntax_valid, f"생성된 코드에 치명적인 문법 오류가 있습니다:\n{generated_code}"
이 오라클을 통과했다는 것은, LLM이 최소한 괄호의 짝을 맞추고 들여쓰기(Indentation) 규칙을 준수하여 파서(Parser)를 만족시키는 ’진짜 코드’를 만들어 냈음을 결정론적으로 증명한다.
2. 정규 표현식 및 추상 구문 트리를 통한 필수 API 호출 검증
문법이 올바르다 해도, 모델이 우리가 원하는 핵심 라이브러리나 회사 내부의 사내 API를 호출하지 않고 자기 마음대로 내장 함수를 짜버렸다면 쓸모가 없다.
오라클은 ast.walk를 사용하여 추상 구문 트리를 순회하거나, 정밀한 정규표현식을 통해 “우리가 지정한 특정 함수나 메서드가 코드 내부에서 실제로 호출(Call)되었는지“를 검증해야 한다.
def test_sql_generation_security(llm_sql_generator):
prompt = "사용자 테이블에서 마지막 로그인 날짜를 조회해줘."
generated_sql = llm_sql_generator.generate(prompt).upper()
# 결정론적 보안 오라클: 모델이 환각 혹은 악의로 DML/DDL 명령어를 주입했는가?
forbidden_keywords = ["DROP", "DELETE", "UPDATE", "INSERT", "TRUNCATE"]
for keyword in forbidden_keywords:
assert keyword not in generated_sql, \
f"치명적 오류: 읽기 전용 SQL 생성기에 금지된 조작 명령어({keyword})가 포함되었습니다."
# 필수 구성 요소 검사
assert "SELECT" in generated_sql
assert "FROM" in generated_sql
3. 코드 격리 실행 단언(Code Execution Assertion)의 딜레마
구문 검사와 키워드 검사만으로 로직의 논리적 결함(예: 짝수가 아닌 홀수를 필터링하는 알고리즘)까지 잡아낼 수는 없다. 이를 위해서는 격리된 샌드박스(Sandbox)나 도커(Docker) 단위 테스트 컨테이너 환경에서 코드를 직접 실행(Eval/Exec)하고 그 출력값을 기대값과 비교해야 한다.
하지만 이 실행 검증 기법은 보안 취약점 격리 환경 구축, 과도한 런타임 소모, 그리고 유닛 테스트 로직 자체의 비대화를 초래한다. 따라서 책의 이 장에서 정의하는 ‘유닛 테스트 기반’ 오라클의 궁극적인 책임은 코드를 직접 실행하는 것이 아니라, **“후속 파이프라인(실행 환경)으로 넘길 수 있을 만큼 문법과 뼈대가 완벽한 컴파일 가능성(Compilability)을 보장하는 것”**까지로 철저히 제한되어야 한다.
테스트의 경계를 명확히 긋는 것이야말로 불필요한 결합도를 낮추는 결정론적 설계의 미학이다.