개발 도구 · 자동화

생성기를 위한 골든 테스트: 결정성·정규화와 무의미한 스냅샷 피하기

AI아키텍트 2026. 8. 1. 14:13

코드 스캐폴더, 템플릿 렌더러, 직렬화기, 포매터, 리포트·문서 생성기 — 이들의 공통점은 입력을 받아 구조화된 출력을 만든다는 것입니다. 이런 생성기에서 가장 무섭고 흔한 사고는 "관계없어 보이는 변경 하나가 출력을 조용히 바꿔 모든 사용처에 퍼지는 것"입니다. 이걸 가장 싸게 잡는 안전망이 골든 테스트 (Golden Test) 입니다. 알려진 정답 출력을 "골든" 파일로 고정해 두고, 매번 생성 결과와 대조하는 방식입니다. 흔히 스냅샷·승인·특성화 테스트와 묶어 부르지만 결이 조금씩 다릅니다 — 스냅샷 테스트는 프레임워크가 저장·비교를 지원하는 구현 방식, 승인 테스트는 사람이 결과를 검토·승인하는 워크플로, 특성화 테스트는 기존 시스템의 현재 동작을 기록하는 목적(골든 방식을 쓸 수 있음)입니다. 이 글은 그 공통 뼈대인 '기준 산출물과 대조'를 다룹니다.

그런데 골든 테스트는 악명 높은 실패 모드 두 가지를 함께 가지고 있습니다. 하나는 비결정성 때문에 이유 없이 깨지는 것(flaky), 다른 하나는 검토 없이 자동 갱신되어 아무것도 못 잡는 것(rubber-stamp)입니다. 이 글은 골든 테스트가 잘 맞는 곳과 아닌 곳, 출력을 흔드는 비결정성을 정규화 (Canonicalization) 로 잡는 법, 골든 갱신을 코드처럼 다루는 규율, 그리고 결정성 자체를 검증하는 클린룸 반복 생성까지 정리합니다.

본문의 예시 코드·경로·값은 합성 예시이며 특정 제품·회사와 무관합니다. 상호 참조와 공식 문서 링크만 실제 URL을 사용합니다.

1. 골든 테스트란 무엇이고, 무엇이 아닌가

골든 테스트는 회귀(regression) 안전망이지 정확성 증명이 아닙니다. "이 출력이 옳다"를 보장하는 게 아니라, "출력이 직전에 승인한 것과 달라졌다"를 알려 줄 뿐입니다. 이 구분이 중요합니다. 골든이 처음부터 틀렸다면, 골든 테스트는 그 틀린 출력을 충실히 지켜 줍니다.

구분 명시적 단언(assertion) 골든 테스트
검증하는 것 "결과가 이 규칙을 만족" "결과가 승인된 스냅샷과 동일"
강점 의도를 코드로 표현 크고 구조적인 출력의 회귀를 싸게 포착
약점 큰 출력엔 단언이 장황 정답 여부는 사람이 최초 생성·모든 갱신에서 판단해야 함
실패 신호 규칙 위반 무엇이든 달라짐(=검토 필요)

그래서 골든 테스트는 명시적 단언을 대체하지 않고 보완합니다. 핵심 로직의 정확성은 단언으로, 큰 출력의 "의도치 않은 변화"는 골든으로 잡는 이중 구조가 건강합니다.

2. 어디에 강한가

골든 테스트가 특히 빛나는 대상은 출력이 크고 구조적이며, 사람이 매번 눈으로 대조하기엔 번거로운 것들입니다.

  • 코드 스캐폴더·생성기 — 프로젝트 초기화, 클라이언트 SDK 생성 등. 템플릿을 고치면 산출물이 예상대로만 바뀌는지.
  • 직렬화·포매터 — JSON/YAML 직렬화, 코드 포매터, 마크다운→HTML 렌더링.
  • 파서·컴파일러 출력 — AST, IR, 진단 메시지.
  • API 계약 스냅샷 — 응답 스키마·예시가 의도치 않게 바뀌는지.
  • 보안·암호 계약 — 정규 바이트열·서명 같은 골든 벡터 (Golden Vector). 이 주제는 폴리글랏 보안 계약 검증에서 Canonical Byte와 함께 자세히 다뤘습니다.

