본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 05. 25. 11:51

LLM 시대이기에 권장하는 리グレ션 테스트(Regression Test) ~테스트가 사양서가 된다~

요약

LLM 시대의 AI 주도 개발 환경에서 테스트 코드를 '실행 가능한 사양서'로 활용하는 전략을 제안합니다. 정적 문서의 한계를 극복하고 구현과 동기화되는 리グレ션 테스트를 통해 시스템의 동작을 보증하는 방법을 다룹니다.

핵심 포인트

  • 테스트 코드는 구현과 항상 동기화되는 '살아있는 사양서' 역할을 수행함
  • 모호한 텍스트 문서보다 정밀도가 높은 '유일한 진실(SSOT)' 제공
  • LLM 활용 시 사양서와 코드 간의 괴리 문제를 방지하는 합리적 선택
  • 모든 것을 테스트로 대체하기보다 영역을 구분하여 관리하는 것이 중요

서론

"테스트를 작성하면 사양서(Specification)는 필요 없다"

이것은 문서를 작성하고 싶지 않은 사람의 억지처럼 들릴 수도 있습니다.

하지만, 최근의 LLM에 의한 AI 주도 개발(AI-Driven Development) 시대이기에 최대한의 위력을 발휘하며, 실용적으로 운용 가능해집니다.

본 기사에서는 전반부에서 리グレ션 테스트(Regression Test)에 대해 정리하고, 후반부에서는 LLM 시대이기에 더욱 유효해지는 테스트 결과를 기록하는 리グレ션 테스트의 진행 방식Claude Code를 이용한 하네스(Harness) 구축 사례를 제안합니다.

논의의 핵심은, 업데이트되지 않는 "죽은 문서"보다, 구현과 계속 동기화되는 살아있는 테스트가 더 신뢰할 수 있다는 점에 있습니다.

1. 왜 "테스트 = 사양서"라고 말할 수 있는가

1. "실행 가능한 사양서(Executable Specification)"로 재정의하기

테스트를 사양서의 대체재로 이야기할 때, 그것은 단순한 버그 체크의 문제가 아니라, **행동 주도 개발(BDD, Behavior-Driven Development)**의 맥락에서 다루는 문제입니다.

기존의 사양서는 작성하는 순간부터 낡아가지만, 테스트 코드(Test Code)는 구현과 어긋나는 순간 실패합니다. 즉, 테스트는 강제적으로 "최신 사양"을 계속 반영하는 메커니즘을 내포하고 있습니다.

2. 정적 문서 vs 동적 테스트

일본어나 영어로 작성된 사양서는 아무래도 해석의 차이가 발생합니다.

반면, 테스트 코드는 "입력 A에 대해 출력 B가 반환된다"라는 사실을 모호함 없는 코드로 정의합니다.

해석의 여지가 있는 100페이지의 문서보다, 누가 실행해도 같은 결과가 나오는 100개의 테스트 케이스가 사양으로서의 정밀도가 더 높습니다.

3. 유일한 진실(Single Source of Truth)의 일원화

사양서와 코드 양쪽을 모두 유지보수하는 것은 단순히 비용이 두 배로 듭니다. 특히 LLM에게 사양서를 쓰게 하면서 구현도 시키는 경우, 구현을 진행할 때마다 방대한 사양서와 코드가 순식간에 생성되어, 사양서와 코드가 괴리되어 가고, 최종적으로는 어느 쪽이 맞는지 알 수 없게 됩니다.

최종적으로 시스템의 동작을 결정하는 것은 코드이므로, 그 동작을 기술한 테스트야말로 "유일한 진실(Single Source of Truth)"이며, 이중 관리를 배제하는 합리적인 선택이 될 수 있습니다.

업데이트가 멈춘 100페이지의 md 파일은 미래의 부채가 됩니다. 하지만 코드를 변경할 때마다 테스트를 통과하는지/테스트가 실패하는지를 확인하는 테스트 코드는 현재 진행형인 사양서입니다. 우리가 구하는 것은 "문서를 남겨두는 안심감"이 아니라 "동작의 보증"이 아닐까요?

4. 모든 것을 테스트로 해결하려 하지 말 것

