AI 생성 코드를 어떻게 리뷰할까? 테스트가 모두 통과해도 반례와 증거로 검증하기
Vibe Coding은 코드 생산 속도를 높였지만 리뷰 부담까지 같은 속도로 줄여 주지는 않습니다. LLM은 깔끔한 diff를 빠르게 만들 수 있지만, 자연스러운 설명이나 테스트의 전체 통과만으로 운영 환경에 안전한 구현이라고 증명되지는 않습니다.
사례는 이 블로그 프로그램의 애니메이션 터미널 SVG를 위한 빌드 전 검증기입니다. 제한된 XML/CSS 규칙, manifest 경로와 SHA-256이 실제 파일과 일치하는지, 여덟 언어 글이 화면에 보이는 데모 이미지를 참조하는지 확인합니다. 고정 canonical SVG는 정상 입력용 fixture입니다. 네 가지 결함은 병합 전 격리 테스트에서 발견됐으며, 증거 범위는 이 검증기의 오프라인 검사에 한정됩니다.
초기 테스트는 8/8 통과했지만 독립 리뷰에서 잘못된 허용 두 건이 발견됐습니다. 첫 수정 뒤 10/10 통과했으나 다음 리뷰에서 두 건이 더 발견됐습니다. 최종 고정 버전은 주요 테스트 12/12 통과했고, 독립 리뷰어가 추가 공격 탐침 네 개를 실행해 모두 거부됐습니다. JavaScript 파일 세 개도 node —check를 통과했습니다.
범위와 불변 조건을 고정하고, 최소 반례를 만들고, 실제 실패가 관찰된 경우에만 RED로 기록하고, 정확한 commit에서 양성·음성 사례를 독립 재검증한 뒤 위험에 따라 판단합니다. 로직 테스트는 빌드, 플랫폼 검사, 런타임 검증을 대신하지 않습니다. 고위험 변경은 범위, Base/Head commit, 불변 조건, 정상 사례, 반례, 실제 명령과 환경, 미검증 경계, 결론을 작은 증거 카드에 기록할 수 있습니다. 검증기는 블로그 저장소의 빌드 전 구성요소이며 별도 배포되는 범용 CLI가 아닙니다. 리뷰 시간 절감도 측정하지 않았습니다.
공개 저장소에는 비식별 처리된 합성 반례와 실행 가능한 교육용 테스트만 포함됩니다. 이 블로그 프로그램의 운영용 검증기, 실제 수정 코드, 내부 아키텍처는 포함하지 않습니다. 본문의 8/8→10/10→12/12는 원래 엔지니어링 작업의 오프라인 검증 결과이며, 공개 테스트는 네 가지 결함 유형만 재현하고 운영 구현을 복제하지 않습니다.
왜 8/8 테스트를 통과하고도 잘못된 입력을 허용했을까?
첫 번째 반례는 허용된 이미지와 승인되지 않은 다른 글의 리소스 참조를 한 줄에 넣었습니다. 기존 로직은 허용된 이미지 태그 하나를 발견하면 줄 전체를 통과시켰습니다. 독립 리뷰는 다른 slug에 실제 SVG 파일을 만들고 여덟 언어 글의 정상 이미지와 같은 줄에 Markdown 참조를 추가했습니다. 파일 부재로 인한 가짜 실패를 배제하기 위해 대상도 실제로 만들었습니다. 기존 구현은 posts_checked=8로 성공을 반환했습니다.
<img src="/assets/terminal-demo.svg" alt="Terminal demo" width="356" height="137" /> 
두 번째 반례는 파일 시스템 경쟁 조건입니다. 검사한 경로와 최종적으로 열린 파일이 달라질 수 있습니다. checkout 안의 원래 경로에는 잘못된 바이트가 있고 외부에는 기대 SHA-256과 일치하는 canonical SVG가 있었습니다. 부모 디렉터리를 외부 디렉터리로 향하는 symlink로 바꾸자 기존 구현은 외부 파일을 읽고 잘못 허용했습니다. 우회된 것은 해시가 아니라 파일 출처 제약입니다.
최종 Linux 경로는 신뢰된 checkout 루트를 열고 이미 열린 부모 디렉터리 파일 디스크립터를 기준으로 경로를 해석합니다. 중간 요소는 디렉터리여야 하며 O_NOFOLLOW를 사용하고 최종 파일의 symlink도 거부합니다. 파일 종류, 링크 수, 크기를 검사하고 읽기 전후의 device, inode, 크기를 비교한 뒤 digest를 확인합니다. Linux /proc/self/fd를 사용하고 procfs가 없으면 fail closed합니다. 범위는 신뢰된 checkout 루트와 procfs를 전제로 하며 임의의 동일 UID 또는 호스트 수준 공격자는 포함하지 않습니다. Linux openat2(2) 문서를 참고하세요.
두 번째 라운드: 해시는 맞아도 콘텐츠가 보이지 않을 수 있다
첫 수정 뒤 테스트는 10/10 통과했습니다. 세 번째 반례는 reduced-motion 설정에서 마지막 프레임이 보이는지에 관한 문제입니다. SVG 프레임은 기본적으로 투명하고 애니메이션이 내용을 드러냅니다. 움직임 줄이기 규칙이 애니메이션만 멈추고 마지막 프레임을 표시하지 않으면 화면이 비어 있을 수 있습니다.
.f { opacity: 0; }
@media (prefers-reduced-motion: reduce) {
#f0 { animation: none; opacity: 1; }
#f3 { animation: none; } /* 최종 프레임은 투명 */
}
테스트는 SVG SHA-256, 바이트 크기, content-addressed 파일명, canonical manifest를 다시 계산해 주변 검증을 유지했습니다. 검증 대상은 “최종 결과가 보여야 한다”는 계약입니다. MDN의 prefers-reduced-motion를 참고하세요.
네 번째 반례는 Markdown 주석입니다. 기존 검증기는 정규식으로 이미지 태그를 세면서 HTML 주석을 제외하지 않았습니다. 유일한 이미지를 주석으로 감싸면 소스에 태그는 남아도 페이지에는 보이지 않지만 기존 로직은 이미지 한 장으로 계산했습니다. CommonMark 0.31.2의 HTML block 규칙에 따르면 주석 안의 태그는 보이는 이미지가 아닙니다.
<!-- <img src="/assets/demo.svg" alt="Terminal demo" /> -->
테스트 결과를 정확한 버전에 연결하기
| 코드 버전 | 주요 테스트 | 이후 독립 리뷰 |
|---|---|---|
| 초기 버전 | 8/8 통과 | SEC-01, SEC-02 잘못된 허용 |
| 1차 수정 버전 | 10/10 통과 | SEC-03, SEC-04 잘못된 허용 |
| 최종 오프라인 검증 버전 | 최상위 테스트 12/12 통과 | 별도 공격 탐침 네 개 모두 거부 |
최종 12개 테스트에는 SEC-01~04 회귀 테스트가 포함됩니다. 추가 네 탐침은 12/12에 포함되지 않으므로 16/16으로 합산하면 안 됩니다. JavaScript 파일 세 개는 node —check를 통과했지만 이는 문법 검사입니다.
뒤의 두 회귀 테스트는 소스 수정 전에 커밋됐지만 테스트 전용 커밋에서 전체 suite를 실행하지 않았습니다. 따라서 전체 10/12 RED 기록은 없습니다. 확인 가능한 것은 이전 검증기에서 잘못된 허용을 독립적으로 재현했고 최종 고정 버전의 주요 테스트와 추가 탐침이 통과했다는 점입니다.
동작 증거가 안정된 뒤 복잡도를 검토합니다. helper, 분기, 설정, 호출 지점을 따라가며 각 요소가 어떤 불변 조건을 보호하는지, 중복인지, 실제 사용되는지 확인합니다. 코드가 짧다고 더 단순한 것은 아닙니다. 경로 보호를 제거하면 닫힌 경쟁 조건이 다시 생길 수 있습니다.
인터랙티브 증거 패널
합성 반례를 선택해 입력, 불변 조건, 재현된 실패와 증거 범위를 확인하세요.
선택한 사례: SEC-01 · 존재는 배타성을 뜻하지 않음
SEC-01 · 존재는 배타성을 뜻하지 않음
- 입력 조건
- 허용된 이미지와 승인되지 않은 다른 글의 리소스 참조가 함께 있습니다.
- 지켜야 할 불변 조건
- 모든 관련 참조가 규칙을 충족해야 하며 허용된 참조 하나로 전체 안전을 증명할 수 없습니다.
- 합성 예시가 재현하는 실패
- 기존 로직은 허용된 이미지 하나를 찾자 줄 전체를 승인해 추가 참조를 놓쳤습니다.
- 기대되는 올바른 동작
- 모든 참조를 검사하고 경계를 벗어난 참조가 있으면 거부합니다.
- 원래 리뷰 단계
- 첫 번째 독립 리뷰에서 발견
- 재사용할 리뷰 교훈
- 유효·무효 내용이 함께 있는 최소 반례로 전체 입력 검사를 확인합니다.
SEC-02 · 검사와 사용 사이의 경쟁 조건
- 입력 조건
- 경로 검사 후 파일을 열기 전에 상위 디렉터리나 심볼릭 링크를 바꿉니다.
- 지켜야 할 불변 조건
- 검증한 대상과 접근 대상이 같아야 하며 의도한 파일 시스템 경계를 지켜야 합니다.
- 합성 예시가 재현하는 실패
- 경로를 검사한 뒤 다시 열면 검사와 사용 사이에 대상이 바뀔 수 있습니다.
- 기대되는 올바른 동작
- 검증과 접근에서 경계를 유지해야 합니다. 이 합성 예시는 운영 수정 구현을 주장하지 않습니다.
- 원래 리뷰 단계
- 첫 번째 독립 리뷰에서 발견
- 재사용할 리뷰 교훈
- 검증과 사용 사이에 다른 작성자가 바꿀 수 있는 상태가 있는지 확인합니다.
SEC-03 · 동작 감소 모드에서도 마지막 프레임 표시
- 입력 조건
- SVG 프레임은 투명하게 시작하며 동작 감소 설정에서는 애니메이션이 멈춥니다.
- 지켜야 할 불변 조건
- 애니메이션을 꺼도 중요한 최종 콘텐츠가 숨겨지면 안 됩니다.
- 합성 예시가 재현하는 실패
- 마지막 프레임을 보이지 않고 애니메이션만 멈추면 화면이 비어 보일 수 있습니다.
- 기대되는 올바른 동작
- 동작 감소 모드에서도 읽을 수 있는 최종 상태를 바로 보여야 합니다.
- 원래 리뷰 단계
- 두 번째 독립 리뷰에서 발견
- 재사용할 리뷰 교훈
- 접근성 설정을 별도 입력으로 보고 콘텐츠가 계속 보이는지 확인합니다.
SEC-04 · 주석은 보이는 콘텐츠가 아님
- 입력 조건
- HTML 주석 안에 이미지 요소처럼 보이는 문자열이 있습니다.
- 지켜야 할 불변 조건
- 소스 텍스트와 브라우저가 실제로 렌더링하는 요소는 다릅니다.
- 합성 예시가 재현하는 실패
- 태그 수 세기는 주석 안 이미지를 보이는 이미지로 셀 수 있습니다.
- 기대되는 올바른 동작
- 주석 노드를 제외하고 구조를 파싱해 실제 렌더링 요소만 판단합니다.
- 원래 리뷰 단계
- 두 번째 독립 리뷰에서 발견
- 재사용할 리뷰 교훈
- 가짜 태그를 주석 안에 넣어 구조를 검사하는지 확인합니다.
공개 합성 교육 증거
공개 저장소는 합성 결함 패턴만 재현합니다. 테스트 통과는 실패 재현을 뜻하며 운영 수정의 성공을 뜻하지 않습니다.
고정 버전 합성 교육 저장소 열기원래 엔지니어링 오프라인 요약
원래 기록: 8/8 → 10/10 → 12/12. 독립 탐침 네 개는 별도로 실행했으며 12/12에 포함되지 않습니다.
검증 범위
범위는 Node.js v22.16.0 / Linux x64의 오프라인 검증기입니다. 악의적 동시 쓰기에 대한 네이티브 macOS와 전체 사이트 런타임은 미검증입니다.
관련 글: Mutation Testing, OpenQodex pre-push 리뷰의 fail-open 경계, Diffle 리뷰 댓글 앵커。
테스트 통과와 운영 승인 사이에 남은 것
최종 독립 검증은 Node.js v22.16.0, Linux x64의 오프라인 검증기를 대상으로 했습니다. 주요 테스트 12/12 통과, 추가 공격 탐침 네 개 모두 거부, canonical SVG·manifest·8개 언어 바인딩의 정상 사례 통과, JavaScript 파일 세 개의 node —check 통과가 확인됐습니다.
이 증거는 해당 환경과 고정 규칙에서 알려진 네 가지 잘못된 허용이 수정됐다는 제한된 결론을 뒷받침합니다. 블로그 전체가 운영 준비를 마쳤다는 뜻은 아닙니다. 악의적인 동시 파일 시스템 작성자에 대한 네이티브 macOS 동작은 미검증이며 전체 사이트 빌드와 최종 페이지 실행 확인도 오프라인 테스트 범위 밖입니다.
실제 승인선은 위험에 따라 나눠야 합니다. 단위 테스트는 포함된 동작을, 정적 검사는 특정 패턴을, 빌드와 통합 테스트는 모듈 결합을, 플랫폼과 런타임 검사는 배포 경계를 확인합니다. 중요한 경계가 검증되지 않았다면 부분 성공을 전체 운영 준비 완료라고 말하면 안 됩니다.
계약과 반례로 의심할 동작을 찾고 정확한 버전의 테스트와 독립 증거로 다음에 깊이 읽을 부분을 결정합니다. 사람의 판단을 없애는 것이 아니라 “괜찮아 보인다”를 증거와 범위가 있는 검증 가능한 리뷰 결정으로 바꾸는 것입니다.