이들의 공통점은 "결과가 정해진 규칙에 따라 결정적으로 나와야 한다"는 기대입니다. 그리고 바로 그 결정성이 골든 테스트의 전제이자 최대 난관입니다.

3. 진짜 문제: 출력을 흔드는 비결정성

골든 테스트가 이유 없이 깨진다면, 대개 출력에 비결정적 (non-deterministic) 요소가 섞인 것입니다. 대표적인 원인들입니다.

- 시간값        : 타임스탬프, 생성일시, 경과시간
- 난수·식별자   : UUID, 랜덤 seed, 자동 증가 ID
- 순서          : 맵/딕셔너리 순회 순서, 파일 목록 순서, 병렬 결과 도착 순서
- 경로          : 절대 경로, 홈 디렉터리, 임시 디렉터리 이름
- 로케일·환경   : 언어·타임존·숫자/날짜 형식, 환경 변수
- 개행·인코딩   : CRLF vs LF, BOM, 트레일링 공백
- 부동소수      : 반올림·표현 차이
- 동시성        : 스레드 스케줄에 따른 출력 순서

이것들을 그대로 두고 전체 출력을 스냅샷하면, 테스트는 코드가 아니라 환경이 바뀔 때마다 깨집니다. 그러면 팀은 곧 골든 테스트를 신뢰하지 않게 되고, "또 그거네" 하며 무시하거나 꺼 버립니다. 비결정성을 다루지 못한 골든 테스트는 금세 신뢰를 잃습니다.

4. 정규화: 이음매 주입과 값 고정

해법은 비교 전에 출력을 정규화(canonicalize) 하는 것입니다. 두 축으로 접근합니다.

(1) 비결정성의 원천을 통제한다 — 이음매(Seam) 주입. 시간·난수 같은 소스를 코드에 하드코딩하지 말고 주입 가능한 이음매로 빼서, 테스트에서 고정값을 넣습니다.

- 시계(clock)를 주입 → 테스트는 고정 시각을 공급
- 난수 생성기(RNG)를 주입하거나 결정적 ID 공급자 사용 → 재현 가능한 난수·ID
  (단순 seed 고정은 그 구현이 실제로 그 RNG를 쓸 때만 유효; OS CSPRNG 기반 UUID는 통제 안 될 수 있음)
- ID 발급기를 결정적 스텁으로 교체

(2) 출력 자체를 정규화한다 — 값 고정·마스킹. 통제하기 어려운 값은 비교 전에 정규화하거나 자리표시자로 마스킹합니다.

- 맵/집합은 키로 정렬해 직렬화(정렬 직렬화)
- 절대 경로 → 저장소 기준 상대 경로
- 개행 LF 통일, 트레일링 공백 제거, 인코딩 UTF-8 고정
- 로케일·타임존을 UTC·고정 로케일로
- 부동소수는 고정 자릿수 포맷
- 그래도 휘발하는 값(UUID·타임스탬프)은 <UUID>·<TS> 같은 자리표시자로 치환

이 정규화는 멱등 인덱싱과 중복 Chunk 방지정규 콘텐츠 해시(canonical content hash)폴리글랏 보안 계약Canonical Byte 와 같은 발상입니다 — "같은 의미면 같은 바이트"를 만든 뒤에야 비교가 의미를 가집니다.

5. 갱신 규율: 골든 diff도 코드처럼 리뷰한다

정당하게 출력을 바꿨다면 골든 기대값도 함께 바꿔야 합니다. 이때 흔한 방식이 환경 변수 스위치입니다.

# 평소: 생성 결과와 골든을 대조 (다르면 실패)
make test

# 의도한 변경일 때만: 골든을 현재 출력으로 재생성
UPDATE_GOLDEN=1 make test   # 그리고 diff를 반드시 커밋 리뷰에 포함