그렇다고 해서 모든 것을 테스트로 끝낼 수 있는 것은 아니므로, 영역을 명확히 나누는 것이 현명합니다.

구분구체적인 예시
테스트로 대체하는 것상세 설계, 로직, 에지 케이스(Edge Case)의 동작
문서로 남겨야 하는 것아키텍처 다이어그램(전체 조망), 외부 API 연동 사양, UI/UX 와이어프레임

"테스트로 사양을 표현할 수 있는 영역"과 "그림이나 문서가 적합한 영역"을 구분함으로써 문서의 모호함을 줄일 수 있습니다.

2. 리グレ션 테스트의 기본을 파악하기

테스트를 살아있는 사양서로 만들기 위해, 그 사양서를 변경 후에도 망가뜨리지 않음을 보증하는 메커니즘이 리グレ션 테스트입니다.

먼저 리グレ션 테스트의 정석적인 방법을 정리해 두겠습니다.

리グレ션 테스트(Regression Test, 회귀 테스트)란, 기능 추가·수정·리팩터링(Refactoring)을 수행한 후에, 기존 기능이 망가지지 않았음을 확인하는 테스트입니다.

정당한 리グレ션 테스트는 대체로 다음 계층으로 구성됩니다.

  • 단위 테스트(Unit Test): 함수·클래스 단위. 로직이나 에지 케이스를 고속으로 대량 검증한다.
  • 통합 테스트(Integration Test): 복수 모듈이나 DB·외부 서비스와의 연동을 검증한다.
  • E2E 테스트(End-to-End Test): 사용자 조작의 흐름 전체를 실제 환경과 유사한 환경에서 검증한다.

이것들을 CI(지속적 통합)에 포함시켜 변경될 때마다 자동 실행한다. 차이가 발생하면 "의도한 변경"인지 "리グレ션(Degradation, 퇴보)"인지를 판단하는 것이 교과서적인 진행 방식입니다.

LLM 시대에, 문서와 코드의 괴리는 더욱 가속화된다

여기서, 1장에서 언급한 문서와 코드의 괴리 문제가 LLM 시대에 심각해지고 있음을 강조해 둡니다.

지금까지도 「사양서가 코드와 일치하지 않는」 문제는 있었습니다. 하지만 LLM을 개발에 도입하면, 그 괴리 속도는 한층 더 빨라집니다.

코드의 변경 속도가 올라간다: LLM을 통한 구현 지원으로, 코드는 이전보다 더 빠른 속도로 다시 작성된다. 반면 README.md나 설계 메모와 같은 Markdown 문서들은 인간이 의식적으로 업데이트하지 않는 한 뒤처지게 된다. -
LLM 스스로가 오래된 문서를 참조한다: 코딩 에이전트(Coding Agent)나 리뷰 지원에 LLM을 사용하는 경우, LLM은 주어진 Markdown을 「올바른 사양」으로 읽어들인다. 문서가 오래되었다면 LLM은 과거의 전제를 바탕으로 구현이나 리뷰를 진행하게 되어, 디그레션(Degression, 기능 퇴보)의 원인이 된다. -
「어느 쪽이 SSOT인지 알 수 없는」 상태가 상시화된다: spec.md에는 「A라고 적혀 있고」, 코드는 「B처럼 동작한다」. 인간도 LLM도 어느 쪽을 믿어야 할지 판단할 수 없게 된다.

즉, LLM 시대에는 「문서와 코드 중 어느 쪽이 진실(Single Source of Truth, SSOT)인지 알 수 없는」 상태가 이전보다 더 빠르고 광범위하게 발생합니다.

그렇기에 리그레션 테스트(Regression Test)가 효과적이다

이 문제에 대해 리그레션 테스트는 명쾌한 답을 제시합니다.

「동작의 진실」을 Markdown이 아닌 테스트에 두는 것. 이것이 핵심입니다.

  • 테스트는 구현과 어긋나면 반드시 실패한다. Markdown과 달리 괴리를 묵인한 채 방치할 수 없다. -
  • CI에 통합하면 코드가 바뀔 때마다 「이전의 동작이 유지되고 있는가」를 자동으로 검증한다. LLM이 코드를 고속으로 다시 작성하더라도, 리그레션 테스트가 「망가진 부분」을 즉시 알려준다.
  • LLM에게 코드를 작성하게 할 때, 참조해야 할 「올바른 사양」을 오래될 수 있는 Markdown이 아니라, 계속해서 Green(통과) 상태를 유지하는 테스트에서 찾는다.

