· 17 min read

완벽과 AI, 그리고 TDD

사람이든 AI든 완벽할 수 없다. AI가 코드 변경량을 늘리는 동시에 테스트 작성 비용을 낮춘 지금, TDD와 CI가 왜 더 중요하고 더 가성비 좋은 선택이 되었는지 실제 게임·프레임워크 개발 경험으로 정리했다.

AI가 만든 결과물은 믿을 수 없다는 말을 자주 듣는다. 그럴듯한 거짓말을 하고, 코드를 대충 짜며, 결국 사람이 전부 검수해야 한다는 얘기다.

AI의 결과물을 사람이 검수해야 하는 것은 맞다. 그런데 사람이 만든 결과물도 다른 사람이 검수한다. 코드 리뷰를 하고 QA를 하며, 장애가 나면 회고한다. AI가 없던 시절의 소프트웨어도 버그투성이였고 잘못된 아키텍처 때문에 끊임없이 리팩터링했다.

AI만 특별히 불완전한 것이 아니다. 인간도 불완전하다.

그렇다면 질문은 “AI를 믿을 수 있는가?”가 아니라 “누가 만들었든 잘못될 수 있는 결과물을 어떻게 안전하게 다룰 것인가?”여야 한다.

AI 시대에는 이 질문이 더 중요해졌다. AI가 짧은 시간에 훨씬 많은 기능과 변경을 만들어내면서 사람이 모든 상호작용을 직접 확인하기 어려워졌기 때문이다. 반대로 AI는 코드를 읽고 테스트를 설계하며 반복적인 fixture와 경계값을 작성하는 데도 능숙하다.

AI는 테스트의 필요성을 키우면서 작성 비용은 낮췄다. TDD와 CI의 가성비가 양쪽에서 좋아진 셈이다.

테스트는 실행 가능한 의사소통이다

혼자 무언가를 만들 때는 머릿속의 직관을 바로 구현할 수 있다. 하지만 다른 사람에게 넘기려면 직관을 말이나 글로 바꿔야 한다. 그 과정에서 정보가 사라지고, 듣는 사람은 자신의 경험으로 빈칸을 채우고 오해가 생기기도 한다.

인간이 AI에게 지시할 때도 마찬가지다. 프롬프트를 아무리 잘 써도 지시자의 직관을 그대로 복제할 수는 없다. 이것은 AI만의 문제가 아니라 의사소통 자체의 한계다.

“이 기능을 잘 만들어줘”라는 말에는 해석의 여지가 많다. 반면 다음 조건은 훨씬 좁다.

  • 골드가 일정 값 이상 쌓이면 특정 시스템이 해금된다.
  • 튜토리얼 중에는 지정된 버튼만 누를 수 있다.
  • 메뉴 구성이 바뀌어도 다음 튜토리얼 버튼은 유효해야 한다.
  • 같은 보상은 한 번만 지급되어야 한다.

테스트는 이런 조건을 실행 가능한 형태로 바꾼다. 구현을 맡은 사람이 인간인지 AI인지는 중요하지 않다. 같은 입력에서 무엇이 나와야 하고, 무엇이 깨지면 실패인지 합의할 수 있다.

내가 생각하는 TDD의 가장 큰 가치는 여기에 있다. 테스트를 무조건 먼저 써야 한다는 의식보다, 구현 전에 실패 조건과 완료 조건을 명시하는 것이다. 자연어로 다 전달하지 못한 의도를 관측 가능한 계약으로 남긴다.

이 계약이 있으면 내부 구현은 과감하게 바꿀 수 있다. 테스트는 소프트웨어가 완벽하다는 것을 증명하지 않지만, 리팩터링이 우리가 지키기로 한 동작을 깨뜨렸는지는 알려준다.

버그를 없애는 것이 아니라 빨리 고치는 구조

최근 내가 만들고 있는 Open Field Framework, 줄여서 OFF의 CI를 정리하면서 상징적인 일을 겪었다.

신규 클라이언트에게 모든 엔티티의 스폰 메시지가 정확히 두 번씩 전달되는 문제가 있었다. 좀비 5개와 생존자 3개가 있는 셀이라면 8개가 아니라 16개의 메시지가 도착했다. 서로 다른 두 경로가 같은 월드 스냅샷을 보내면서도 이미 무엇을 보냈는지 공유하지 않는 것이 원인이었다.

두 경로가 상태를 공유해 스폰을 정확히 한 번만 보내도록 고치고, 그 동작을 회귀 테스트로 고정했다. 그런데 다음 Nightly에서는 다른 테스트가 실패했다. 테스트 코드가 네트워크 연결을 완료한 뒤 메시지 이벤트를 구독하고 있어, 연결과 구독 사이에 도착한 첫 메시지를 놓치는 경쟁 조건이 있었다. 이전에는 메시지가 두 번 왔기 때문에 첫 번째를 놓쳐도 두 번째를 받았던 것이다. 하나의 버그가 다른 버그를 숨기고 있었다.