여기서 절대 빠뜨리면 안 되는 규율: 골든 파일의 diff는 소스 코드 diff와 똑같이 리뷰되어야 합니다. 골든이 100줄 바뀌었다면, 리뷰어는 그 100줄이 의도한 변경의 결과가 맞는지를 봐야 합니다. 골든을 버전 관리(VCS)에 넣는 이유가 바로 이것입니다 — 변화가 diff로 드러나 검토 대상이 되기 때문입니다.

6. 고무도장 실패 모드: 무검토 자동 갱신은 무가치

골든 테스트를 죽이는 가장 흔한 방법은 "깨지면 그냥 갱신" 하는 습관입니다. 실패할 때마다 반사적으로 UPDATE_GOLDEN을 돌려 통과시키면, 골든은 "현재 출력"을 무조건 승인하는 고무도장(rubber-stamp) 이 됩니다. 그 순간 골든 테스트는 아무 회귀도 잡지 못합니다.

이를 막는 장치는 문화와 도구 양쪽입니다.

  • 문화 — "골든이 왜 바뀌었는가"를 PR 설명에 적게 한다. 이유를 못 대면 그 변경은 의심 대상이다.
  • 도구 — CI에서는 UPDATE_GOLDEN금지하고(대조만), 갱신은 로컬에서 의도적으로만. 대량 골든 변경에는 리뷰어 추가·경고를 건다.

골든 테스트의 가치는 "깨졌을 때 멈춰서 생각하게 만드는 것"에 있습니다. 멈추지 않고 갱신하면 가치는 0이 됩니다.

7. 취약성 vs 커버리지: 전체 스냅샷의 함정

출력 전체를 통째로 스냅샷하면 커버리지는 넓지만, 사소한 변화에도 깨지는 취약한(brittle) 테스트가 됩니다. 반대로 너무 좁게 잡으면 회귀를 놓칩니다. 균형의 요령은 "안정적인 것만 남기고 휘발하는 것은 마스킹" 하는 것입니다.

예를 들어 아래 스냅샷은 휘발 필드(id, createdAt)를 자리표시자로 치환하고, 의미 있는 비즈니스 필드만 고정했습니다.

{
  "id": "<UUID>",
  "createdAt": "<TS>",
  "name": "invoice",
  "items": [
    { "sku": "A-100", "qty": 2 },
    { "sku": "B-200", "qty": 1 }
  ],
  "total": 3
}

이렇게 하면 id가 매번 달라도 테스트는 안정적이고, total이 3에서 2로 바뀌는 의미 있는 회귀는 즉시 잡힙니다. 다만 주의 — "의미를 보존하는 정규화"와 "정보를 버리는 마스킹"은 다릅니다. id를 모두 <UUID>로 치우면 UUID 형식 오류나 두 객체에 같은 UUID가 배정되는 결함도 함께 지워지므로, 마스킹한 필드는 별도 단언(형식·존재·유일성)으로 따로 검증해야 합니다. 거대한 무편집 스냅샷(수천 줄 HTML 전체 등)은 리뷰가 불가능해 결국 고무도장이 되기 쉬우므로, 대상을 좁히고 정규화하는 편이 거의 항상 낫습니다.

8. 클린룸 반복 생성: 결정성 자체를 검증한다

골든 테스트는 "출력이 골든과 같은가"를 봅니다. 하지만 한 걸음 더 나아가 "생성기가 애초에 결정적인가" 를 직접 검증할 수 있습니다. 방법은 단순합니다.

1) 깨끗한 임시 디렉터리에서 생성 → 산출물 A
2) 또 다른 깨끗한 임시 디렉터리에서 같은 입력으로 생성 → 산출물 B
3) A와 B를 대조 → 원시 출력 비교는 생성기 자체의 바이트 결정성을,
      정규 출력 비교는 정의한 정규형의 결정성을 본다(둘을 분리)

