TopicReview의 1,562줄 테스트는 거의 전부 Claude가 작성했다. 나는 review만—배포 후 2주가 넘도록 추가된 테스트는 있어도 고쳐 쓴 테스트는 없다. 이 글은 왜 테스트가 Claude에게 맡기기 가장 좋은 일인지, review에서 봐야 할 것과 보지 말아야 할 것(실제 spec에 숨어 있는 누락된 경계 포함), 그리고 이 분업의 기본 설정을 다룬다.
지난 글 말미에 TopicReview의 "119 spec 전부 녹색"이라고 했다. 독자가 진짜로 던질 다음 질문: 그 테스트 누가 썼는가?
답: Claude가 1,562줄 테스트 코드의 거의 전부를 썼다. 나는 review만 한다. 배포 2주가 넘도록 이 1,562줄의 유지보수는 새 테스트를 추가할 뿐, 기존 테스트를 고쳐 쓴 적은 없다.
이 글은 왜 테스트가 Claude에게 맡기기 가장 좋은 일인지, review에서 무엇을 보고 무엇을 보지 않아야 하는지, 그리고 이 분업이 실제로 어디까지 작동하는지에 관한 것이다.
TopicReview의 테스트는 7개 파일에 나뉘어 있다:
spec/services/topic_review_service_spec.rb 760줄 (88 테스트)
spec/requests/topic_reviews_spec.rb 281줄 (32 테스트)
spec/requests/review_appeals_spec.rb 152줄 (16 테스트)
spec/requests/review_votes_spec.rb 127줄
spec/policies/topic_review_policy_spec.rb 109줄
spec/jobs/close_topic_review_job_spec.rb 71줄 (7 테스트)
spec/models/topic_review_spec.rb 62줄
───────────────────────────────────────────
1,562줄
네 가지 테스트 타입을 커버한다: service(비즈니스 로직), request(컨트롤러 + 통합), policy(Pundit 권한), job(스케줄 작업).
초기 commit d162f1e는 Co-Authored-By: Claude Sonnet 4.6가 찍혀 있고, 이 중 1,100줄 이상을 한 번에 썼다. 이후의 spec 변경은 전부 "Add test for..."—refactor나 rewrite는 한 건도 없다:
00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning
3b185da Update specs to use PROVISIONAL_PENALTY constant
구멍 메우기지 되돌아가는 작업이 아니다. 이 디테일이 뒤에서 다시 중요해진다.
단단한 이유 네 가지:
1. 입력과 출력이 명확하다. 테스트의 본질은 "주어진 상태 → 기대 행위"다. 이것이 Claude가 가장 잘하는 번역이다—스펙을 assertion으로 바꾸는 일. 비즈니스 로직은 때로 저울질이 필요하지만, 테스트는 그럴 게 별로 없다.
2. 기계적 × 대량. 하나의 describe .open! 아래에 "eligible jurors 있음 / 없음 / topic 없음 / 이미 active review 있음"의 네 가지 context, 각 context에 2–5개의 it 블록. 사람은 세 번째 context쯤 되면 반사적으로 몇 케이스 빼먹고 싶어진다. Claude는 게으름 피우지 않는다—88번째 it를 첫 번째와 똑같은 성실함으로 쓴다.
3. 피드백 루프가 극단적으로 짧다. 테스트를 쓰고 rspec을 돌리면 통과/실패가 즉시 확인된다. 비즈니스 코드는 실제 사용한 지 며칠은 지나야 문제가 드러난다. 루프가 짧다는 건 Claude의 실수가 rspec에 바로 잡힌다는 뜻—지켜볼 필요가 없다.
4. 자연스럽게 병렬적. it 블록들은 서로 독립이고, 숨은 의존이 없으며, 대규모로 펼칠 수 있다. Claude가 독립 테스트 수십 개를 한 번에 생성하는 건 정확히 그의 강점이다.
이 부분이 분업의 핵심이다.
보지 않는다:
본다:
마지막 항목이 review의 진짜 가치다. Claude는 "자신이 떠올릴 수 있는" 테스트는 커버하지만, 떠올리지 못하는 테스트는 자발적으로 보완하지 않는다. 이게 바로 human review의 자리다—비즈니스 규칙으로부터 거꾸로 커버리지 누락을 추적하는 것.
spec/services/topic_review_service_spec.rb 앞부분의 describe ".open!"를 연다:
describe ".open!" do
context "when there are eligible jurors" do
# review 상태 정상 / post under_review / assignments 생성 / author 알림 / jurors 알림 / 이중 open 없음
end
context "when there are no eligible jurors" do
# review는 생성되지만 assignments는 생성 안 됨
end
context "when post has no topic" do
# nil 반환
end
end
보기엔 포괄적이다. 하지만 model의 eligible_jurors 실제 규칙은 세 부류를 배제한다:
def eligible_jurors
excluded_ids = [ post.user_id ] + post.reports.pluck(:user_id) + review_votes.where(stage: :initial).pluck(:user_id)
User.jurors_and_judges.where.not(id: excluded_ids.uniq)
end
이제 테스트를 보자—"원작자는 배심원으로 선정되지 않는다"를 단언하는 테스트는 어디 있는가?
service_spec.rb와 model_spec.rb를 다 뒤져도 없다. model_spec.rb는 pending_vote_by 스코프의 몇 케이스만 다루고 eligible_jurors 자체는 다루지 않는다. service_spec.rb에는 주석 한 줄뿐: # Jurors must NOT be the post author—setup의 주석이지, 단언이 아니다.
이것이 review가 발견할 수 있는 누락이다: 세 가지 배제 규칙(원작자 / 신고자 / 초심 투표자), 어느 하나도 테스트로 보호되지 않는다. 나중에 누군가 eligible_jurors의 구현을 리팩터링하면서 실수로 post.user_id를 배제 리스트에서 빠뜨려도, 기존 테스트는 모두 통과한다—그리고 프로덕션은 조용히 원작자를 자기 배심원석에 앉힌다.
Claude는 틀리지 않았다. 요청된 것을 테스트했을 뿐이다. 다만 "이 세 규칙 각각에 테스트 보호가 필요한가"를 스스로 묻지 않았다. 이 질문—규칙에서 커버리지로의 역추적—이 review가 해야 할 일이다.
(솔직히 나도 첫 review 때 이걸 놓쳤다. 이 글을 쓰기 위한 두 번째 audit에서야 발견했다. 그래서 review도 한 방에 끝나지 않는다—하지만 안 하는 것보다 10배 낫다.)
"Claude가 쓰고 + human이 review"가 완벽한 분업이라면, 초기 commit 이후에는 새 테스트 commit이 없어야 한다. 실상은 더 흥미롭다—구멍 메우기는 있지만 다시 쓰기는 없다:
00393fc Add test for finalize! with zero votes (expired review)
3f53304 Add test for finalize! with legacy votes missing reasoning
첫 번째는 버그 발견 후의 regression test—e8cb2db Default to keep verdict when review expires with zero votes가 fix, 00393fc가 세트로 따라붙은 추가 테스트. 두 번째도 같은 패턴으로 abaa22e Fix CloseTopicReviewJob failing due to reasoning validation on old votes 뒤에 따라붙는다.
이 두 commit이 동시에 증명하는 것:
"충분히 좋음 + 계속 보완 가능"—"완벽"보다 훨씬 현실적인 기준. 완벽한 review를 쫓다 보면 Claude에게 테스트를 맡기지 못하게 된다. "충분히 좋음"을 받아들여야 분업이 시작된다.
모든 테스트가 완전 위임에 적합하지는 않다:
이런 경우 사람이 주도하고 Claude가 보조한다.
이 분업을 실행 가능한 default로 만들면:
프로그래머가 테스트를 읽을 때의 심리 자원은 코드를 읽을 때보다 적다. 테스트는 반복적이고, 기계적이고, 지치게 하지만 필수적이다. 이 모든 특징이 Claude의 강점과 겹친다—지루해하지 않고, 지치지 않고, 50번째 it에서 대충하지 않는다.
당신이 할 일은 "테스트를 쓰는 것"이 아니라 "모든 비즈니스 규칙이 테스트로 덮이도록 보장하는 것"이다. 하나는 구현, 다른 하나는 판단—판단은 당신에게 남고, 구현은 Claude에게 간다.
119 spec / 1,562줄이 한 번의 배포로 올라가고 2주 넘도록 rework 없이 돌아가는 건 내가 테스트를 더 잘 써서가 아니라, 내가 전혀 쓰지 않기 때문이다. 나는 Claude보다 한 가지만 더 한다: 어떤 비즈니스 규칙이 보호받아야 하는지 결정하는 일.