문서와 코드 중 어느 쪽이 SSOT인지 고민할 필요는 없습니다. 리그레션 테스트를 통과하는 한, 테스트가 정의하는 동작이야말로 SSOT입니다.

그렇게 되면 Markdown 문서는 테스트로는 표현하기 어려운 전체상(아키텍처 다이어그램 등)을 설명하는 역할에만 집중하면 됩니다.

3. AI 주도 개발에서의 리그레션 테스트 권장 사항

여기서부터가 본론입니다. 기존의 리그레션 테스트는 「코드로 작성할 수 있는 것」을 대상으로 해왔습니다. 하지만 LLM을 개발에 도입하면, 코드만으로는 다 표현할 수 없는 테스트 케이스가 늘어납니다.

따라서 필요한 접근 방식은, LLM으로 테스트를 실행하고 그 결과를 기록하여 리그레션 테스트의 자산으로 만드는 접근 방식입니다.

왜 LLM이 필요해지는가

단위 테스트(Unit Test), 통합 테스트(Integration Test), E2E 테스트를 작성하다 보면, 로직으로서 엄밀하게 쓰기는 어렵지만 확인은 하고 싶은 항목에 반드시 부딪히게 됩니다.

  • 자연어 출력이 「의도한 대로의 톤과 내용인가」
  • 에러 메시지가 「사용자에게 이해하기 쉬운가」
  • UI의 표시 문구가 「사양의 의도를 충족하는가」
  • 사양 변경 시 「이전과 동작이 달라지지 않았는가」에 대한 종합적인 판단

이러한 「인간이 리뷰하면 순식간에 알 수 있지만, 테스트 코드로 구현하기 어려운」 영역을 LLM이 판정하게 합니다.

결과를 기록하여 리그레션 테스트로 만든다

핵심은 테스트 실행 결과를 구조화하여 저장하는 것입니다. JSONL (JSON Lines) 형식이라면 테스트 케이스를 한 줄에 하나의 레코드로 추가하거나 차분 관리(Diff management)하기 쉽고, CI와도 궁합이 좋습니다.

{"id": "login-001", "type": "e2e", "input": "올바른 이메일과 비밀번호로 로그인", "expected": "대시보드로 전환됨", "actual": "대시보드로 전환됨", "llm_judgment": "pass", "run_at": "2026-05-25T10:00:00Z"}
{"id": "error-msg-014", "type": "unit", "input": "존재하지 않는 사용자 ID 지정", "expected": "사용자 친화적인 에러 문구", "actual": "User not found (code: 404)", "llm_judgment": "fail", "reason": "너무 기술적이라 엔드 유저용이 아님", "run_at": "2026-05-25T10:00:00Z"}

이 JSONL을 리포지토리(Repository)에 커밋해 두는 것으로써, 다음과 같은 운용이 성립됩니다.

  • 테스트 실행 시, 단체(Unit)·통합(Integration)·E2E 결과를 JSONL로 출력한다.
  • LLM에 "기대되는 동작"과 "실제 출력"을 전달하여, 합격 여부와 이유를 판정하게 한다. - 이전의 JSONL(기록된 정답 스냅샷)과 이번 결과를 비교한다.
  • 차분이 발생하면, 그것이 "의도한 변경"인지 "리グレ션 (Regression)"인지 리뷰한다. -
  • 차분으로 디그레션(Degression)을 검지할 수 있다: 이전의 JSONL과 비교하는 것만으로 동작의 변화가 가시화된다.

운용상의 주의점