Nightly 결과는 86/86에서 85/1로 바뀌었다. 기존 순서에 1초 지연을 넣어 같은 실패를 재현하고, 구독을 연결보다 앞으로 옮겨 수정했다. 실패 뒤 수정안을 작성하기까지 약 42분, Nightly가 다시 완전히 초록이 되기까지 약 1시간 23분이 걸렸다.

이 사례에서 중요한 것은 단순히 빨리 고쳤다는 사실이 아니다. 하나의 버그를 바로잡자 그 버그가 가리고 있던 다른 결함이 드러났고, 같은 CI가 이를 별개의 실패로 다시 포착했다. 내가 신뢰하는 것은 “고쳤다”는 보고가 아니라 이 반복 가능한 과정이다.

RED는 사람과 AI가 공유하는 문제 정의다

Tidemark에서는 다른 셀의 ghost 데이터가 실제 소유 셀의 값을 덮어쓰는 문제가 있었다. 바로 수정하지 않고 먼저 “다른 셀은 기존 비콘과 유닛의 소유 상태를 바꾸면 안 된다”는 실패 테스트를 추가했다. 정상적인 갱신은 통과했기 때문에 문제는 전체 동기화가 아니라 특정 갱신 경로로 좁혀졌다. 실제 데이터를 계측해 원인을 확인한 뒤 ghost를 제외하도록 고쳤고, RED부터 GREEN까지 약 42분이 걸렸다.

이 과정은 AI와 함께 진행했다. 여기서 AI는 정답을 내놓는 신탁이 아니라, 실패를 재현하고 가설을 좁히는 빠른 작업자였다. RED 테스트는 인간과 AI가 같은 문제를 보고 있는지 확인하는 공통 언어였다.

게임에서도 테스트할 수 있다

주변 게임 개발자들에게 TDD나 CI 이야기를 하면 종종 “게임에서는 안 된다”는 답이 돌아온다. 게임은 시각적 결과가 중요하고 기획 변경이 잦다. 재미나 타격감, 액션 게임의 모든 전투 상황을 자동 테스트로 보장할 수도 없다. 하지만 어렵다는 것과 불가능하다는 것은 다르다.

게임의 튜토리얼은 특정 버튼을 누르게 하고, 연출과 보상을 거쳐 다음 메뉴와 씬을 연다. 그러다 예상보다 많은 골드를 얻어 다른 시스템이 먼저 해금되고, 새 메뉴가 튜토리얼 버튼을 밀어낼 수 있다. 튜토리얼 상태와 해금 조건을 관측할 수 있게 만들고 입력과 시간, 보상 지급을 제어할 수 있다면 이런 흐름은 충분히 테스트할 수 있다.

전투의 재미는 자동 테스트가 판단하지 못해도 “이 스킬은 최대 N명을 타격한다”, “먼 대상은 피해가 감소한다”, “같은 대상에게 중복 피해를 주지 않는다”는 규칙은 테스트할 수 있다. 테스트하기 어렵다면 게임이 특별해서가 아니라 UI와 상태와 연출이 테스트를 고려하지 않은 채 엉켜 있기 때문일 수 있다. 게임은 테스트할 수 없을 만큼 특별한 소프트웨어가 아니다.

AI가 테스트의 경제성을 바꿨다

테스트가 가능하다고 해서 전부 PR마다 실행할 필요는 없다. 빠른 단위·계약 테스트는 PR마다, 멀티프로세스 통합 테스트와 E2E는 Nightly로, 부하와 성능 회귀 테스트는 정해진 시간이나 릴리스 후보에서 실행할 수 있다. GPU나 실제 브라우저가 필요하면 전용 runner로 분리하면 된다.

테스트가 동작 계약이라면 CI는 모든 변경을 그 계약과 반복해서 대조하는 장치다. 중요한 것은 테스트의 수가 아니라 실행 계획이다. 비싼 테스트를 언제 어디서 돌릴지, 실패와 중단과 미실행을 어떻게 구분할지를 설계해야 한다. 팀의 예산과 제품 성격이 다르므로 하나의 정답은 없다.

내 프로젝트들에서는 감으로 전체 노력의 약 15%를 테스트와 CI에 쓴다. 적지 않은 비용이지만 AI는 이 계산을 크게 바꿨다.

한쪽에서는 테스트의 필요성이 커졌다. AI가 하루에 만들어내는 변경량은 사람이 모든 코드를 직접 작성하던 때보다 훨씬 많다. 기능 개발은 빨라졌지만 변경 표면도 넓어졌고, 사람의 수동 검수는 쉽게 병목이 된다.

