WEBBOOK CHAPTER

Codex로 구축하는 AI 개발팀: 11장. 기계식 품질 게이트

11장. 기계식 품질 게이트

다음은 여러 현장에서 반복된 패턴을 합친 합성 사례로, 등장하는 숫자는 설명을 위한 예시 수치다.

에이전트가 “테스트가 모두 통과했다”고 보고했다. 실제로는 새 테스트 파일의 구문 오류 때문에 테스트 러너가 해당 디렉터리를 발견하지 못했고 실행된 테스트 수는 0이었다. 다른 변경은 기능 테스트를 통과했지만 도메인 모듈이 데이터베이스 드라이버에 직접 의존해 아키텍처 경계를 무너뜨렸다.

품질 게이트는 명령 목록이 아니다. 특정 위험을 대표하는 검사, 신뢰할 수 있는 판정, 실패를 고칠 수 있는 피드백, 우회 방지의 묶음이다.

이번 장의 약속

  • 위험을 기능·계약·구조·보안·운영 게이트에 연결한다.
  • 빠른 검사에서 넓은 검사로 피드백 순서를 설계한다.
  • 아키텍처 규칙을 실행 가능한 구조 테스트로 만든다.
  • 불안정 테스트, 0개 실행, 게이트 우회를 탐지한다.

게이트는 어떤 위험을 막는가

위험 대표 게이트 증거
구문·형식 오류 파서, 타입, 린트 진단 위치와 코드
제품 규칙 오류 단위·속성 테스트 수용 기준별 결과
API 호환성 파괴 계약 테스트 스키마 diff, 소비자 사례
모듈 경계 침범 구조 테스트 금지 의존 경로
비밀·취약 패턴 비밀/정적 보안 검사 규칙 ID와 위치
실제 사용 흐름 파괴 통합·브라우저 테스트 시나리오 단계와 캡처
성능 회귀 고정 부하 비교 기준선·분포·환경
배포 불능 빌드·패키지 검사 재현 산출물과 해시

검사 수보다 위험 연결이 중요하다. 어떤 게이트도 대표하지 않는 고위험 항목은 사람 리뷰나 새 자동 검사가 필요하다.

빠른 실패, 충분한 최종 증거

실행 중 피드백 루프와 통합 전 최종 판정을 나눈다.


행동 후 초고속 검사: 경로 정책 → 구문/타입 → 관련 단위 테스트
작업 완료 후보: 구조 → 전체 단위 → 계약 → 보안
통합 후보: 통합/브라우저 → 패키지 → 성능/운영 위험 검사
경로 정책, 구문과 타입, 관련 단위 테스트, 구조, 계약, 보안, 통합과 브라우저, 패키지, 성능과 운영 위험 검사가 아래에서 위로 넓어지는 품질 게이트 사다리.
경로 정책, 구문과 타입, 관련 단위 테스트, 구조, 계약, 보안, 통합과 브라우저, 패키지, 성능과 운영 위험 검사가 아래에서 위로 넓어지는 품질 게이트 사다리.

그림 11-1. 빠른 관련 검사는 수정 루프를 짧게 하고, 통합 전 전체 검사는 선택기가 놓친 위험을 보완한다. 그림은 현장 공장의 목표 구성이며 로컬 예제의 실제 게이트는 구문·구조·필수 파일·인수 테스트다.

에이전트가 사소한 구문 오류를 고치기 위해 전체 브라우저 테스트 20분을 기다리게 하지 않는다. 반대로 관련 테스트만 통과했다고 완료하지 않는다.

게이트 계약

최종 보고에는 두 층이 있습니다. summary.json은 실행 statusfinalGates를 포함하고, gate-results.json은 같은 최종 상태를 statusgates로 저장합니다. 모든 게이트가 공통으로 갖는 필드는 namepassed뿐입니다. required-filesarchitecturecheckedfailures를, acceptance-testscode, checked, stdout, stderr를 추가합니다. 아래는 작업 단계의 구조 게이트에서 먼저 실패해 finalGates가 비어 있는 architecture-violation 실행의 실제 summary.json 발췌입니다.

실제 산출물 발췌 — 원본: .factory/runs/architecture-violation/summary.json; 생성: npm run demo:architecture-violation(기대 종료 코드 1).