LLM을 사용하는 이상, 주의도 필요합니다.

  • LLM의 판정은 흔들린다: 동일한 입력이라도 결과가 달라질 수 있다. 판정 기준(프롬프트(Prompt))을 고정하고, 온도(Temperature) 설정을 낮추며, 판정 그 자체도 기록·리뷰 대상으로 삼는다. -
  • LLM 판정은 "코드로 작성할 수 있는 테스트"의 대체가 아니다: 엄밀하게 평가할 수 있는 것은 기존 방식대로 코드로 작성한다. LLM은 어디까지나 "코드로 쓰기 어려운 영역"을 보완하는 역할이다. -
  • JSONL은 키워 나가는 것이다: 처음부터 완벽을 목표로 하지 말고, 리グレ션이 발견될 때마다 케이스를 추가해 나간다.

덤: 외관상의 리グレ션에는 "비주얼 리グレ션 테스트 (Visual Regression Test)"

테스트 코드로 구현할 수 없는 영역에는 UI의 외관이 있습니다. 레이아웃 깨짐·색상이나 폰트의 변화·요소가 삐져나오는 현상과 같은 시각적인 디그레션은 코드로 포착하기 어려운 대표적인 사례입니다.

이에 대해서는 **비주얼 리グレ션 테스트 (Visual Regression Test)**라는 수법이 있습니다. 화면의 스크린샷을 촬영하여 기준 이미지(베이스라인(Baseline))로 저장해 두고, 변경 후에 재촬영한 이미지와 픽셀 단위로 차분을 비교하는 방식입니다. Playwright의 스냅샷 기능 등의 도구가 잘 알려져 있습니다.

생각하는 방식은 본 기사의 JSONL 기록과 완전히 동일합니다. "있어야 할 상태의 스냅샷을 기록하고, 변경 후와 차분을 비교한다". 기록하는 대상이 텍스트인지 이미지인지가 다를 뿐입니다. LLM 판정(의미의 정확성)과 비주얼 리グレ션 테스트(외관의 정확성)를 조합하면, 테스트 코드에서 누락되는 영역을 상당히 넓게 커버할 수 있습니다.

4. Claude Code에서의 구축 예시

AI에게 코드를 수정해 달라고 할 때 가장 무서운 것은, AI가 "수정했지만 테스트는 확인하지 않은" 상태인 채로, 태연하게 "완료했습니다"라고 말하는 것입니다.

이는 인간의 리뷰에서도 놓치기 쉬우며, 자동 체크(CI)로 알아차리는 것은 작업이 일단락된 후가 됩니다. "방금 수정했을 터인 곳이, 다른 수정으로 인해 다시 망가진다" —— 이러한 "되돌아오는 상황(Backtrack)"의 발견이 항상 뒤처지게 됩니다.

그래서 "AI가 작업을 마치려는 바로 그 순간"에 테스트를 자동으로 실행하여, 결과가 이상하면 종료시키지 않는 장치를 Claude Code의 Stop hook으로 만들었습니다.

만든 것

소스를 수정한 채로 작업을 마치려 하면, 자동으로 테스트를 돌립니다.

결과가 달라져 있다면 종료를 일단 멈추고, 수정 방법의 절차로 유도합니다.

중요한 것은, AI에게 "테스트를 돌려줘"라고 부탁하는 것이 아니라, 시스템 측에서 강제하는 것입니다. 부탁 기반이면 AI는 깜빡 잊어버리고, 인간도 확인을 잊습니다. 그래서 "잊을 수 없는 동선"을 마련합니다.

한번 시스템화하면, 평소에는 그 존재를 의식하지 않고 사용할 수 있습니다. 테스트가 통과되는 한 hook은 묵묵히 통과시키며, 무언가 망가졌을 때만 가시화됩니다. 개발자가 "테스트를 돌려야지"라고 떠올릴 필요조차 없게 됩니다.

전체는 3개의 부품으로 구성되어 있습니다.

turn 종료
└─ Stop hook (settings.json 이 배선됨)
└─ regression_check.sh
...

Claude Code의 Stop hook 기초

Claude Code에는 "정해진 타이밍에 작은 스크립트를 자동으로 호출하는" 구조가 있으며, 이를 **hook (후크)**라고 부릅니다. 이번에 사용하는 Stop hook은, AI가 응답을 마치려는 순간에 호출됩니다. 설정은 .claude/settings.json에 작성하기만 하면 됩니다.