다른 쪽에서는 테스트 비용이 낮아졌다. AI는 기존 코드를 읽고 테스트 후보를 찾거나, 반복적인 fixture와 경계값을 작성하고, 실패 로그와 커밋 이력을 대조하는 일을 잘한다. 과거에는 기능 하나만 만들기도 바빠 미뤘던 테스트를 이제는 비교적 적은 추가 비용으로 함께 만들 수 있다.

TDD는 AI와 특히 잘 맞는다. 먼저 실패하는 테스트를 주면 AI가 해결해야 할 목표와 완료 조건이 동시에 생긴다. 긴 자연어 지시만 반복하는 것보다 RED를 GREEN으로 만들도록 하는 편이 훨씬 명확하다.

물론 AI가 작성한 테스트도 틀릴 수 있다. 같은 오해를 구현과 테스트 양쪽에 복사할 수도 있다. 무엇을 지켜야 하는지는 여전히 사람이 판단해야 한다. 다만 그 판단을 실행 가능한 스펙으로 옮기는 비용이 크게 낮아진 것이다.

나는 기능 구현뿐 아니라 현상 파악과 회귀 수정에도 AI를 활용한다. 사람이 모든 변경의 맥락을 직접 따라갈 수 없다는 전제 아래, 실행된 테스트와 실패·미실행 상태, 수정 후 재검증 결과가 남도록 한다. AI가 더 많은 일을 맡을수록 개인의 기억보다 시스템이 실패를 드러내는 방식으로 품질을 보장해야 한다.

믿을 것은 현상뿐이다

AI를 믿을 수 있느냐는 질문은 처음부터 잘못됐다.

사람의 의견도, 사람이 작성한 코드도, AI가 생성한 결과도 그 자체로 믿을 이유는 없다. 누가 만들었는지, 얼마나 자신 있게 설명하는지는 소프트웨어가 실제로 동작하는지와 별개다.

믿을 수 있는 것은 정해진 조건에서 실제로 어떤 현상이 반복되는가뿐이다. 수정 전에는 같은 테스트가 실패하고 수정 후에는 통과하는지, 내부를 리팩터링해도 같은 입력에 같은 동작이 유지되는지를 관측할 수 있어야 한다.

테스트가 진리를 증명하는 것은 아니다. 테스트도 틀릴 수 있고 모든 경우를 담을 수도 없다. 그래도 “잘 동작한다”는 주장만 남기는 대신, 그 주장을 반복해서 확인할 수 있는 현상으로 바꾼다. CI는 변경이 생길 때마다 그 현상을 다시 관측한다.

AI가 더 많은 코드를 만들수록 검증해야 할 주장도 많아진다. 다행히 AI는 그 검증 장치를 만드는 비용도 낮췄다. AI 시대의 TDD와 CI는 AI를 믿기 위한 장치가 아니다. 애초에 누구도 믿지 않고 개발하기 위한 장치다.

누가 만들었느냐보다 무엇이 반복해서 관측되는가. 내가 믿는 것은 사람도 AI도 아닌 현상이다.

Related Posts

View All Posts »
self-hosted CI에 Claude Code 구독 물리기 — PR 자동 분석·테스트 파이프라인

self-hosted CI에 Claude Code 구독 물리기 — PR 자동 분석·테스트 파이프라인

API가 아닌 Claude Code 구독을 self-hosted runner에 물려, CI가 돌 때마다 테스트 자동화와 PR 변경 분석을 수행하는 파이프라인. AI와 함께 코딩하니 테스트가 늘고, 테스트가 늘으니 CI가 필요해진 흐름을 정리했다.

#Aethelgard #AI #Claude #GameDev

엔진과 게임을 함께 개발하는 구조 — 승격 프로토콜, 파사드, 짝 worktree

게임 splanet.io는 직접 만든 엔진 OFF 위에 있다. 레포는 둘인데 변경은 대개 하나다. 무엇을 엔진에 올릴지는 규칙이 아니라 감각이지만, 어떻게 올릴지는 프로토콜이 있다. 파사드로 경계를 좁히고, worktree를 짝으로 움직이고, 어느 코딩 에이전트가 와도 같은 계약에서 출발하게 만든다.

#OFF #GameDev #Architecture #Git

.NET 서버 메모리 누수 잡기 — dotnet-gcdump로 안 보이는 할당 폭탄

.NET 서버 메모리가 2분 만에 136MB에서 14GB로 폭증했다. dotnet-gcdump와 dotnet-counters로도 안 잡히는 할당 폭탄을 추적하고, 그 과정을 재사용 가능한 스킬로 만든 이야기.

#Aethelgard #AI #Claude #GameDev