증거 기반 개발(Evidence-Driven Development): 코딩 에이전트에게 증명할 무언가를 제공하기
요약
본 기사는 '증거 기반 개발(Evidence-Driven Development, EDD)'이라는 방법론을 소개합니다. 이는 코딩 에이전트가 단순히 기능을 완성하는 것을 넘어, 주장을 기록하고 이를 반증할 실험을 설계하며 관찰 가능한 증거를 통해 다음 결정을 내리는 과정을 강조합니다. TDD나 BDD와 유사하지만, 주장-실행 조건-관찰 결과-결정의 연결 상태에 초점을 맞춥니다.
핵심 포인트
- EDD는 주장을 기록하고 반증할 실험을 설계하는 방법론입니다.
- 코딩 에이전트가 모든 단계에서 도움을 받을 수 있습니다.
- TDD, BDD와 달리 '주장-실행 조건-관찰 결과-결정'의 연결 상태를 강조합니다.
- 작동 예제(Tiny Tasks)와 소스 코드를 GitHub에 제공합니다.
AI 코딩 에이전트가 기능을 완성합니다. 테스트는 통과하고, 설명은 합리적으로 들립니다.
그러다가 누군가 작은 질문을 던집니다: “만약 제가 그 요청을 다시 실행하면 어떻게 되나요?”
그 질문 하나가 프로젝트의 형태를 바꿀 수 있습니다. 이제 ‘완료’라는 것이 관찰 가능한 무언가를 의미해야 합니다. 답변은 실험이 필요하고, 그 실험은 틀릴 수 있는 방법을 필요로 합니다.
이 튜토리얼은 바로 그 아이디어를 중심으로 작은 작업 목록을 만듭니다. 끝날 때쯤이면 작동하는 애플리케이션 하나와 의도적으로 고장 난 버전 두 개, 그리고 각 버전이 어떤 약속을 지키는지 보여주는 증거 폴더를 갖게 될 것입니다.
저는 이 작동 방식을 설명하기 위해 **증거 기반 개발(evidence-driven development)**이라는 용어를 사용합니다: 주장을 기록하고, 그것을 반증할 것이 무엇인지 결정하며, 관련 실험을 실행하고, 그 결과를 다음 결정에 따르게 합니다.
코딩 에이전트가 모든 단계에서 도움을 줄 수 있습니다. 증거는 에이전트의 요약 내용을 신뢰하지 않고도 검사 가능해야 합니다.
먼저, 이 아이디어에 대한 공로를 밝힙니다
이 이름은 코딩 에이전트보다 앞선 개념입니다. Jan Bosch가 2017년에 출판한 책, 『Speed, Data, and Ecosystems』에는 증거 기반 개발(Evidence-Driven Development) 장이 포함되어 있습니다. 그 내용은 요구사항, 가설, 그리고 실험을 연결합니다. 저는 이 용어를 방법론을 발명했다고 주장하기보다는 실제 에이전트 지원 워크플로우에 사용하고 있습니다. 출판사 카탈로그.
AI 분야에도 관련 연구가 있습니다. Xia와 동료들은 LLM 에이전트를 위한 지속적인 피드백 루프로서 평가 기반 개발 및 운영(evaluation-driven development and operations), 즉 EDDOps를 설명합니다. 그들의 논문은 우리가 여기서 만들 작은 애플리케이션보다 더 광범위한 에이전트 라이프사이클을 다룹니다. EDDOps 논문
TDD는 여전히 유용한 빨강–초록–리팩토링(red–green–refactor) 루프를 제공합니다. BDD는 사람들이 논의할 수 있는 예제를 통해 동작을 표현하는 데 도움을 줍니다. 이 튜토리얼에서 EDD는 주장(claim), 실행 조건(execution conditions), 관찰된 결과(observed result), 그리고 그에 따른 결정(resulting decision)을 연결 상태로 유지하는 학문 분야를 명명합니다. 이러한 실천 방법들이 함께 어우러집니다. TDD, Given–When–Then.
실제 가치를 지닌 작은 애플리케이션
저희 앱은 Tiny Tasks입니다. 이 앱은 작업을 추가하고 목록으로 보여줍니다. Python과 SQLite만으로 충분하며, 애플리케이션과 실험 실행기는 Python 표준 라이브러리만을 사용합니다. SQLite 지원이 되는 Python 3.10 이상 버전을 사용하세요.
동반 저장소에는 전체 소스 코드, 표지 아트워크, 그리고 기록된 증거가 포함되어 있습니다. 따라 하기 위해 클론(Clone)하세요:
GitHub logo copyleftdev / evidence-driven-development
반증 가능한 주장, SQLite 재시도, 음성 통제(negative controls), 그리고 재현 가능한 결과를 다루는 실행 가능한 증거 기반 개발 튜토리얼.
Evidence-Driven Development: Tiny Tasks
Evidence-Driven Development: Give Your Coding Agent Something to Prove를 위한 독립적인 동반 프로젝트입니다. 작은 로컬 작업 목록은 명시적 주장, 신선한 프로세스 실험을 통한 음성 통제(negative controls), 0이 아닌 운동 게이트(nonzero exercise gates), 그리고 재현 가능한 증거를 가르칩니다.