{
  "hooks": {
    "Stop": [
      ...
    ]
  }
}

hook이 동작할 때의 작업 위치는 정해져 있지 않습니다. Claude Code는 프로젝트의 루트 위치를 CLAUDE_PROJECT_DIR이라는 이름으로 전달해 주므로, 그것을 기점으로 합니다.

파일 구성

프로젝트마다 바뀌는 값은 regression_check.config.sh라는 파일 하나에 모아두었기에, 본체 스크립트와 skill에는 손을 대지 않고 재사용할 수 있습니다.

.claude/
├── settings.json # Stop hook 배선
├── hooks/
...

바뀌는 값은 이 설정 파일에 나열하기만 하면 됩니다.

# regression_check.config.sh
REGRESSION_WATCH_PATHS='^src/' # 감시 경로 (ERE)
REGRESSION_CMD='npm run regression' # 비교 모드 실행
...

hook 본체 작성하기

먼저 "반드시 지킬 것"을 정해둡니다.

  • hook 자체는
    테스트의 "정답 데이터" (저장된 기대 결과)를 다시 쓰지 않습니다. "이것은 의도한 대로의 변경이다"라는 승인은 반드시 사람이 수행합니다. -
    테스트 관련 파일에는 손대지 않습니다. -
    감시 대상 소스에 변경이 없을 때는 아무것도 하지 않고 즉시 종료합니다 (불필요하게 테스트를 실행하지 않습니다).

이를 만족하는 본체가 다음과 같습니다. 설정 파일이 있으면 읽어오고, 없으면 기본값으로 동작합니다.

#!/usr/bin/env bash
set -u
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
...

수정 방법도 절차화해 두기

블록된 후, AI는 reason 지시를 읽고 그대로 수정을 시작합니다. 즉, skill이 없어도 최소한의 동작은 가능합니다. 다만 매번 흔들림 없이 수정하도록 하기 위해, regression-fix라는 작은 skill에 "수정 규칙"을 고정해 둡니다. 요점은 다음과 같습니다.

  • 테스트 본체나 "정답 데이터"를 다시 써서 억지로 통과시키는 것은 금지합니다.
  • 결과가 바뀌었다면 "의도한 변화인가 / 실수로 망가뜨렸는가"를 구분합니다. 의도한 변화라면 사람에게 차이점(diff)을 보여주고 승인을 받은 뒤 정답을 업데이트합니다. 판단이 서지 않는다면 "망가뜨린" 쪽으로 취급합니다.
  • 몇 번 시도해도 고쳐지지 않으면 마음대로 진행하지 않고 사람에게 상담하고 멈춥니다.

skill의 실체는 .claude/skills/regression-fix/SKILL.md라는 파일 하나이며, 내용도 이 정도면 동작합니다.

예:

---
name: regression-fix
description: 테스트가 실패한 소스를, 테스트 측은 건드리지 않고 본체만 수정하여 통과시키는 루프. "수정해" 또는 "fix"라고 말하면 발화.
...

테스트 코드로 만들 수 없는 부분을 어떻게 보호할 것인가

여기까지는 "테스트가 실패하면 멈춘다"는 이야기였습니다. 하지만 정말로 보호하고 싶은 것 중 상당수는 애초에 테스트 코드로 옮길 수 없습니다. 생성된 문장의 자연스러움, 순서, UI의 모습, 도메인적인 타당성 등입니다. 생각하는 방식은 2단계가 있습니다.

① 정답을 쓸 수 없다면 「변화」를 검출하여 사람이 판단한다

기대값을 단언할 수 없더라도, 출력을 통째로 기록해 두면 "이전과 달라졌는가"는 기계가 알 수 있습니다. 이것이 스냅샷 테스트 (Snapshot Test)입니다. 옳고 그름을 판정하는 것이 아니라, 변화를 검출하는 것뿐입니다. 그리고 변화가 나타나면, 좋을지 나쁠지는 사람이 결정합니다.

이 메커니즘에서 "baseline의 업데이트를 자동화하지 않는다"고 결정한 이유가 바로 이것 때문입니다. 기계에게 정답이 없는 이상, 마지막 승인은 사람이 쥐고 있어야 합니다. "어렴풋이 맞다"라고밖에 말할 수 없는 것도 기록만 해두면 변화는 반드시 겉으로 드러납니다.

