테스트 코드만 추가한 PR — 그리고 genAI 시대의 TDD

Published:

머지된 PR: aws/aws-cli#10152 (CLIv2), aws/aws-cli#10249 (CLIv1 백포트). 2026-04-27 v2.34.37 / v1.44.86 으로 릴리스.

지난 fish shell 글 에 이은 두 번째 aws-cli contribution. 이번엔 6 년 묵은 이슈 #5084 를 골랐다. 이번 글이 흥미로웠던 건 — 내가 한 일이 사실상 “테스트 코드 8 줄을 추가한 것” 이 전부였기 때문이다.

한 줄짜리 fix 가 6 년 동안 머지되지 못한 이유. 그리고 8 줄짜리 테스트가 그걸 풀었다는 사실. 이 사건을 따라가면 자연스럽게 “테스트 코드란 뭐고 왜 중요한가” → “TDD” → “genAI 시대의 TDD” 로 이어진다.

이슈 #5084 의 6 년

타임라인부터.

2020-03-27. thenoid 라는 사용자가 이슈 #5084 와 PR #5085 를 같이 올렸다. 문제는 단순했다 — legacy boto 호환 문제다.

옛날 boto 라이브러리는 임시 자격증명의 세션 토큰을 aws_security_token 이라는 키 이름으로 ~/.aws/credentials 에서 읽었다. 후속 boto3 는 같은 값을 aws_session_token 으로 부른다. 그래서 aws-cli 는 두 키 모두 인식은 한다. 그런데 aws configure set 으로 저장할 때 동작이 갈렸다.

aws configure set aws_session_token VALUE   # → ~/.aws/credentials  (정상)
aws configure set aws_security_token VALUE  # → ~/.aws/config       (legacy 와 안 맞음)

aws_security_token 을 set 하면 자격증명 파일이 아니라 일반 config 파일에 가서 박혔다. 그러면 옛날 boto 코드는 그걸 못 읽는다. legacy 환경을 쓰는 사람들은 매번 ~/.aws/credentials 를 손으로 고쳐야 했던 셈.

원인은 명확했다. aws-cli 의 ConfigureSetCommand 안에 “이 키들은 credentials 파일로 보내라” 는 화이트리스트가 있는데, aws_security_token 만 빠져 있었다. 한 줄 추가하면 끝.

thenoid 는 이슈를 올린 다음, 같은 날 그 한 줄짜리 PR 까지 첨부했다. 본인이 풀이까지 가져온 좋은 contribution 이었다.

2020-07-22. 메인테이너 joguSD 가 PR 에 코멘트를 달았다.

This looks right, could we get a test similar to test_session_token_written_to_shared_credentials_file in tests/unit/customizations/configure/test_set.py for this?

요지: fix 는 맞아 보인다, 그런데 옆에 이미 있는 test_session_token_... 패턴을 본떠서 aws_security_token 용 테스트를 하나만 추가해 달라.

이 코멘트에 대한 응답은 — 없었다.

2022-04-04. 거의 2 년 뒤. 메인테이너 stealthycoin 이 다시 코멘트.

Our team just put out a recent proposal in #6828 detailing improvements to the contribution process. We are working through open PRs and have determined this PR to be in the Implementation stage. Action items would be addressing the above comment from joguSD.

aws-cli 팀이 컨트리뷰션 가이드를 새로 정비하면서 묵은 PR 들을 다시 훑었고, “이건 구현 단계에 멈춰 있다, 위 코멘트만 해결되면 된다” 고 다시 핑했다. 그래도 응답이 없었다.

2023-08-02. 또 1 년이 지나고, tim-finnigan 이 PR 을 close 했다.