실행하기 (Run)
표준 SQLite 모듈이 포함된 Python 3.10 이상 버전이 필요합니다. 애플리케이션 의존성, API 키, 모델 계정, 네트워크 서비스 또는 클라우드 리소스가 필요하지 않습니다.
git clone https://github.com/copyleftdev/evidence-driven-development.git
cd evidence-driven-development
python3 -m tiny_tasks add "Water plants" --request-id plant-1
python3 -m tiny_tasks add "Water plants" --request-id plant-1
python3 -m tiny_tasks list
python3 -m unittest discover -s tests -v
python3 scripts/prove.py
The 두 번째 create는 created: false를 가진 원본 작업을 반환합니다. 진정으로 새로운 동작을 위해서는 새로운 요청 ID(request ID)를 사용하세요. 제목은 주변 공백을 제거한 후 비교됩니다. 기본 데이터 위치는 .local/tasks.sqlite이며, add 또는 list 서브 커맨드 이전에 --db PATH를 사용하여 다른 파일을 설정할 수 있습니다. 충돌하는 요청 재사용 및 유효하지 않은 입력은 다음과 같은 결과를 반환합니다…
git clone https://github.com/copyleftdev/evidence-driven-development.git
cd evidence-driven-development
git checkout 1f03111db02d359b24602992eddae60122d49d74
...
checkout은 이 아티클에서 사용된 소스 버전을 선택합니다. add 명령어는 작업을 반환하고, list 명령어는 새로운 프로세스를 시작하여 이를 다시 읽어옵니다. 기본 데이터베이스는 .local/tasks.sqlite에 있습니다.
요청 ID(request ID)는 사용자 의도한 동작에 속합니다. 호출자가 동일한 동작을 재시도할 때, 동일한 ID를 전송합니다. 진정으로 새로운 작업은 제목이 일치하더라도 새로운 ID를 받습니다.
이 작은 차이가 우리에게 증명할 가치가 있는 무언가를 제공합니다.
1. 틀릴 수 있는 주장 작성하기
"작업 목록을 신뢰성 있게 만들기(Make the task list reliable)"는 해석의 여지를 너무 많이 남깁니다.
여기에 애플리케이션 구현 전에 우리가 작성했던 실험 계약서가 있습니다:
| 주장 (Claim) | 이를 반증하는 결과 (A result that disproves it) |
|---|---|
| 인지된 작업은 프로세스 종료 후에도 살아남는다 (Acknowledged tasks survive a process ending) | 새로운 프로세스가 저장된 작업을 읽을 수 없다 (A new process cannot read the saved task) |
| ... |
완전한 계약서는 여기에 포함되어 있으며, 리포지토리에는 커밋 고정 복사본이 있습니다:
저장소는 해당 주장들을 experiments/001-reliable-tasks/experiment.json에 기록합니다. 또한 세 번의 재시작 라운드, 여덟 개의 동시 재시도 프로세스, 두 개의 독립적인 반복 횟수, 그리고 실제로 실행되어야 하는 시나리오를 선언합니다.
이 수치들은 작은 학습 작업 부하일 뿐입니다. 이것은 통계적 신뢰성 추정치나 처리량 목표가 아닙니다.
에이전트에게 코드를 요청하기 전에, 다음과 같은 종류의 작업을 제공하십시오:
Build a local task list that adds and lists tasks.
First propose observable claims and the outcomes that would disprove them.
...
주장을 직접 검토하세요. "중단에서 생존(survives interruption)"이라는 것을 조용히 "동일 객체 내에서 두 번 작동(works twice in the same object)"으로 좁히는 에이전트는 잘못된 약속에 대한 아름다운 테스트를 생성할 수 있습니다.
2. 에이전트에게 증거를 남길 장소를 제공하기
프로젝트에는 각기 다른 역할을 하는 몇 가지 작은 문서들이 있습니다:
AGENTS.md working rules and commands
docs/plan.md task progress and handoff
docs/decisions.md decisions and their reasons
...
AGENTS.md는 에이전트에게 이러한 것들이 어디에 있는지 알려줍니다. 실험 파일은 주장을 정의합니다. 실행 디렉토리는 무슨 일이 일어났는지 기록합니다. 결정 문서는 그것 때문에 우리가 무엇을 선택했는지 설명합니다.
짧은 지침으로 시작할 수 있습니다:
Read the experiment contract before changing behavior.
Run the declared scenarios and keep failed outputs.
Create a new run directory; never replace an earlier run.
...
지침은 유지보수가 필요합니다. 유용한 제약 조건이 이미 표준 문서에 존재한다면, 그곳을 링크하세요. 반복되는 지침의 쌓이는 더미는 다음 에이전트의 작업을 더 어렵게 만들 수 있습니다. OpenAI의 현재 가이드라인 역시 집중된 지침과 작업별 컨텍스트를 선호합니다. Guidance on skills and project instructions.
3. 가장 작고 유용한 설계를 구현하기
Tiny Tasks는 각 태스크와 요청 ID를 함께 저장합니다. 고유한 데이터베이스 제약 조건은 두 개의 저장된 행이 동일한 ID를 공유하는 것을 방지합니다.
생성(Creation) 과정은 하나의 트랜잭션을 사용합니다. 이 과정에서 ID를 찾고, ID가 존재할 경우 정규화된 제목을 비교하며, 새로운 경우에만 삽입합니다. 명령어는 트랜잭션 커밋 후 성공을 반환합니다.
전체 tiny_tasks/store.py 구현은 아래에 포함되어 있습니다. 또한 커밋 고정 소스에서도 읽을 수 있습니다.
여기서 '정규화(Normalized)'는 명시적인 의미를 가집니다: 앱은 제목을 비교하기 전에 앞뒤 공백을 제거합니다. add 메서드는 트랜잭션 경계(transaction boundary)를 보여주며, 검증(validation), 연결 설정(connection setup), 목록 조회(listing)가 포함되어 전체 예제를 확인할 수 있습니다.
이러한 경계는 중요합니다. 왜냐하면 하나의 트랜잭션 내에서 확인하고 나중에 삽입하는 방식은 다른 프로세스가 개입할 여지를 남기기 때문입니다. SQLite는 BEGIN IMMEDIATE의 쓰기 트랜잭션 동작을 트랜잭션 참고 자료에 문서화합니다. 그럼에도 불구하고, 그럴듯한 코드는 단지 후보 설명일 뿐입니다. 우리는 지원하겠다고 약속한 워크로드를 실행해야 합니다.
4. 주장하는 경계를 넘어서
테스트는 Store를 생성하고, 태스크를 추가한 다음 즉시 목록을 조회할 수 있습니다. 이것은 유용합니다. 하지만 새로운 프로세스에서 어떤 일이 발생하는지는 확립하지 못합니다.
저희 실험 러너(experiment runner)는 실제 CLI를 서브프로세스(subprocesses)로 호출합니다. 모든 명령어는 새로 시작됩니다. 재시작 시나리오의 경우, 세 개의 태스크를 순차적으로 추가하고, 각 성공적인 생성 후 별도의 프로세스가 예상 목록을 읽을 수 있는지 확인합니다.
재시도(retries)에 대해서는 두 가지 다른 상황을 테스트합니다:
- 생성이 성공하지만, 이를 호출한 쪽이 응답을 무시하고 새로운 프로세스가 요청을 반복하는 경우.
- 동기화 장벽(synchronization barrier)을 통해 실행된 여덟 개의 프로세스가 동일한 요청을 제출하는 경우.
두 번째 시나리오는 반환된 ID, created: true 응답의 수, 그리고 실제로 저장된 행을 확인합니다. 정확히 하나의 작업만 존재해야 합니다.
무시된(ignored) 응답은 액션이 성공했는지 확신하지 못하는 호출자를 위한 통제된 대체재입니다. 커밋 중에 네트워크 연결을 끊거나 프로세스를 종료한 것은 아닙니다. 이러한 구분을 명확하게 유지하는 것이 결과를 유용하게 만듭니다.
5. 의도적으로 애플리케이션을 중단시키기
이제 제가 가장 좋아하는 부분이 나옵니다: 이 실험이 자신이 감지한다고 주장하는 결함을 실제로 포착할 수 있음을 입증합니다.
lab/ 디렉터리에는 의도적으로 잘못된 구현 두 가지가 포함되어 있습니다. 이들은 실제 앱과 동일한 명령어 형태를 사용합니다:
- Volatile: 성공적인 생성을 반환하지만 영구적인 작업은 남기지 않습니다.
- Duplicates: 작업을 저장하지만 요청 ID를 무시하여 재시도할 때마다 행을 삽입합니다.
이들은 부정적 통제(negative controls)입니다. 즉, 검사가 특정 이유로 거부해야 하는 알려진 잘못된 예시들입니다. 이들은 우리가 우연히 발견했다고 가장하는 버그가 아니라 학습용 고정 장치(teaching fixtures)입니다.
Volatile 버전은 재시작 주장(restart claim)에 실패해야 합니다. Duplicate-accepting 버전은 동시 재시도 주장(concurrent retry claim)에 실패해야 합니다. 깨진 버전의 구문 오류는 두 속성 중 어느 것도 확립하지 못할 것이므로, 저장된 응답과 실패 이유를 검사하십시오.
Review는 게이트의 첫 번째 버전에 약점을 발견했습니다: 이 버전은 이유를 확인하지 않고 통제의 실패를 수용했기 때문입니다. 자식 프로세스 충돌이 성공적인 버그 감지처럼 위장할 수 있었습니다. 우리는 회귀 테스트(regression tests)를 추가하고, 통제에 대해 0이 아닌 실행 횟수를 요구했으며, 게이트가 목표로 하는 실패 이유를 확인하도록 만들었습니다. 이전 실행 기록은 해당 게이트의 한계가 기록된 채 저장소에 남아 있습니다.
오해의 소지가 있는 녹색 결과를 얻는 또 다른 방법이 있습니다: 아무것도 실행하지 않는 것입니다.
우리의 러너(runner)는 각 필수 시나리오가 몇 번 실행되었는지 추적합니다. 이 의도적으로 불완전한 보고서는 모든 주장된 결과가 true라고 말함에도 불구하고 실패해야 합니다.
empty = {
"claims": {name: True for name in required_scenarios},
"exercised": {name: 0 for name in required_scenarios},
...
'zero 활동을 거부하는 체크는 **실행 게이트(exercise gate)**입니다. 이는 성공을 해석하기 전에 기본적인 질문에 답합니다: 우리의 결론이 의존하는 시나리오를 실제로 시도했는가?
6. 실행하고 기록을 보관하세요 (Run it and keep the receipts)
동반 프로젝트에서:
python3 -m unittest discover -s tests -v
python3 scripts/prove.py
첫 번째 명령어는 아홉 개의 테스트를 실행합니다: 애플리케이션 동작에 대한 6개와 하네스(harness)에 대한 3개입니다. 두 번째 명령어는 프로세스 경계 실험을 실행하며, 여기에는 부정적 제어 구현과 empty-exercise control이 모두 포함됩니다.
기록된 실행에서 결과는 다음과 같았습니다:
AI 자동 생성 콘텐츠
본 콘텐츠는 Dev.to AI tag의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기