AI 코드가 테스트를 통과하고도 틀리는 이유

AI 코드가 테스트를 통과하고도 틀리는 이유

타입 체커와 린터를 속이는 'well-formed but untrue' 버그—AI-First 팀에 2차 검증 레이어가 필요한 이유

AI 코드 품질 도메인 검증 well-formed bug AI 코드 리뷰 비즈니스 로직 검증 CI 파이프라인 AI-First 워크플로우
광고

문법은 맞고, 사실은 틀렸다

AI가 생성한 코드가 CI를 통과하고, 타입 체커를 통과하고, 코드 리뷰도 통과한 뒤 프로덕션에서 조용히 틀린 결과를 내고 있다면? 이건 가상의 시나리오가 아니다. dev.to에 공개된 두 개의 실증 사례는 이 문제가 얼마나 구조적인지를 날카롭게 보여준다.

케이스 1: 이메일은 발송됐지만 아무도 받지 못했다

한 개발자는 프로덕션 코드에서 잘못된 이메일 도메인을 발견했다. 하나가 아니라 여섯 개였고, 수개월 동안 아무도 몰랐다. AI가 생성한 코드에는 자신이 소유하지 않은 .com 도메인, .io 도메인, 그리고 이메일 서비스 제공자의 샘플 튜토리얼 도메인(noreply@resend.dev)이 그대로 박혀 있었다.

타입 체커 입장에서 "alerts@brand.io"는 완벽한 문자열이다. 번들러는 파일이 파싱된다는 사실만 안다. 테스트는 메일 클라이언트를 목킹하기 때문에 실제 발신 도메인을 볼 수 없다. 배포 시스템은 함수가 컴파일되고 업로드됐다는 사실만 확인한다. 툴체인의 어떤 단계도 '이 도메인이 우리 조직 소유인가'라는 사실을 알지 못한다.

문제의 본질은 AI 모델이 거짓말을 한 게 아니라는 점이다. brand.com은 실제로 존재하는 도메인이다. AI는 맥락상 가장 그럴듯한 토큰 시퀀스를 생성했을 뿐이고, 그 '그럴듯함'이 정확히 함정이었다. 브랜드명이 X라면 x.com은 압도적으로 그럴듯한 추론이다. 이메일 제공자의 발신 필드라면 공식 문서의 예제 주소가 가장 그럴듯하다. 이걸 리뷰 단계에서 잡아내려면 이미 정답을 알고 있어야 한다. 리뷰어도, 툴도, 그 정답을 모른다.

케이스 2: 최적화가 데이터 무결성을 파괴했다

두 번째 사례는 재고 관리 시스템에서 나왔다. 12년간 고부하 ERP를 개발해온 엔지니어가 dev.to에 공개한 사례인데, AI 어시스턴트와 주니어 개발자가 반복적으로 저지르는 실수 패턴이 놀랍도록 일관적이다.

재고 잔량 조회를 SUM(quantity)나 윈도우 함수(ROW_NUMBER())로 구현하면 교과서적으로는 맞다. 문제는 수백만 건 데이터에서 이 쿼리가 MySQL/MariaDB에 치명적인 filesort를 유발한다는 것이다. 이를 피하기 위해 실무에서는 LIMIT 9999999999라는 의도적인 '해킹'을 사용한다. 옵티마이저가 정렬을 건너뛰지 못하도록 강제하는 트릭이다.

AI 어시스턴트는 이 코드를 보면 즉시 '클린업'하려 한다. LIMIT 9999999999는 명백한 냄새나는 코드처럼 보이고, ROW_NUMBER() 윈도우 함수는 교과서가 권장하는 패턴이다. AI는 둘 다 알고 있다. 그래서 확신을 갖고 '개선'한다. 그 결과는? 쿼리 속도는 일시적으로 빨라지고, 며칠 후 재고 잔량이 틀려진다. MariaDB가 정렬 없이 GROUP BY를 수행해 랜덤 행을 반환하기 때문이다. 코드는 문법적으로 완벽하고, 타입도 맞고, 실행도 된다. 결과만 틀렸다.

두 사례가 동시에 가리키는 것

이 두 케이스는 표면적으로 달라 보이지만 동일한 구조적 문제를 드러낸다. AI는 도메인 컨텍스트 없이 통계적으로 그럴듯한 답을 생성한다. 그리고 기존 툴체인은 그 '그럴듯함'이 실제로 사실인지를 검증할 수단을 갖고 있지 않다.