클린룸 반복 생성(clean-room repeat generation) 은 단일 골든 대조보다 강한 보증을 줍니다. 골든과의 일치는 "직전 승인본과 같다"만 말하지만, 반복 생성 비교는 현재 시험한 조건에서 남아 있는 비결정성을 탐지할 가능성을 높여 줍니다(유한한 실행이 모든 환경에서의 결정성을 증명하지는 않습니다). 이때 작업 디렉터리뿐 아니라 프로세스·캐시·환경 변수·로케일·타임존·해시 시드·의존성/도구 버전까지 통제하거나 의도적으로 교란해야 효과가 큽니다. 생성기·스캐폴더라면 이 테스트를 골든 테스트와 함께 두는 것을 권장합니다.

9. CI에서의 골든 테스트

  • 드리프트에 반드시 실패한다 — 골든과 다르면 CI가 빨간불이어야 합니다. "경고만" 하면 아무도 안 봅니다.
  • 골든은 VCS에 저장 — 변화가 diff로 드러나야 리뷰됩니다. 단, 대용량·바이너리 골든은 저장소를 무겁게 하므로, 텍스트 정규화 표현으로 바꾸거나 별도 저장소/LFS를 고려합니다.
  • 갱신은 로컬에서만 — CI에서 UPDATE_GOLDEN을 막아 고무도장을 원천 차단합니다.
  • "도구 없음 = 통과" 금지 — 골든 테스트가 전제 도구 부족으로 실행되지 않았다면 통과가 아니라 실패로 처리해, "초록불인데 사실 안 돌았다"는 착시를 막습니다.

이런 재현·검증 규율은 AI PoC가 운영 단계에서 멈추는 이유에서 강조한 "재현 가능한 평가"와 같은 문제의식이며, 테스트 설계 전반은 자연어에서 MCP Tool Call까지: 통합 테스트와도 이어집니다.

10. 안티패턴과 쓰지 말아야 할 때

안티패턴

  • 모든 것을 스냅샷한다 → 리뷰 불가 → 고무도장.
  • 휘발 데이터를 정규화 없이 스냅샷한다 → flaky.
  • 명시적 단언을 골든으로 대체한다 → 정확성 검증 공백.
  • 깨지면 습관적으로 갱신한다 → 회귀 포착 0.

쓰지 말아야 할 때

  • 출력이 본질적으로 휘발적이고 정규화도 어려운 경우.
  • 정확성이 핵심 로직인 경우(금액 계산 등) → 골든이 아니라 명시적 단언으로.
  • 출력이 지나치게 커서 검토가 불가능한 경우 → 대상을 좁히거나 요약 지표로.

11. 체크리스트와 마무리

[ ] 이 출력은 결정적으로 나와야 하는가(생성기·직렬화·계약)
[ ] 시간·난수·ID를 이음매로 주입해 고정했는가
[ ] 순서·경로·개행·로케일·부동소수를 정규화했는가
[ ] 휘발 필드는 자리표시자로 마스킹했는가
[ ] 골든은 VCS에 있고, diff를 코드처럼 리뷰하는가
[ ] CI는 드리프트에 실패하고, UPDATE_GOLDEN을 막는가
[ ] 클린룸 반복 생성으로 결정성 자체도 검증하는가
[ ] 정확성 핵심은 골든이 아니라 명시적 단언으로 두었는가

정리하면, 골든 테스트는 "의도치 않은 변화"를 싸게 잡는 강력한 안전망이지만, 두 가지를 지켜야 살아 있습니다 — 정규화로 flaky를 크게 줄이고(외부 프로세스·경쟁 상태·버전 차이는 정규화만으로 다 없앨 수 없습니다), 리뷰 규율로 고무도장을 막는 것. 여기에 클린룸 반복 생성을 더하면, 단순한 "직전과 같음"을 넘어 "언제나 같음"이라는 결정성까지 보증할 수 있습니다. 생성기를 다루는 팀에게 골든 테스트는 옵션이 아니라 기본기입니다.

함께 읽으면 좋은 글: 폴리글랏 보안 계약 검증(Golden Vector·Canonical Byte), 멱등 인덱싱과 정규 콘텐츠 해시, AI PoC가 운영에서 멈추는 이유(재현 가능한 평가), 자연어에서 MCP Tool Call까지: 통합 테스트.