테스트 1,269개를 쌓으며 배운, 검증을 어디까지 할지 정하는 법
로직보다 증명이 오래 걸렸다
2026/09/03테스트 1,269개를 쌓으며 배운, 검증을 어디까지 할지 정하는 법
이전 글에서 Claude Code 설정을 기기 간에 동기화하는 플러그인, claude-sync를 소개했다. 이 도구는 원격 저장소의 변경 사항을 받아 로컬 설정 파일을 덮어쓰거나 지운다. 단 한 번의 오작동으로도 사용자가 공들여 세팅한 환경이 조용히 날아갈 수 있다. "대충 돌아가는" 수준으로는 배포할 수 없었고, 엣지 케이스를 막다 보니 결과적으로 테스트 코드가 1,269개(v3.1.2 기준)까지 늘어났다.
이 글은 그 많은 테스트를 어떻게 설계했는지, 그리고 역으로 과도한 검증이 개발 속도를 늦추기 시작했을 때 어떻게 적정선을 찾았는지에 대한 기록이다.
작업 방식
Spec → Plan → TDD
큰 기능 변경은 늘 설계 문서(Spec) 작성에서 시작했다. 설계가 정해지면 이를 작은 작업(Task) 단위의 구현 계획(Plan)으로 쪼개고, 각 작업마다 아래의 TDD(테스트 주도 개발) 흐름을 엄격하게 지켰다.
- 실패하는 테스트를 작성한다.
- 실제로 실패하는지 실행하여 확인한다.
- 코드를 구현한다.
- 테스트가 통과하는지 확인한다.
- 코드를 일부러 망가뜨려 보고, 방금 작성한 테스트가 이 오류를 잡아내는지 확인한다. (변조 확인)
- 커밋한다.
LLM 서브 에이전트(Sub-agent) 활용하기
구현은 작업 단위별로 독립된 LLM 서브 에이전트에게 맡겼고, 결과물은 다른 에이전트들을 통해 두 번 교차 검증(리뷰)을 거치게 했다. 한 번은 '설계 문서대로 구현되었는지', 다른 한 번은 '코드 품질이 적절한지'를 중점적으로 살폈다.
계획서(Plan)의 규모가 커지면서 요령도 생겼다. 본문이 5,000줄을 넘어가면 에이전트에게 전체를 주지 않고, 공통 규칙과 해당 작업에 필요한 부분만 발췌해서 전달했다. 특히 LLM의 컨텍스트(토큰) 사용량이 86%까지 차올랐을 때는, 남은 한도를 무리하게 소모하며 작업을 억지로 이어가는 대신 현재 상태를 '인계 문서'로 깔끔하게 정리한 뒤 다음 세션으로 넘겼다.
사람이 쥐고 있던 통제권
Push, 병합(Merge), 태그 생성, 배포 등 최종 결정은 모두 내가 직접 수행했다. 판단이 모호한 부분 역시 에이전트가 임의로 결정하지 않고 나에게 묻도록 통제했다.
재미있는 제약도 하나 있었다. 개발 중이던 내 로컬 기기에는 구버전(v2.0.0) 플러그인이 캐시되어 있어, 이 기기에서 /sync-backup을 실행하면 캐시된 구버전이 돌아 실제 백업 저장소가 망가질 위험이 있었다. 그래서 여러 기능을 구현하는 동안 실제 기기에서는 동기화 명령어를 단 한 번도 실행하지 않았다. 오직 테스트 코드만으로 동작을 보장했고, 실제 기기 테스트는 릴리즈 직전 체크리스트를 통해서만 진행했다.
어떤 테스트를 했나
첫 테스트는 2026년 6월에 작성한 6개에서 출발해, v3.1.2 시점에는 1,269개가 되었다. 전부 나열하기엔 많으니, 성격이 다른 핵심 테스트 전략 다섯 가지만 추렸다.
1. 다중 기기 교차 사용 시나리오
동기화 도구의 치명적인 결함은 대개 단일 기기 안에서는 드러나지 않는다. 그래서 임시 HOME 디렉터리를 여러 개 만들어 각각을 '독립된 기기'처럼 구성하고, 동일한 저장소를 공유한 상태에서 백업(Backup)과 복원(Restore)을 번갈아 실행하는 통합 테스트를 구축했다.
단순히 백업만 반복하는 것으로는 부족했다. 복원 과정에서 사용자가 충돌 해결 선택지를 고른 이후의 상태 변화가 중요했기 때문이다. 실제로 기준(Base) 갱신 규칙이 누락되었던 버그는 백업만 반복하는 테스트를 무사히 통과해버린 적도 있다.
2. 반복 실행 안전성 검증
이전 글에서 언급했던 '멀쩡한 서버 설정이 사라진 버그'를 겪은 후 도입한 테스트다. 동일한 입력으로 백업을 여러 번 반복하더라도 데이터가 유실되거나 발산하지 않고 항상 같은 결과(고정점)에 수렴하는지 검증한다.
3. SKILL.md의 자연어 문장 검증
Claude Code 환경에서 절차를 이끄는 것은 Python 스크립트가 아니라, LLM이 읽고 해석하는 SKILL.md 내부의 **자연어 프롬프트(문장)**다. 초기에는 Python 코드 로직만 검사했는데, 이는 반쪽짜리 테스트였다.
실제로 SKILL.md의 프롬프트를 고의로 훼손해 보았더니, 스크립트가 내놓는 키 이름을 바꿨을 때는 테스트 40개가 실패한 반면 같은 이름을 프롬프트에서 바꿨을 때는 하나도 실패하지 않았다. 이후부터는 SKILL.md에 정의된 절차가 스크립트가 반환하는 핵심 키워드나 명세를 정확히 포함하고 있는지 대조하는 테스트를 추가했다.
4. 셸(Shell) 스크립트 실제 구동 테스트
SKILL.md에 포함된 Bash 스크립트 블록도 처음엔 사본을 만들어 검증했다. 하지만 이는 가짜 안전망이었다. 최신 버전을 찾는 명령어인 sort -V | tail -1을 단순히 첫 줄만 가져오는 head -1로 바꿔도 기존 테스트 13개가 전부 통과해 버렸다. 블록을 꺼내 실제로 실행하도록 바꾼 뒤에도 구멍이 남았는데, 파일 시스템이 우연히 최고 버전을 맨 먼저 돌려주는 바람에 두 명령의 결과가 같았기 때문이다.
이후부터는 테스트용 버전 문자열을 극단적으로 섞어(3.9.0, 12.0.0 등 자릿수가 다른 버전 포함) 배치하고, 셸 블록을 추출해 실제 Bash 환경에서 실행하여 의도한 결과가 나오는지 검증하도록 변경했다.
5. 테스트를 테스트하기 (Mutation Testing)
'테스트가 통과했다'는 사실이 '코드가 완벽하다'는 것을 보장하진 않는다. 그래서 운영 코드를 의도적으로 망가뜨려(변조, Mutation) 테스트가 이를 제대로 잡아내는지 확인했다. 변조된 코드로 테스트가 실패하면 방어 성공(CAUGHT), 테스트가 여전히 통과해 버리면 방어 실패(SURVIVED)로 판정한다.
변조 테스트를 운영하며 얻은 교훈은 다음과 같다.
- 원본 코드부터 통과하는지 먼저 확인한다. 대조군이 실패하는 상태라면 이후의 변조 판정은 신뢰할 수 없다.
- 코드가 치환되지 않은 것(
APPLY_FAIL)을 통과(SURVIVED)로 착각해선 안 된다. SURVIVED라고 해서 무조건 테스트의 구멍은 아니다. 구현 방식만 다를 뿐 결과적으로 동일하게 동작하는 '등가 변조'일 수도 있다.
검증이 발목을 잡다
테스트를 촘촘히 쌓을수록 새로운 문제가 생겼다. 검증 프로세스 자체가 너무 느려진 것이다.
간단한 작업 하나를 처리하는 데 1시간 53분이 걸렸다. 추가된 코드는 102줄, 삭제된 코드는 22줄에 불과했다. 그중 순수 구현에 든 시간은 48분이었고, 에이전트의 5단계 리뷰에 52분이 들었다. 리뷰 라운드별로 나온 지적은 이랬다.
| 라운드 | 비즈니스 로직 결함 발견 | 기타 지적 사항 |
|---|---|---|
| 스펙 리뷰 1 | 0건 | 문서 서술 오류 2건 |
| 스펙 리뷰 2 | 0건 | 의미 없는 검증식 1건 |
| 스펙 리뷰 3 | 0건 | 0건 |
| 품질 리뷰 1 | 0건 | 테스트 누락 1건, 주석 오류 1건, 사소한 지적 5건 |
| 품질 리뷰 2 | 0건 | 사소한 지적 1건 |
5번의 라운드를 거치는 동안 치명적인 로직 결함은 단 한 건도 발견되지 않았다. 한 줄로 정리하면 이랬다.
로직은 첫 시도에 완성되었다. 완성되지 않은 것은 「증명」이다.
검증 다이어트: 어디서 멈출 것인가
이후 개발 속도를 높이기 위해 작업 계획(Plan)에 세 가지 명확한 제동 규칙을 추가했다.
1. 멈춤의 기준(Definition of Done) 명문화
무한정 검증하는 것을 막기 위해 명확한 기준을 세웠다.
- 커버리지 기준: 함수 호출 계약, 주요 로직 분기, 데이터 I/O 계층 등 핵심 검증 포인트를 설정하고, 각 포인트마다 최소 1개 이상의 변조 테스트가 모두
CAUGHT상태가 되면 검증을 마친다. - 깊이의 제한: 테스트가 검사할 대상이 비어 있으면 통과가 아니라 실패하도록 만들되, '테스트 코드를 다시 테스트하기 위한 테스트'는 만들지 않는다.
- 리뷰 조기 종료: 위 조건이 충족되고 발견된 로직 결함이 0건이라면 즉시 리뷰를 종료한다.
2. 위험도에 따른 차등 검증
검증의 강도를 "코드냐, 문서냐"가 아니라 **"이 부분이 오작동했을 때 복구 불가능한 피해가 발생하는가?"**를 기준으로 나누었다. 이 프로젝트에서는 SKILL.md 문장이 곧 실행 권한을 가지므로, 이를 단순 문서로 취급하여 검증을 소홀히 하면 가장 위험한 곳이 방치되는 셈이었다.
| 위험도 | 대상 | 검증 강도 |
|---|---|---|
| 상(High) | 데이터 변경 로직, 파일 덮어쓰기 경로, 사용자 실행을 유도하는 프롬프트 | 핵심 포인트 변조 테스트 + 2단계 리뷰 |
| 중(Medium) | 단순 상태 보고 포맷, 사실을 명시하는 Docstring | 문자열 매칭(grep) 확인 + 1단계 리뷰 |
| 하(Low) | 변수명 변경, 코드 가독성 개선, 주석 다듬기 | 리뷰 없이 즉시 커밋 |
3. 무의미한 LLM 리뷰 대신 변조 테스트에 집중
리뷰 에이전트가 찾아낸 유의미한 결함들은 대부분 '새로운 변조 테스트'를 제안했을 때 발견되었다. 구현 단계에서 이미 돌린 변조를 다시 돌리는 것으로는 새로운 정보가 나오지 않았다.
나중에는 LLM의 코드 리뷰 단계를 과감히 생략하고 엣지 케이스 변조 테스트만 집중적으로 돌린 작업도 있었다. 해 보니 변조 쪽이 리뷰어보다 더 효과적이었다. (단, 변조 테스트를 돌리는 전체 범위 자체를 줄인 것이 아니라, 작성하는 변조 케이스의 개수와 검토 단계를 최적화한 것이다.)
개선 결과
| 첫 번째 Task | 이후 두 Task | 5번째 Task | 6번째 Task | |
|---|---|---|---|---|
| 소요 시간 | 100분 | 30분 | 55분 | 60분 |
| 스펙 리뷰 라운드 | 3회 | 1회 | 2회 | 2회 |
초기 100분이 걸리던 작업이 30분으로 단축되었다. 5~6번째 작업에서 시간이 다시 늘어난 것은 SKILL.md의 위험한 프롬프트(경고 문구와 정반대로 동작하는 안내 등 사용자에게 치명적인 영향을 줄 수 있는 오류)를 수정하는 과정에서 리뷰가 제 역할을 톡톡히 했기 때문이다. 이 시간은 결코 낭비가 아니었다.
효율적인 테스트 구동 습관
테스트 전략 외에 일하는 습관도 바뀌었다.
작업 중에는 관련된 테스트만
초기에는 코드 한 줄을 고칠 때마다 전체 테스트를 전부 돌렸다. 속도가 느려지자, 작업 중에는 현재 수정 중인 도메인과 관련된 테스트만 좁혀서 실행하도록 계획서에 명시했다.
# 실패 확인 — 이번 변경과 직접 관련된 테스트만 빠르게 실행
uv run --with pytest pytest plugins/claude-sync/tests/test_reconcile.py -q -k "reject_bucket or splits_reject or excluded_file"
작업 완료 직전에는 반드시 전체 테스트
하지만 좁게만 돌리면 사각지대가 생긴다. 한 번은 리뷰 중에 핵심 규칙 하나를 일부러 지워 보는 실험을 했는데, 해당 모듈의 테스트 20개는 모두 통과했다. 전체 스위트를 돌려보고서야 전혀 다른 모듈에 있던 테스트 2개가 실패하며 그 결함을 잡아냈다.
따라서 작업 중에는 빠르고 좁게 테스트하되, 작업을 닫고 커밋하기 직전에는 반드시 전체 테스트를 구동하는 원칙을 세웠다.
# 통합 통과 확인 — 커밋 전 전체 스위트 실행
uv run --with pytest pytest plugins/claude-sync/tests -q
마무리
테스트 코드를 많이 작성하는 것 자체는 크게 어렵지 않았다. 정말 어려웠던 것은 이 테스트들이 실질적인 가치를 지니고 있는지 증명하는 일, 그리고 언제 검증을 멈춰야 할지 적정선을 찾는 일이었다. 검증에도 명백한 비용이 존재하며, 그 비용을 위험도에 맞게 효율적으로 분배하는 것까지가 엔지니어링임을 배웠다.
claude-sync가 정확히 어떤 문제를 해결하는 도구인지는 이전 글에, 설치 명령어와 저장소 링크는 Lab의 Claude Tools 페이지에 정리해 두었다. 상세한 구현 코드와 1,269개의 테스트는 GitHub 저장소에서 확인할 수 있다.