타입 시스템은 값의 형태를 검사한다. 비즈니스 로직 레이어의 사실 여부는 검사하지 않는다. 정적 분석은 문법을 검사한다. 도메인 지식을 요구하는 의미론적 정확성은 검사하지 않는다. 테스트는 우리가 명시한 assertion을 검사한다. 우리가 아직 모르는 가정의 위반은 잡아내지 못한다.

2차 검증 레이어를 설계해야 한다

첫 번째 사례의 개발자가 찾은 해법은 실용적이다. 소스 전체에서 URL 호스트와 이메일 도메인을 추출하고, 선언된 허용 목록과 비교해 불일치 시 실패하는 스크립트를 만들었다. 20분 작업이었고, 손으로 찾은 6개 외에 놓쳤던 1개를 추가로 발견했다.

여기서 설계 원칙 두 가지가 중요하다. 첫째, 허용 목록은 반드시 리포지토리 안에 있어야 한다. AI 에이전트가 접근할 수 있는 설정 파일에 두면, 에이전트가 조용히 목록을 확장할 수 있다. 리포에 있어야 변경이 diff로 남고, diff는 리뷰를 받는다. 둘째, 실행 위치를 의도적으로 설계해야 한다. 로컬 pre-push 훅만으로는 충분하지 않다. AI 에이전트가 직접 커밋하고 푸시하는 환경에서는 로컬 훅이 정확히 그 코드를 보지 못한다. 스케줄 기반으로 원격 브랜치에도 동일 스크립트가 실행돼야 한다.

두 번째 사례의 시사점은 다른 방향에서 온다. 재고 시스템의 RDBMS 물리적 특성, 음수 재고 처리, 소급 문서 입력의 재계산 로직—이것들은 어떤 범용 AI도 프롬프트 한 번으로 올바르게 생성할 수 없다. 12년의 운영 경험이 만들어낸 도메인 지식이기 때문이다. 저자는 이 지식을 시스템 프롬프트로 명문화하는 방식을 제안한다. AI에게 컨텍스트를 주지 않은 채 코드 생성을 맡기면, AI는 교과서 해법을 자신 있게 내놓는다.

팀 리드가 지금 당장 해야 할 것

AI-First 워크플로우에서 이 문제는 더 자주, 더 조용하게 발생한다. 개발자가 직접 타이핑할 때는 대시보드나 DNS 레코드를 눈으로 읽어 값을 입력한다. AI가 생성할 때는 맥락상 가장 그럴듯한 값을 추론한다. 두 경로의 결과물은 코드 레벨에서 구분되지 않는다.

내가 팀에 권고하는 방향은 세 가지다. 도메인 상수의 명시적 선언: 코드베이스가 접촉할 수 있는 도메인, 엔드포인트, 계정 ID를 한 곳에 열거하고 이를 검증 기준으로 삼는다. 이 목록을 작성하는 과정 자체가 '우리 시스템이 무엇과 대화하는가'를 처음으로 명확히 하는 작업이 된다. 도메인 특화 컨텍스트의 문서화: 비즈니스 로직의 비자명한 결정들—특히 'why'가 코드에 드러나지 않는 것들—을 AI가 읽을 수 있는 형태로 명문화한다. AI 생성 코드의 의미론적 검증 게이트: 타입 체크와 린트 이후에, 비즈니스 규칙 레벨의 검증이 CI 파이프라인 안에 존재해야 한다.

이 문제는 AI가 나빠서가 아니다

마지막으로 냉정하게 짚어두자. 이건 AI 도구의 실패가 아니다. 타입 체커가 사실을 검증하지 않는 것이 설계 결함이 아닌 것처럼, AI가 도메인 진실을 모르는 것도 결함이 아니다. 문제는 AI 생성 코드를 기존 검증 레이어만으로 충분히 검증될 것이라 가정하는 운영 방식이다.

'well-formed but untrue'—문법적으로 올바르지만 사실은 틀린 값. 이 카테고리의 버그는 AI 코딩 도구의 사용량이 늘수록 비례해서 증가한다. 2차 검증 레이어는 선택이 아니라 AI-First 워크플로우의 전제 조건이다.

출처

더 많은 AI 트렌드를 Seedora 앱에서 확인하세요