Closing as we have not heard back regarding this PR and the request to add a test mentioned above. We can continue tracking the issue (#5084) and revisit the proposed changes.

PR 은 닫혔지만 이슈는 살아있었다. 그리고 거의 3 년이 더 흘러서 — 내가 그걸 봤다.

2026-03-24. PR #10152 을 올렸다. 한 줄 fix 는 thenoid 의 원안 그대로. 거기에 5 년 전에 메인테이너가 가리켰던 패턴 그대로 테스트 함수 하나, 그리고 체인지로그를 붙였다. 4 월 24 일에 v2 브랜치로 머지, CLIv1 백포트 PR #10249 도 같이 머지. 4 월 27 일 v2.34.37 / v1.44.86 으로 릴리스. 6 년이 마무리된 순간이었다.

내가 한 일 — 14 줄

gh pr view 10152 --json additions 로 보면 전체 추가 라인이 14 줄, 삭제 0 줄이다.

세 파일이 바뀌었다.

1. awscli/customizations/configure/set.py — 한 줄. WRITE_TO_CREDS_FILE_KEYS 상수 리스트에 'aws_security_token', 한 줄을 추가했다. 원안 그대로다.

WRITE_TO_CREDS_FILE_KEYS = [
    'aws_access_key_id',
    'aws_secret_access_key',
    'aws_session_token',
    'aws_security_token',   # 새로 추가
]

2. tests/unit/customizations/configure/test_set.py — 8 줄. configure set 의 unit 테스트 파일에 들어 있는 test_session_token_written_to_shared_credentials_file 를 그대로 모사했다.

def test_security_token_written_to_shared_credentials_file(self):
    set_command = ConfigureSetCommand(self.session, self.config_writer)
    set_command(args=['aws_security_token', 'foo'], parsed_globals=None)
    self.config_writer.update_config.assert_called_with(
        {'__section__': 'default', 'aws_security_token': 'foo'},
        self.fake_credentials_filename,
    )

읽어보면 의도가 한눈에 보인다 — “aws_security_token 을 set 하면, config 가 아니라 credentials 파일 쪽 writer 가 호출되어야 한다.” 이게 빠져 있어서 6 년 동안 머지되지 않았다.

3. .changes/next-release/enhancement-configure-59283.json — 5 줄. aws-cli 는 체인지로그를 직접 손으로 쓰는 게 아니라 scripts/new-change 가 JSON 파일을 만들어주고, 릴리스할 때 자동 합쳐진다. 이걸 빼먹어서 첫 리뷰에서 한 번 코멘트를 받았다.

리뷰는 의외로 빨리 마무리됐다. 메인테이너 ashovlin 이 4 월 21 일에 “diff 는 좋은데 체인지로그만 추가해 달라”, 4 월 23 일에 “CIv1 으로도 백포트 가능하냐” 두 번 코멘트하고 끝. 6 년이 걸린 머지치고는 본 작업이 1 주일도 안 걸린 셈이다.

핵심은 이거다 — fix 가 막혀 있던 게 아니라 테스트가 막혀 있었다.

테스트 코드란 무엇인가

가장 짧게 정의하면 “코드를 검증하는 또 다른 코드” 다. 본 코드와 같은 언어로, 같은 레포 안에, CI 가 매번 같이 돌리는 — 실행 가능한 명세.

이 정의에서 중요한 건 “실행 가능한” 이다. 글로 쓴 명세 (스펙 문서, README, comment, 사양서) 와 비교해 보면 차이가 명확해진다.

항목글로 쓴 명세테스트 코드
코드와의 동기화사람이 직접 맞춰야 함코드가 바뀌면 깨져서 알려줌
모호성자연어 → 해석 여지입력/출력이 정확히 못 박힘
검증 비용사람이 읽고 판단CI 가 자동으로 통과/실패
시간이 흐른 후drift 누적, 결국 거짓말drift 가 코드 변경 즉시 빨간불

자연어 명세는 시간이 지나면 거짓말이 된다. 테스트는 그렇지 않다. 테스트가 통과한다는 건 — 그 시점에 적어도 그 동작은 살아있다는 증거다.

여기서 테스트 코드가 하는 네 가지 역할이 나온다.

1. 회귀 (regression) 방지

리팩터, 의존성 업그레이드, 다른 기능 추가 — 모든 변경은 잠재적인 regression 이다. 테스트는 “지금까지 작동했던 것” 을 박제해서, 미래의 누군가가 모르고 깨뜨리면 즉시 알려준다. aws-cli 의 unit 테스트 폴더에 있는 수천 개의 함수가 사실상 다 이 역할이다.

2. Intent 의 문서화

테스트 함수 이름이 test_security_token_written_to_shared_credentials_file 이라고 적혀 있으면, 그게 곧 “이 코드는 security token 을 shared credentials 파일에 쓰도록 의도되어 있다” 는 선언이다. 다음 사람이 한 달 후, 1 년 후에 와도 그 의도를 코드로 읽을 수 있다.

내가 추가한 테스트도 본질적으로 이 역할이다. legacy boto 호환을 위해 aws_security_token 을 credentials 쪽으로 흘려보내야 한다는 의도를, 코드로 박제했다. 다음에 누가 리팩터하다가 무심코 키를 빼버려도 — CI 가 잡아준다.

3. 리팩터의 안전망

“리팩터” 라는 단어가 의미를 가지려면 “외부 동작은 그대로” 라는 보장이 있어야 한다. 그 보장의 정체가 곧 테스트 통과다. 테스트가 없으면 리팩터는 그냥 “이게 아직도 작동하기를 바라며 코드를 옮기는 행위” 일 뿐이다.

4. 협업 시 PR 리뷰의 시그널

OSS 메인테이너 입장에서 가장 중요한 게 바로 이거다. 본인이 그 도메인에 익숙하지 않더라도, 테스트가 명확하면 “최소한 이 코드는 의도한 동작을 한다” 가 보장된다. 그래서 #5084 fix 가 6 년 동안 머지될 수 없었다.

joguSD 의 입장에서 생각해보면 단순하다. 메인테이너는 본인이 모든 코드 경로를 머릿속에 다 들고 있을 수 없다. PR 이 들어오면 “이 한 줄을 바꿨을 때 다른 시나리오가 안 깨지는가” 를 빠르게 확인할 방법이 필요한데, 그게 테스트다. 테스트가 있으면 머지 후에 누가 또 건드려도 자동 검증된다. 테스트가 없으면 — 그 한 줄의 fix 가 미래의 어떤 변경에서 조용히 망가져도 알 길이 없다.

한 줄 fix 도 테스트 없이는 받을 수 없다. 코드는 “동작” 만 약속하지만, 테스트는 “동작이 계속 유지된다는 약속” 을 추가한다. OSS 메인테이너가 사는 시간 축은 PR 작성자의 시간 축보다 훨씬 길다.

테스트의 종류 — 피라미드

테스트는 한 종류만 있는 게 아니다. 보통 테스트 피라미드 라는 모양으로 정리한다 (Mike Cohn, Succeeding with Agile, 2009).

         /\
        /  \  E2E (적게)
       /----\
      /      \  Integration (중간)
     /--------\
    /          \  Unit (많이)
   /____________\
  • Unit test — 함수/클래스 하나만 격리해서 본다. 외부 IO, DB, 네트워크는 mock 으로 차단. 빠르고 결정적이다. 내가 추가한 test_security_token_... 도 여기 속한다 — ConfigureSetCommand 만 띄우고, config_writer 는 mock 객체.
  • Integration test — 컴포넌트 간 결합을 본다. 실제 DB 를 띄우거나, 여러 서비스를 함께 돌린다. 느리지만 더 진짜에 가깝다.
  • E2E test — 사용자 시점에서 시스템 전체를 본다. UI 자동화, 실제 네트워크 호출 포함. 느리고 깨지기 쉽지만 “정말 굴러가는가” 를 검증한다.

피라미드 모양인 이유는 실행 비용 / 신뢰성 / 정보량의 트레이드오프 때문이다. Unit 은 싸고 빠르지만 “이 컴포넌트가 진짜 시스템에서 의도대로 협업하는가” 는 못 본다. E2E 는 그걸 보지만 한 번 돌리는 데 분 단위가 걸리고 외부 요인에 깨지기 쉽다. 그래서 일반적으로 unit 으로 폭넓게 깔고, integration 으로 핵심 결합을 검증하고, E2E 는 가장 중요한 시나리오 몇 개만.

aws-cli 같은 CLI 도구는 unit 비중이 압도적으로 높다. 외부에 부수효과를 일으키는 코드들도 대부분 mock 으로 차단해서 unit 영역에서 검증한다. tests/unit/ 디렉토리가 그래서 그렇게 크다.

Mock 의 트레이드오프

unit 테스트의 핵심 도구가 mock 이다. 외부 의존성을 가짜 객체로 바꿔서, 본인이 검증하려는 컴포넌트만 격리한다. 내 테스트에서도 self.config_writer 가 mock 이다 — 진짜 파일을 안 건드리고, “어떤 인자로 update_config 가 호출됐는가” 만 검증한다.

빠르고, 결정적이고, 외부 환경에 안 흔들린다. 그런데 함정이 있다.

Mock 은 통과하는데 prod 는 깨질 수 있다. mock 이 모사하는 인터페이스가 실제와 어긋나면 — mock 위에선 다 잘 도는데 진짜 객체로 바꾸는 순간 무너진다. 외부 라이브러리의 contract 가 마이너 버전 사이에 바뀌어도 mock 은 그걸 모른다.

이걸 어떻게 다룰지에서 두 학파가 갈린다.

  • Classic / Detroit 학파 (Kent Beck, Martin Fowler 류): mock 은 최소한으로. 가능하면 실제 객체로 테스트한다. 외부 IO 만 fake/stub 으로 대체. 결과 (state) 를 검증.
  • London / Mockist 학파 (Steve Freeman, Nat Pryce, Growing Object-Oriented Software, Guided by Tests): mock 을 적극적으로. 객체 간 협력 (interaction) 을 검증한다. 모든 의존성을 mock 으로 갈고, 호출 자체가 명세.

두 학파 다 합리적인 근거가 있고, 실무는 보통 섞어 쓴다. 도메인 로직처럼 결과가 명확한 곳은 classic, 협력 패턴이 본질인 곳은 london — 식의 운용이 일반적이다.

aws-cli 의 unit 테스트는 mockist 쪽에 가깝다. 내가 추가한 테스트도 “update_config 가 이런 인자로 호출되는가” 라는 interaction 검증이다. CLI 도구의 본질은 “외부 시스템 (파일, API) 과의 협력” 이라서 mockist 가 자연스럽다.

TDD — Red / Green / Refactor

여기까지는 “코드 다 짜고 나서 테스트를 붙인다” 는 흐름이었다. 내 PR 도 사실 이 모드다 — fix 는 이미 있었고, 테스트를 사후에 붙였다.

TDD (Test-Driven Development) 는 순서를 뒤집는다. 테스트를 먼저 쓴다.

Kent Beck 이 Test-Driven Development: By Example (2002) 에서 정리한 사이클이 유명하다.

+---------+        +---------+        +-----------+
|   Red   |  --->  |  Green  |  --->  | Refactor  |
+---------+        +---------+        +-----------+
     ^                                       |
     +---------------------------------------+
  • Red — 아직 구현이 없는 동작에 대해 실패하는 테스트를 쓴다. 컴파일이 안 되거나, 예상 결과와 다르거나, 어쨌든 빨갛게 떨어진다.
  • Green — 테스트를 통과시키는 최소한의 코드 만 쓴다. 우아함, 일반성, 확장성 다 잠시 미뤄두고. “테스트만 통과하면 된다.”
  • Refactor — 통과 상태를 유지하면서 중복을 제거하고 구조를 다듬는다. 테스트가 있으니 안전망 위에서 작업하는 셈.

이 사이클을 분 단위로 짧게 돌린다. Red 한 번에 한 동작만, Green 도 한 번에 한 동작만. 사이클이 1~5 분쯤이면 잘 굴리고 있는 것이고, 30 분 넘어가면 너무 큰 step 을 잡았다는 신호.

왜 먼저 테스트를 쓰는가

세 가지 효과가 있다.

1. 의도를 먼저 박제한다. “이 함수는 무엇을 해야 하는가” 를 코드로 적고 시작하면, 구현하는 동안 헷갈릴 일이 없다. 구현이 의도에 맞춰가는 게 아니라, 의도가 먼저 못 박혀 있고 구현이 거기 맞춰진다.

2. 테스트 가능한 구조로 자연스럽게 흘러간다. 외부 의존성을 격리해야 테스트가 쉽게 써지므로, 자연스럽게 의존성 주입, 작은 함수 단위, 단일 책임 원칙 같은 설계 결정이 따라온다. TDD 가 “사실은 설계 도구” 라는 말이 여기서 나온다.

3. 짧은 피드백 루프. Red → Green 까지가 분 단위라서, 한 사이클에서 잘못된 가정이 드러난다. 큰 덩어리로 짠 다음 마지막에 통합해서 깨지는 패턴 (big-bang integration) 을 막는다.

TDD 가 잘 맞는 곳 / 잘 안 맞는 곳

마법의 도구는 아니다.

잘 맞는 곳:

  • 순수 함수, 알고리즘 — 입력/출력이 명확하면 테스트 작성이 쉽다. parser, validator, calculation.
  • 비즈니스 로직 — “할인율은 이렇게 계산되어야 한다” 같은 규칙. 자연어 요구사항을 테스트로 그대로 옮길 수 있다.
  • 버그 fix — 버그를 재현하는 테스트를 먼저 쓰고 (Red), fix 한다 (Green). 같은 버그가 다시 안 돌아오는 게 자동 보장.
  • legacy 코드의 안전망 만들기 — characterization test 라 부른다. 현재 동작을 일단 박제해야 안전한 변경이 가능.

잘 안 맞는 곳:

  • UI / 시각적 출력 — “버튼이 예쁘게 보인다” 를 테스트로 표현하기 어렵다.
  • 외부 API 통합 초기 탐색 — 그 API 가 실제로 어떻게 동작하는지 모르는 상태에서 테스트 먼저 쓰면, 테스트 자체가 잘못된 가정 위에서 굳는다. 이럴 땐 spike 로 먼저 동작을 확인하고, 알게 된 것을 테스트로 옮기는 게 낫다.
  • 프로토타입, 탐색적 코드 — “이게 가능한지 보자” 단계에서 TDD 는 오히려 발걸음을 무겁게 한다.

내 #5084 PR 같은 건 TDD 의 정석 케이스다. 동작이 명확하고 (aws_security_token 은 credentials 파일로 가야 한다), 입출력이 한정적이고, 회귀 위험이 분명하다. fix 한 줄을 넣기 전에 테스트를 먼저 쓰고 Red 를 본 다음 fix 했다면 — 그게 TDD 의 정석 흐름이었을 거다.

genAI 시대의 TDD — 내가 진행하는 이유

Claude / Cursor / Copilot 으로 코딩하는 시간이 늘어나면서 테스트의 의미가 다시 무거워졌다. 정확히는 — TDD 의 가치가 다시 무거워졌다.

GeekNews 의 “AI 시대의 개발 방법론 — SDD+TDD” 글 이다. 요약하면 이런 주장이다.

AI 가 사람보다 빠르게 코드를 만들어내는 시대에는, 개발자가 “코드를 쓰는 사람” 에서 “의도를 정의하고 검증하는 아키텍트” 로 옮겨가야 한다. 테스트는 AI hallucination 의 가드레일이다. 구현보다 먼저 테스트를 적어서, AI 의 창의성을 요구사항 경계 안에 가둔다. 사람은 What/Why 를 정의하고, AI 는 How 를 구현한다.

Kent Beck — TDD 의 저자가 AI 시대에 하는 말

흥미로운 건 Test-Driven Development: By Example 을 쓴 Kent Beck 본인이 AI 시대의 TDD 에 대해 한 인터뷰 다. 52 년 경력의 개발자가 LLM 도구를 직접 써보고 정리한 관찰이라 무게가 다르다. 몇 가지가 인상적이었다.

TDD 는 결함률이 아니라 불안 (anxiety) 을 다루는 도구. Beck 은 프로그래밍을 “끝없는 불안의 원천” 이라고 표현한다. TDD 가 그 불안을 “완전히 사라지게 한다” 는 게 그가 30 년간 TDD 를 끌고 온 진짜 이유다. 결함 밀도 감소 같은 기술적 효과보다 — “이 코드가 작동한다는 확신” 이라는 심리적 효과가 더 크다는 것. AI 와 코딩할 때 이 부분이 더 절실해진다. AI 가 만든 코드를 라인 단위로 따라가며 검증하는 동안 쌓이는 불안 — 그걸 끄는 게 결국 통과하는 테스트 묶음이다.

AI 는 “예측 불가능한 지니 (unpredictable genie)”. Beck 의 표현. 의도를 자주 잘못 해석하고, 본인만의 agenda 가 있고, 가끔 놀라운 설계 답을 내놓기도 한다. 그래서 “슬롯머신처럼 간헐적 보상” 을 주는 도구라고 평한다 — 한 번 더 프롬프트를 던지고 싶은 충동이 계속 든다. 이 비유가 정확하다. AI 와의 페어 프로그래밍은 안정적인 도구를 쓰는 것보다 도박판에 가깝고, 테스트는 그 도박판에서 자기를 보호하는 유일한 안전장치다.

AI 가 약한 영역은 결합도 (coupling) 와 응집도 (cohesion). Beck 의 진단. AI 는 한 자리의 변경이 멀리서 일으키는 ripple effect 를 잘 못 잡는다. 그래서 그가 쓰는 방어 수단이 — 300 밀리초 안에 끝나는 거대한 테스트 묶음. AI 가 무심코 깬 곳을 즉시 빨갛게 만든다. 빠른 테스트가 사치가 아니라 AI 시대의 필수 인프라가 됐다.

Immutable annotations. Beck 이 본인 워크플로에서 쓰는 트릭. 핵심 테스트에 “이건 정답이다, 바꾸면 영원히 어둠 속에서 깨어 있게 된다 (Change it and you’ll be awake in darkness forever)” 같은 강한 annotation 을 박아둔다. AI 가 통과시키지 못하는 테스트를 만나면 — 가끔 테스트 자체를 손대거나 지우려 하기 때문에. 사람이 못 박아둔 invariant 가 있어야 AI 가 그 안에서만 논다.

이 네 가지를 깔고, 내가 직접 느낀 네 가지를 얹는다.

1. AI 의 코드는 그럴듯하다 — 그게 가장 위험하다

AI 가 만드는 가장 흔한 실패 모드는 “완전히 틀린 코드” 가 아니라 “그럴듯해 보이는데 미묘하게 틀린 코드” 다. 함수 시그니처가 살짝 다른 라이브러리, 옵션 이름이 헷갈린 호출, edge case 한두 개가 빠진 분기. 사람이 코드 라인을 한 줄씩 읽어서 잡아내려 하면 — 잡힌다는 보장이 없고 시간만 잡아먹는다.

테스트가 먼저 있으면 그럴듯함이 못 통과한다. 테스트를 통과하지 못한 코드는 — 그게 AI 가 짠 거든 사람이 짠 거든, 그냥 틀린 거다. 테스트가 판단 부담을 사람에서 CI 로 옮긴다.

2. Spec-as-test — 테스트가 가장 정확한 프롬프트다

자연어 프롬프트로 AI 에게 요구사항을 전달하는 건 본질적으로 모호하다. “사용자 입력 검증 함수 만들어줘” 라고 하면 AI 가 만들어 낸 결과는 매번 달라진다. email 만 보는 버전, password 강도까지 보는 버전, 한국어 이름을 거절하는 버전 — 다 “그럴듯한 해석” 이다.

같은 요구사항을 테스트로 적으면 모호하지 않다. 입력 X 일 때 출력 Y 여야 한다. 이게 통과하지 못하면 끝. AI 가 hallucinated API 를 가져다 써도 — 그 함수가 import 단계에서 실패하거나, 호출 결과가 expected 와 안 맞으면 — 즉시 빨간불이다.

테스트가 곧 사양이고, 사양이 곧 프롬프트가 된다. 자연어로 풀어 쓴 요구사항보다 정밀하고, AI 가 정확히 어디까지 해야 하는지 경계가 분명하다.

3. Refactor 단계가 거의 공짜가 된다

전통적인 TDD 에서 가장 부담스러웠던 게 Refactor 였다. Green 까지 갔으면 이미 동작은 하니까, 사람은 “이 정도면 됐어” 하고 다음으로 넘어가고 싶어진다. 그래서 코드 베이스에 중복과 어색한 네이밍이 쌓인다.

AI 는 Refactor 단계의 grunt work 를 사람보다 잘한다. 네이밍 통일, 중복 제거, 추상화 추출, 매개변수 정리 — 지치지 않고 빠르게. 사람이 할 일은 Red 만 잘 잡아두고, Green 까지 만든 코드를 AI 에게 “테스트 다 통과시키면서 깔끔하게 다듬어줘” 라고 넘기는 것뿐이다.

Red 만 사람이 책임지면, 나머지 사이클은 AI 가 거의 자동화한다. 그래서 TDD 의 가장 약한 고리였던 “사람이 Refactor 를 안 한다” 가 풀린다.

4. 리뷰의 무게중심이 옮겨간다

PR 리뷰 방식 자체가 바뀐다. AI 와 코딩하면 PR diff 가 사람이 쓸 때보다 커지는 경향이 있다 — AI 는 한 번에 더 많은 라인을 만들어내니까. 라인 단위 리뷰는 더 이상 현실적이지 않다.

대신 테스트만 보는 리뷰 가 의미를 가진다. “이 테스트가 옳은 의도를 잡고 있는가” 만 검증하면 — 구현이 옳다는 건 그 테스트의 통과가 보장한다. 리뷰어의 시간이 “구현 라인을 따라가며 버그 찾기” 에서 “테스트가 시나리오를 충분히 cover 하는가, 의도가 정확한가” 로 옮겨간다.

내가 PR #10152 에서 받은 리뷰가 사실 이 모양이었다. ashovlin 은 14 줄 diff 에서 한 줄 fix 의 정확성을 길게 검증하지 않았다. 그것보다 — 테스트가 기존 패턴 (test_session_token_...) 을 정확히 따라가는지, 체인지로그가 형식을 지키는지를 봤다. 본질적으로 fix 의 옳음을 테스트가 대신 증명 한 셈이다.

한계 — TDD 도 만능은 아니다

위 네 가지가 말끔하게 작동하는 건 도메인 로직, 데이터 처리, 알고리즘 영역이다. 외부 통합 (서드파티 API, 디바이스 IO), UI, 탐색적 작업에선 여전히 테스트 먼저가 어렵다.

그래서 실제 운용은 — 도메인/데이터 계층에선 TDD 우선, 통합/UI 는 끝나고 회귀 테스트로 채우기. 이게 내가 genAI 와 쓰는 실용적인 분배다. AI 의 코드 생성 속도가 빨라질수록, 빨라진 만큼 테스트가 적용 가능한 영역에선 테스트를 우선 박는 게 유일하게 유효한 가드레일이다.

닫는 글

한 줄 fix 가 6 년 묵었던 이유는 “테스트가 없어서”. 한 줄 fix 가 풀린 이유도 “테스트 8 줄을 보탰기 때문”. 별거 없는 이벤트인데, 테스트가 코드의 통화 (currency) 다 라는 걸 다시 새기게 한 사건이었다.

코드는 “지금 동작한다” 를 증명한다. 테스트는 “앞으로도 동작할 것” 을 증명한다. 메인테이너가 받아들일 수 있는 건 후자이고, 그래서 테스트가 머지 가능성의 화폐다.

AI 에게 코드를 점점 더 위임할수록 — 그 통화의 가치는 더 올라간다. AI 가 만드는 그럴듯한 코드를 거르고, 모호한 자연어 사양을 정확한 사양으로 박제하고, 빨라진 생성 속도에 맞춰 회귀를 잡아주는 — 결국 테스트만이 그걸 한다.

다음 contribution 에선 — 테스트 먼저 쓰고 시작해보려고 한다. 이번엔 사후에 테스트를 붙였지만, 다음엔 Red 부터.