{
  "runId": "architecture-violation",
  "status": "RED",
  "tasks": [
    {
      "id": "reverse-dependency",
      "status": "failed",
      "lastFailure": {
        "gate": "architecture",
        "issues": [
          {
            "file": "src/domain/order.mjs"
          }
        ]
      }
    }
  ]
}

실제 messagesrc/domain/에서 src/application/로 향하는 import를 금지한다고 설명합니다. 같은 판정은 events.jsonlgate.completed에도 남지만 상세 필드는 게이트 종류에 따라 다릅니다. 운영 확장에서는 공통 스키마에 게이트 버전, 실행 규칙이나 명령, 소요 시간, 이유 코드와 별도 artifact URI를 추가합니다. 현재 예제가 이미 그 필드를 저장한다고 주장하지 않습니다.

아키텍처를 희망이 아니라 검사로

저장소 지도에서 정한 의존 방향을 기억하자.


api → application → domain
store → domain ports
domain ─X→ api, store, factory

실제 src/quality-gates.mjs는 상대 import를 읽고 명세의 architectureLayers[].mayImport와 비교합니다. 핵심 판정은 다음과 같습니다.

실행 가능 — 원본: 03-labs/src/quality-gates.mjs; 검증: cd 03-labs && npm test.


const targetLayer = layerFor(targetRelative, layers);
if (sourceLayer && targetLayer &&
    !sourceLayer.mayImport.includes(targetLayer.path)) {
  failures.push({
    file: sourceRelative,
    message: `${sourceLayer.path} 계층은 ` +
      `${targetLayer.path} 계층을 import할 수 없습니다.`
  });
}

실제 파서는 언어의 모듈 문법을 정확히 다뤄야 한다. 이 책의 무의존성 예제는 제한된 import 문법을 사용하고 그 한계를 문서화한다. 제품 저장소에서는 해당 언어의 공식 파서나 검증된 분석 도구를 선택한다.

구조 게이트 후보:

  • 모듈 의존 방향
  • 공개 API 외의 내부 파일 import 금지
  • 순환 의존
  • 데이터 접근이 허용된 계층
  • 파일 크기·복잡도 경고
  • 마이그레이션 없이 스키마 변경 금지
  • 정책·테스트 삭제에 대한 승인 요구

모든 스타일 취향을 필수 게이트로 만들지 않는다. 위험과 변경 비용이 큰 규칙을 우선한다.

테스트가 실제로 실행됐는지 검사한다

종료 코드 0만 보지 않는다.

  • 발견된 테스트 수가 최소 기대치 이상인가?
  • 필수 수용 기준 ID가 결과에 존재하는가?
  • 건너뛴 테스트가 허용 목록 안인가?
  • 테스트 러너 설정이 변경되지 않았는가?
  • 변경된 제품 코드에 관련 게이트가 실행됐는가?

설계 예(실행용 아님) — 원본: 이 장의 강화 스키마 설명; 명령: 없음.


{
  "runnerExit": 0,
  "tests": 0,
  "requiredAcceptanceIds": ["AC-01", "AC-02"],
  "observedAcceptanceIds": [],
  "verdict": "failed",
  "reason": "NO_TESTS_EXECUTED"
}

테스트와 게이트 자체를 보호한다

작업자가 실패를 피하려고 기대값을 느슨하게 만들거나 검사를 삭제할 수 있다. 제품 코드와 판정 기준은 다른 신뢰 경계로 다룬다.

  • 명세·정책·핵심 회귀 테스트 변경은 별도 작업과 승인을 요구한다.
  • 작업 계약의 허용 경로에서 게이트 구성을 제외한다.
  • 기준 커밋과 비교해 테스트 삭제·skip 증가를 표시한다.
  • 구현 작업자가 바꾼 테스트는 독립 리뷰에서 명세 의미를 확인한다.
  • 숨은 평가 세트나 생성 입력과 독립된 사례로 과적합을 점검한다.

숨은 테스트는 개발자를 속이는 수단이 아니다. 공개 명세에 있는 규칙을 다른 사례로 검증해 구현이 예시 값에만 맞춰졌는지 확인한다.

속성 테스트로 넓은 입력을 본다

예시 테스트와 함께 불변 조건을 검사할 수 있습니다. 다음은 현장 확장 예시이며 현재 03-labs의 실행 파일을 인용한 것은 아닙니다.

설계 예(실행용 아님) — 원본: 이 장의 속성 테스트 설명; 명령: 없음.