② 결과물에 남지 않는 것은 「확인 강제」로 바꾼다

비결정적인 출력이나 문구의 톤, 표시 깨짐처럼 어떤 기록에도 남지 않는 것은 스냅샷으로도 잡아낼 수 없습니다. 여기서는 발상을 바꾸어, hook을 **테스트 대신 "체크리스트 관문"**으로 만듭니다. 테스트를 돌리는 대신, "끝내기 전에 이것을 확인했는가"를 제시하며 멈추게 하는 것입니다.

# 예: 문구나 프롬프트를 건드리면, 테스트가 아니라 자기 점검을 유도하며 멈춤
if git status --porcelain | awk '{print $NF}' | grep -qE '^src/(ui|prompts)/'; then
node -e 'process.stdout.write(JSON.stringify({
...

목표는 「아마 괜찮을 거야」를 「확인했다」로 바꾸는 것입니다. 내용물을 기계적으로 검증할 수 없더라도, "그냥 지나치는 것"만은 확실히 방지할 수 있습니다. 다만 AI가 입으로만 "확인했습니다"라고 말할 여지까지는 없앨 수 없습니다. 그 부분은 차이점(diff)이나 결과물을 함께 출력하게 하여 사람이 살짝 확인하는 운영 방식으로 보완합니다.

좋은 장치는 존재를 잊게 만든다

설치한 후에는 개발자도 AI도 평소에는 이 hook을 의식하지 않습니다. 테스트가 통과되는 한 아무 일도 일어나지 않으며, 평소처럼 작업이 끝납니다. 망가뜨렸을 때만 조용히 발목을 잡으며 고치는 방법을 가리킵니다. 설정이나 기동 조작은 필요 없으며, "테스트를 돌렸던가?"라고 고민할 필요도 없습니다. 그저 배후에 두기만 하면 되는 하네스(Harness)가 됩니다.

요약

  • AI 주도 개발(AI-driven development)에서 리グレッション 테스트(Regression Test)는 실행 가능한 사양서로서 실용적인 운영이 된다.
  • LLM 시대에는 코드 변경 속도가 빨라지며, Markdown 문서와의 괴리가 가속화된다. "어느 쪽이 SSOT(Single Source of Truth)인지 알 수 없는" 상태가 상시화되기 쉽다.
  • 그렇기에 동작의 진실을 Markdown이 아닌 테스트에 둔다. 리グレッション 테스트가 통과되는 한, 테스트가 정의하는 동작이야말로 SSOT가 된다.
  • 리グレッション 테스트는 단체(Unit), 통합(Integration), E2E 계층으로 구성하며, CI에서 자동화하는 것이 정석이다.
  • UI 변화 등 테스트 코드만으로는 보완할 수 없는 테스트 케이스가 늘어난다. LLM으로 판정하고 결과를 JSONL 등에 기록함으로써, 그것들도 살아있는 사양서로서 자산화할 수 있다.
  • 외관상의 디그레션(Degradation)에는 비주얼 리グレッション 테스트(Visual Regression Test)(스크린샷 차이 비교)가 유효하다. 스냅샷을 기록하여 차이를 비교한다는 발상은 공통적이다.
  • LLM에게 "테스트 돌려줘"라고 부탁하는 것이 아니라, Stop hook을 통해 turn 종료 시 강제한다.
  • "AI가 거짓말을 하지 않는다"는 것에 기대하기보다, "거짓말을 할 수 없는 동선"을 하네스로 만드는 것이 더 빠르다. Stop hook은 이를 위한 가장 간편한 레이어가 된다.
  • "정답"의 갱신은 자동화하지 않는다: 망가진 결과를 정답으로 삼아버리는 사고를 방지하기 위해서다.

「AI가 틀리지 않는다」는 것에 기대하기보다, 「틀려도 그냥 지나칠 수 없는 길」을 묵묵히 깔아두는 것이 더 빠릅니다. Stop hook은 이를 위한 가장 간편한 장치가 됩니다.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0