
테스트 코드는 인간을 위한 문서가 아니다 (AI가 작성하는 시대의 보증 대장)
요약
AI가 테스트 코드를 대량 생산하는 시대에, 실행 가능한 테스트와 인간이 읽는 문서 사이의 격차를 어떻게 해결할 것인지 다룹니다. 테스트 코드는 실행을 위한 것이지 사양서가 아니므로, 테스트에서 약속(specification)만을 추출하는 새로운 접근법을 제안합니다.
핵심 포인트
- AI가 생성한 테스트는 실행은 되지만 인간이 의도를 파악하기 어려움
- 문서는 부패하고 테스트는 읽기 어렵다는 문제의 본질적 연결
- 기존의 '실행 가능한 문서' 방식은 유지보수 비용 문제로 실패함
- 테스트는 평범하게 작성하되, 약속(specification)만 추출하는 방식 제안
Claude Code에 구현을 맡기게 되면서, 버그 수정도 기능 추가도 이전이라면 손대지 않고 두었을 양까지 해낼 수 있게 되었습니다. 동시에 늘어난 것은 되돌리기(rework)입니다. 고친 부분이 다음 변경에서 되돌아가거나, 관계없다고 생각했던 부분이 망가집니다. 만들 수 있는 양이 늘어난 만큼, 망가뜨리는 양도 늘어났습니다.
지켜주는 것은 테스트입니다. 다만, 그 테스트조차 Claude Code가 작성하고 있습니다. 실행하면 초록색(pass)이 되고, diff를 대충 훑어보며 통과시키면 테스트 개수만 늘어갑니다. 그리고 다음에 손을 댈 때, 지금 무엇이 보호되고 있는지 알 수 있는 방법은 tests/ 디렉토리를 위에서부터 읽는 것뿐입니다.
그렇다고 변경될 때마다 "망가뜨리지 않았는지 전부 확인해줘"라고 AI에게 던지는 것도 성립하지 않습니다. 매번 테스트 전체를 다시 읽게 하는 것은 비용이 너무 크고, 그러면서도 놓치는 부분이 생깁니다. 무엇을 지켜야 할지가 정해져 있지 않기 때문에, 그때 눈에 들어온 범위까지만 확인합니다.
테스트 코드는 인간이 읽기 위해 작성되지 않았다
테스트가 있으면 사양(specification)을 알 수 있다고들 합니다. 실제로 테스트 코드에서 읽어낼 수 있는 것은 초록색인지 빨간색인지일 뿐, 무엇을 약속하고 있는지가 아닙니다. 피스처(fixture) 구성, fake 교체, 어서션(assertion)의 파편이 섞인 파일들을 여러 개 쫓아가며 머릿속에서 의미로 되돌리는 작업이 필요합니다. 직접 작성한 테스트라면 적어도 작성했을 시점의 이해도가 남아 있습니다. 생성된 테스트에는 그 최초의 1회조차 없습니다.
사양서는 반대입니다. 인간이 읽기 위해 작성되지만, 이를 어겨도 아무 일도 일어나지 않습니다. 릴리스 후의 세세한 수정은 사양서를 쓸 정도는 아니기에 구현만 진행됩니다. 괴리가 생겨도 아무도 눈치채지 못하며, 눈치챘을 때는 "그건 믿을 수 없다"라는 인식만 남습니다.
"테스트는 읽기 어렵다"와 "문서는 부패한다"는 별개의 불만으로 이야기되지만, 이는 동전의 양면과 같습니다. 읽을 수 있는 문서는 집행되지 않고, 집행되는 문서는 읽을 수 없습니다. AI가 테스트를 양산하게 되면서 이 격차는 단번에 벌어졌습니다.
20년 전에는 사양서를 실행하려고 시도했다
이 격차를 메우려는 시도는 여러 번 있었습니다. 방향은 모두 같았습니다. 인간이 읽을 수 있는 문서를 실행 가능하게 만드는 것입니다.
Ward Cunningham의 FIT (Framework for Integrated Test, 2002년)는 wiki 표에 적은 예시를 그대로 수용 테스트(acceptance test)로 실행했습니다. 2008년의 Cucumber는 Gherkin이라는 평이한 구문으로 작성한 시나리오를 스텝 정의(step definition)에 연결하여 실행합니다. 2011년 Gojko Adzic의 『Specification by Example』은 이를 living documentation (구현과 함께 계속 업데이트되는 사양서)으로 체계화했습니다.
하지만 핵심 부분은 뿌리 내리지 못했습니다. Adzic 스스로가 10년 후의 총괄에서 수치를 제시했습니다. 요구사항을 버전 관리되는 사양 파일로 보유하는 팀은 12%였고, 57%는 Jira 등의 태스크 관리 도구가 정본(source of truth)이었습니다. 예시를 사용하는 팀의 약 3분의 1은 자동화하지 않았습니다.
실패한 이유는 사상이 아니라 비용 때문입니다. 시나리오를 쓰고, 스텝 정의 코드로 연결하고, 구현이 바뀔 때마다 양쪽을 모두 수정해야 합니다. 이 이중 관리를 인간이 계속 부담해야 했고, 감당할 수 없었습니다.
방향을 반대로 바꾼다. 테스트는 평범하게 쓰고, 약속만 추출한다
지금 하고 있는 것은 반대 방향입니다. 문서를 실행 가능하게 만드는 것은 포기하고, 테스트는 평범한 테스트로 작성합니다. 그 대신, 테스트가 지키고 있는 약속만을 인간이 읽을 수 있는 형태로 추출하여 하나의 파일에 둡니다. 나는 이것을 보증 대장(guarantee ledger)이라고 부르며, docs/guarantees.md에 두고 있습니다.
내 포트폴리오 사이트의 대장에서 문의 폼의 수신 부분만 뽑아보겠습니다.
### 1. `src/lib/api.test.ts` — src/lib/api.ts (Hono app)
- `POST /api/contact`는 요청 본문(request body)을 JSON으로 파싱할 수 없는 경우,
400과 함께 `{ error: 'invalid_json' }`을 반환한다.
...
마지막 줄이 전형적인 예입니다. 봇 방지용 Turnstile은 토큰이 전달되었을 때만 검증하도록 만들어 두었습니다. 이것은 의도적으로 결정한 사항이지만, 코드만 보면 검증 누락처럼 보일 수 있습니다. 의도된 동작일수록 적어두지 않으면 다음에 손을 댈 때 수정 대상이 됩니다.
본문이 약속의 선언이며, 표는 어떤 테스트가 이를 뒷받침하는지에 대한 색인입니다. "여기을 바꾸면 무엇이 망가질까"라는 질문에 대해, 표로 범위를 좁히고, 선언문을 읽고, 필요하다면 테스트 코드까지 내려갑니다. 이 순서로 따라갈 수 있습니다.
문서와 테스트는 별개로 유지하되, 대응 관계만 갖게 합니다. 작성하는 것은 Claude Code입니다. 구현을 변경할 때 테스트도 수정할 수 있다면, 동일한 변경으로 장부의 행도 수정할 수 있습니다. 20년 전 인간이 감당할 수 없었던 이중 관리의 비용이, 이제는 감당할 수 있는 쪽으로 옮겨갔습니다. 인간이 하는 일은 나온 선언문을 읽는 것뿐입니다.
Gherkin과 다른 점은, 이 표가 그루(Glue) 코드가 아니라 단순한 색인이라는 점입니다. 테스트는 해당 리포지토리의 일반적인 테스트 프레임워크로 평범하게 작성됩니다. 실행을 위해 자연어를 기계 판독 가능한 구문으로 통과시키려 하면, 비즈니스용 외형을 한 프로그래머만이 유지보수할 수 있는 계층이 또 생겨나게 됩니다. 읽게 하고 싶은 대상은 인간이고, 실행하게 하고 싶은 대상은 CI이므로, 억지로 하나의 파일로 합치지 않습니다.
구현보다 먼저, 사람이 약속을 승인한다
장부를 나중에 한꺼번에 업데이트하는 방식을 취하면, 업데이트되지 않는 회차가 반드시 발생합니다. 그래서 변경의 입구에서 승인합니다. 저는 한 번의 변경마다 Issue 파일을 작성하고 있으며, 그 안에 보증 절(clause)이 있습니다.
### 보증
- 새롭게 선언하는 보증: 첨부 파일이 5MB를 초과하는 경우, 413을 반환하고 전송하지 말 것
- 유지하는 보증: 필수 항목이 누락된 요청은 400으로 거부할 것
이 두 줄을 읽고, 삭제·추가·수정을 넣는 것이 저의 작업입니다. 재가(裁可)가 완료된 것만이 구현으로 던져집니다. 테스트 코드는 AI가 작성하지만, 무엇을 약속할지는 인간이 결정합니다. 이러한 분업을 Guarantee-Driven Development (GDD, 보증 주도 개발)라고 부릅니다. 방침의 전문은 여기에 있습니다.
순서를 반대로 하면 성립하지 않습니다. 구현이 끝난 뒤에 보증을 써내게 하면, 구현이 그렇게 되어 있다는 사실을 그대로 베껴 쓰는 것에 불과합니다. 어떤 일이 일어나도 보증은 충족됩니다. TDD가 테스트를 먼저 작성하는 규율이라면, 이것은 약속의 재가를 먼저 두는 규율입니다.
테스트는 품질을 위해서가 아니라, 다음 변경을 가볍게 하기 위해 존재한다
테스트의 가치를 품질 보증이라고 말하면, 테스트를 작성하여 통과(green)된 시점에서 업무가 끝난 것처럼 보입니다. 실제로 가치가 나타나는 것은 그 이후, 즉 다음에 동일한 코드에 손을 댈 때입니다.
새로운 기능을 추가하거나 버그를 수정할 때 무거운 이유는, 작성 작업 그 자체보다 "어디까지 망가질 수 있는지 모른다"는 점 때문입니다. 건드려도 되는 범위를 파악하지 못하면, 관계없는 곳까지 읽으러 가거나 반대로 확인하지 않고 진행하게 됩니다. 테스트가 있으면 그 범위가 확정됩니다. "여기을 바꿔도 이 약속은 자동으로 확인된다"는 확신이 들기에 손을 움직일 수 있습니다.
장부는 그 안전한 영토의 지도입니다. 변경할 때마다 보증을 한 줄씩 추가하면, 보호되는 범위가 조금씩 넓어집니다. 다음 구현에서 참조하는 것은 그때 작성되어 있는 장부입니다.
따라서 기재하는 내용은 외부에서 보이는 동작(공개 API, CLI, 배포물)에만 한정합니다. 내부 구현의 테스트까지 나열하면 지도로서 읽을 수 없는 길이가 되며, 리팩터링(Refactoring)을 할 때마다 장부가 움직여서 약속이 바뀐 것인지 구조가 바뀐 것인지 구분할 수 없게 됩니다. 기재되지 않은 동작은 약속이 아니며, 예고 없이 변경합니다. 망라의 선언이 아니라, 경계의 선언입니다.
장부가 어긋나도 CI는 실패하지 않는다
한계도 명확합니다. 보증을 어기면 테스트가 실패하지만, 장부의 기술과 구현이 어긋나도 실패하지 않습니다. 행을 삭제하는 것을 잊어도, 문구가 오래되어도 CI는 아무 말도 하지 않습니다. 집행 장치가 달려 있는 것은 보증의 내용이지, 장부라는 문서 그 자체가 아니기 때문입니다.
그 부분은 정기적으로 장부와 테스트를 대조하며 찾아내고 있습니다. README와 마찬가지로 부패할 여지는 남아 있으며, 완전히 없애지는 못했습니다.
또 하나, 장부의 스코프(Scope) 외에 새로운 공개 접점이 생겼을 때 "기재되어 있지 않으니 그냥 통과"를 선택할 수 있는 문제가 있습니다. 대상 외의 기재는 통과 근거로 삼지 않는다고 방침에 적어두었지만, 이는 글로 쓴 규율일 뿐 기계가 막아주는 것은 아닙니다.
그럼에도 불구하고 구현할 때, 수정할 때, 버그가 발생했을 때 가장 먼저 여는 파일은 정해졌습니다. 테스트를 작성하게 하는 김에 동일한 변경으로 약속 부분도 작성하게 하고, 그 부분만 인간이 읽습니다. 파일 하나를 늘리는 것만으로, 파괴해서는 안 될 영토를 넓혀가며 확장해 나갈 수 있습니다.
Discussion

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