for (let seed = 1; seed <= 500; seed += 1) {
  const order = generateOrder(seed);
  const total = calculateTotal(order);
  assert.ok(Number.isSafeInteger(total));
  assert.ok(total >= 0, `seed=${seed}`);
}

실패 시 seed와 최소 재현 입력을 기록한다. 무작위 데이터가 실행마다 달라 재현되지 않게 하지 않는다. 생성기의 범위가 실제 도메인과 맞는지도 검토한다.

변경 기반 선택과 최종 전체 검사

작업 중에는 변경 파일과 의존 그래프를 바탕으로 관련 테스트를 선택해 빠르게 피드백한다. 선택기가 놓칠 수 있으므로 통합 전에는 정의된 전체 게이트를 실행한다.


src/orders/domain/total.js 변경
  → 주문 domain 단위/속성 테스트
  → 주문 API 계약 테스트
  → domain 구조 검사

통합 후보
  → 전체 필수 게이트

테스트 선택 결과와 이유를 이벤트로 남긴다. “관련 테스트 없음”도 검토 대상이다.

불안정 테스트는 자동 재시도로 숨기지 않는다

같은 입력·환경에서 결과가 흔들리는 테스트는 공장 신뢰를 무너뜨린다. 실패 뒤 자동 재실행해 통과시키면 실제 결함과 불안정성을 구분하지 못한다.

권장 흐름:

  1. 첫 실패를 원래 결과로 보존한다.
  2. 정책이 허용하면 진단용으로 같은 입력을 반복한다.
  3. 결과가 흔들리면 TEST_FLAKE로 분류한다.
  4. 해당 게이트의 신뢰 수준과 격리 정책을 적용한다.
  5. 소유자·기한이 있는 수정 작업을 만든다.

고위험 계약 테스트를 격리했다고 변경을 자동 통과시키지 않는다. 위험에 맞는 대체 증거가 필요하다.

실습 1: 구조 위반을 만든다


npm run test:architecture

먼저 통과를 확인한 뒤 실습용 시나리오로 도메인 코드가 저장 어댑터를 import하게 한다.


npm run demo:architecture-violation

실제 실패의 핵심은 다음과 같습니다.


RED ✗ — 역방향 계층 의존성 차단
src/domain/order.mjs:
src/domain/ 계층은 src/application/ 계층을 import할 수 없습니다.

현재 메시지는 source·forbidden layer를 알려 줍니다. 운영 확장에서는 안정적인 규칙 ID, 경계의 이유, 대표 수정 방향을 추가해 탐색 비용을 줄입니다.

실습 2: 0개 테스트를 성공으로 위장한다


npm run demo:no-tests
missing-acceptance 터미널. 작업 파일은 통합되지만 최종 품질 게이트가 실패해 RED와 실행 실패 메시지로 끝난다.
missing-acceptance 터미널. 작업 파일은 통합되지만 최종 품질 게이트가 실패해 RED와 실행 실패 메시지로 끝난다.

그림 11-2. 실제 실패 fixture는 구현이 생겨도 실행 가능한 수용 기준이 없으면 RED와 종료 코드 1로 끝난다.

수용 기준 없음 보고서 화면. 상태 RED이며 required-files와 architecture는 PASS, acceptance-tests는 FAIL로 표시된다.
수용 기준 없음 보고서 화면. 상태 RED이며 required-files와 architecture는 PASS, acceptance-tests는 FAIL로 표시된다.

그림 11-3. 같은 차단 실행의 보고서는 acceptance-tests FAIL을 다른 게이트 통과와 분리해 보여 준다.

이 명령은 tasks/missing-acceptance.json을 실행합니다. release에 test/*.test.mjs가 하나도 없으면 하니스가 테스트 러너를 성공으로 간주하지 않고 acceptance-testspassed: false, 코드 1, “실행 가능한 인수 테스트가 없습니다”로 판정합니다. 현재 구현의 이유 코드는 자연어 메시지이며 NO_TESTS_EXECUTED라는 구조화 코드까지 제공하지 않습니다.

실습 3: 최신 게이트 결과를 다시 읽는다

먼저 npm run demo 또는 의도된 실패 fixture를 실행한 뒤 최신 gate-results.json을 요약합니다.


npm run evaluate:gates

현재 evaluate:gates는 결함을 새로 주입하지 않고 최신 run의 게이트 이름과 PASS/FAIL을 다시 출력합니다. 따라서 다음 결함 주입 행렬은 현장 확장 과제입니다.

  • 취소 품목을 총액에 포함
  • 통화 누락을 0으로 변환
  • 도메인에서 저장소 직접 import
  • 응답에 내부 원가 필드 노출
  • 테스트 한 개를 skip

별도 fixture를 하나씩 만들고 각 결함이 예상 게이트에서 잡혔는지 행렬을 만듭니다. 하니스 회귀 테스트에는 이미 수용 기준 누락, 역방향 의존, 경로 탈출, 미완성 표식, 간접 지시 사례가 포함돼 있습니다.

결함 예상 게이트 검출 다른 게이트의 우연 검출
취소 품목 포함 AC-02 unit 미구현/목표 contract
금지 의존 architecture 구현됨/검출 없음
내부 원가 노출 contract/security 미구현/목표 없음

우연히 잡힌 결함은 전용 방어가 아니다. 관련 테스트 데이터가 바뀌면 사라질 수 있다.

브라우저와 화면 검증

화면 기능은 DOM 존재만으로 충분하지 않다. 핵심 사용자 흐름에서 상태, 접근성, 시각적 결과를 확인한다.


주문 상세 열기
→ 총액 레이블과 통화 형식 확인
→ 취소 주문 상태 확인
→ 키보드 탐색과 접근 가능한 이름 확인
→ 오류 응답의 사용자 메시지 확인

스크린샷 차이는 글꼴·운영체제·시간 데이터에 민감하다. 고정 데이터, 뷰포트, 글꼴, 애니메이션 비활성화를 사용하고, 픽셀 차이와 의미 기반 DOM/접근성 검사를 함께 둔다. 책의 화면 캡처는 테스트 산출물에서 생성해 독자 결과와 일치시킨다.

왜 실패하는가

테스트 개수를 품질로 본다

같은 정상 경로를 반복하는 테스트 백 개보다 위험 경계를 대표하는 열 개가 낫다. 결함-게이트 행렬로 공백을 찾는다.

모든 게이트를 매 행동마다 실행한다

피드백이 느려 에이전트가 큰 묶음으로 변경한다. 빠른 관련 검사와 최종 전체 검사를 분리한다.

불안정 테스트를 세 번 재시도해 통과시킨다

공장의 성공률이 좋아 보이지만 결함을 숨긴다. 첫 실패와 흔들림을 별도 상태로 남긴다.

아키텍처 규칙을 리뷰어 기억에 맡긴다

변경량이 늘수록 놓친다. 기계적으로 표현 가능한 경계는 구조 테스트로 옮기고 이유를 메시지에 담는다.

운영 판단: 필수와 권고 게이트

필수 게이트는 실패 시 통합을 막는다. 다음 조건에 가까워야 한다.

  • 실제 고위험 결함과 연결된다.
  • 같은 입력에서 충분히 결정론적이다.
  • 실패 원인을 소유자가 해결할 수 있다.
  • 실행 비용이 위험에 비례한다.
  • 우회와 0개 실행을 탐지한다.

새 규칙은 처음에는 관찰 모드로 오탐과 비용을 측정하고, 수정 경로와 소유자가 준비된 뒤 필수로 올린다. 보안상 즉시 차단해야 하는 명백한 비밀 노출 등은 예외다.

연습문제

  1. 팀의 상위 위험 다섯 개를 대표 게이트와 연결하고 공백을 찾으라.
  2. “서비스 계층만 데이터베이스를 쓴다”를 구조 테스트의 입력·판정·오류 메시지로 설계하라.
  3. 관련 테스트 선택기가 놓칠 수 있는 간접 의존 사례를 만들고 최종 검사로 보완하라.
  4. 불안정 테스트를 격리할 때 통합을 계속 허용할 조건과 막을 조건을 정하라.

체크포인트

  • 필수 게이트마다 대표하는 위험과 구조화된 결과가 있다.
  • 테스트 0개, skip 증가, 정책/게이트 변경을 성공으로 처리하지 않는다.
  • 저장소의 핵심 의존 규칙이 구조 테스트로 실행된다.
  • 고정 결함 세트로 게이트의 실제 검출 능력을 평가한다.

다음 장에서는 게이트를 통과한 변경을 독립 리뷰하고, 공급망·비밀·외부 효과를 확인한 뒤 기준선에 안전하게 통합한다.