AI 시대의 테스트 — 바뀐 실태와, 바뀌지 않는 질문
GitHub Copilot・Claude・Cursor 는 테스트의 초안을 빠르게 생성할 수 있게 되었다. 하지만 테스트 자동화의 근본 문제인 oracle 문제는 AI 로도 풀리지 않았고, 「구현 추인 테스트」 라는 새로운 위험도 실증되었다. 소프트웨어 테스트의 계보 시리즈 마지막 기사, 60 년 된 질문의 행방을 좇는다.
대규모 언어 모델 (LLM) 이 코드 생성에 쓰이게 되면서, 테스트 코드의 생성・자동 수복・평가에도 LLM 이 응용되기 시작했다. GitHub Copilot・Claude (Claude Code 등)・Cursor 같은 도구는 실무에서의 채택도 진행되고 있다. 한편 이 분야의 학술 연구와 실무 보고는, 기대만큼이나 많은 한계도 밝혀왔다. 테스트 자동화의 본질적인 어려움은 「무엇이 올바른 결과인지를 누가・어떻게 판단하는가」 라는 oracle 문제 에 있으며, 이것은 LLM 등장 이전부터 존재했던 문제로, LLM 에 의해 해소된 것은 아니다. 이 시리즈의 마지막 기사로서, 과장을 피하고 현시점에 알려진 실태와 한계를 정리한다.
LLM 에 의한 테스트 생성의 실태
Copilot・Claude・Cursor 같은 코딩 어시스턴트는, 대상 함수나 주석・타입 정보를 맥락으로 읽어들여 테스트 케이스의 골격・대표적인 입력값・어서션의 형태를 생성할 수 있다. 실무상의 가치가 확인된 것은, 기존 패턴과 비슷한 정형 테스트의 초안 작성, 개발자가 놓치기 쉬운 빈 문자열・null・경계값의 상기 지원, 그리고 테스트가 존재하지 않는 레거시 코드에 대한 「현재 동작을 기록하는 테스트」 의 초안 생성이다.
한편 학술적인 평가 연구에서는, 생성된 테스트가 컴파일 에러를 포함한다, 가치가 낮은 자명한 어서션에 그친다, 리포지토리 고유의 명명 규칙이나 헬퍼 함수를 충분히 반영하지 못한다는 등의 과제가 반복적으로 보고되고 있다. 생성 정밀도는 대상 코드에 주어지는 맥락 정보 (타입 시그니처・기존 테스트의 예・코드 주석) 에 얼마나 좌우되는지가 여러 연구에서 공통적으로 지적되고 있다.
「자기 수복」 이라는 말의 과장과 실태
자기 수복 (self-healing) 은 AI 시대의 테스트 분야에서 하이프와 실태의 괴리가 가장 큰 용어 중 하나다. 다만 실용화된 기술도 존재한다. Mabl・Testim・Functionize 같은 상용 E2E 테스트 자동화 도구는, UI 요소를 특정하는 「로케이터」 가 DOM 구조의 변경으로 작동하지 않게 되었을 때, 요소의 속성・시각적 위치・주변 DOM 구조 등 여러 신호를 머신러닝 모델로 대조하여 가장 일치도가 높은 대체 요소를 다시 선택하는 auto-healing 기능을 제공한다. 이것은 이전 기사 플레이키 테스트와의 싸움 에서 다룬 E2E 테스트의 구조적 취약성에 대한, 제한적이지만 실용화된 대처다.
한편 「테스트가 실패했을 때 AI 가 테스트 코드 자체를 자율적으로 다시 써서 수정한다」 는, 더 넓은 의미의 자기 수복은 아직 연구 단계를 벗어나지 못했다. 상용 도구가 실제로 제공하는 것은 대부분 로케이터의 자동 조정 같은 제한된 범위에 머문다.
oracle 문제 — AI 로도 풀리지 않는 근본 문제
oracle 문제 (test oracle problem) 란, 「어떤 프로그램의 출력이 올바른지 아닌지를 어떻게 판정할 것인가」 라는, 테스트 자동화의 근본적인 난제다. 단순한 입출력의 등가 비교로 충분한 경우는 문제가 되지 않지만, 「올바른 출력」 자체를 정의하기 어려운 문제 (자연어 생성・이미지 인식・추천 시스템 등), 기대되는 출력을 사람이 준비하는 비용이 비현실적인 경우, 복수의 타당한 출력이 존재할 수 있는 경우에 심각해진다.
LLM 은 테스트의 입력 후보 (경계값・이상계의 패턴) 를 제안하는 지원에는 유효하지만, 「무엇이 올바른 출력인가」 를 결정하는 권위 (oracle) 를 대체하는 것은 아니다. LLM 에게 「이 출력은 올바른가」 라고 묻는 것 자체는 가능하지만, 그것은 다른 LLM 호출에 판정을 위임하고 있을 뿐이며, 판정의 정당성을 담보하는 근거가 되지는 않는다. oracle 문제는 LLM 등장 이전부터 중심적인 미해결 과제였으며, AI 도입으로 해소된 것이 아니라 「판정을 누가・어떻게 할 것인가」 라는 질문의 형태를 바꾸어 이어지고 있다 고 이해하는 것이 실태에 가깝다.
| oracle 을 정의하기 쉬운 영역 | oracle 이 어려운 영역 |
|---|---|
| 수치 계산・정렬・파서 등, 입출력의 등가 비교로 충분한 처리 | 자연어 생성・요약・대화 응답 |
| 알려진 입출력 예가 풍부한 처리 | 이미지 인식・추천 시스템 등 「정답」 이 확률적・주관적인 처리 |
| 기대값을 사양으로부터 기계적으로 도출할 수 있는 처리 | 복수의 타당한 출력이 있을 수 있는 처리 (같은 의미를 다른 표현으로 반환하는 LLM 의 출력 등) |
전자는 고전적인 단위 테스트가 그대로 기능하는 영역이며, 후자야말로 LLM 을 내장한 시스템 자체의 평가 (후술할 eval) 에서 항상 문제가 되는 영역이다.
프로퍼티・불변조건의 AI 추론
이전 기사에서 다룬 프로퍼티 기반 테스트 는, 개별 입출력 예가 아니라 「항상 성립해야 할 성질」 을 검증하는 기법이다. LLM 을 이 불변조건의 후보 추출에 사용하는 시도도 연구 수준에서 존재하며, 코드나 문서로부터 「이 함수는 결과가 입력보다 항상 커야 한다」 같은 성질의 후보를 LLM 에게 제안하게 하고, 사람이 그것을 precondition/postcondition 으로 채택할지 판단하는, 지원적인 활용이 모색되고 있다. 이것도 「LLM 이 제안한 성질이 정말로 도메인상 올바른가」 를 최종 판단하는 것은 사람이며, oracle 문제를 회피하고 있는 것은 아니다.
AI 가 생성한 코드의 테스트 전략
AI 가 생성한 코드 자체를 어떻게 테스트할 것인가에 대해서는, 「사람이 쓴 코드와 본질적인 차이는 없다」 는 입장이 널리 공유되고 있다. 오히려 다음 점이 실무상 중요해진다 —— 생성된 코드의 의도를 리뷰 없이 대량으로 받아들이면, 그 의도를 테스트만으로 역산해야 하는 상황이 생겨 테스트 설계 자체가 어려워진다는 것. 구현과 테스트를 동일한 프롬프트・동일한 모델 호출로 생성하면, 구현의 버그와 테스트의 잘못된 어서션이 상관관계를 가지고 생성되기 쉬워 독립적인 검증이 되지 않을 가능성이 있다는 것. 그리고 AI 가 구현을 제안하는 경우라도 기대되는 동작을 정의하는 주체는 사람인 편이, oracle 문제에 대한 대처로서 건전하다는 것이다.
AI 시스템 자체의 테스트 — eval, LLM-as-judge, 비결정성
LLM 을 내장한 시스템 (챗봇・에이전트・요약이나 분류 파이프라인) 자체를 평가하는 테스트는, 종래의 결정적인 단위 테스트와는 성질이 다르다. 이 영역에서는 「테스트」 가 아니라 eval (평가) 이라는 말이 쓰이는 경우가 많다. LLM-as-judge 는 LLM 의 출력의 좋고 나쁨을, 다른 LLM 호출에 판정시키는 기법으로, 사람에 의한 평가 비용을 낮출 수 있는 한편, 판정자 쪽의 LLM 에도 편향과 불안정성이 있어 판정 자체의 신뢰성을 검증할 필요가 있다.
LLM 의 출력은 온도 (temperature) 를 0 으로 설정해도 완전히 결정적이 되지는 않는다는 것이 실측 연구로 밝혀져 있다. 추론 기반의 배치 처리나 하드웨어 최적화에서 기인하는 비결정성이 남기 때문으로, 이것은 「한 번의 테스트 실행으로 합격 여부를 단정할 수 없다」 는, 종래 테스트의 전제를 무너뜨리는 성질이다. 실무적인 대응책으로는, 동일한 프롬프트를 여러 번 (기준으로 3〜10 회 정도) 실행하여 평균・분산을 보고한다, 완전 일치가 아니라 의미적인 등가성 (스키마 준수・지시 추종・구조의 정확성) 을 검증한다, 모델 버전・샘플링 파라미터・평가자 버전을 기록하여 재현성을 확보한다 등의 방법이 확산되고 있다. 이것은 이전 기사의 「비결정성」 이라는 개념이, LLM 을 포함하는 시스템에서는 우발적인 버그가 아니라 시스템의 본질적인 성질 로 나타난다는 것을 의미한다.
위험 — 환각・과신・커버리지의 착각・구현 추인 테스트
LLM 에 의한 테스트 생성에는, 현장에서 확인되고 있는 몇 가지 구체적인 위험이 있다. 존재하지 않는 API 나 잘못된 타입 시그니처, 컴파일조차 되지 않는 코드를 생성해 버리는 환각적인 테스트. 「AI 가 생성했으니 옳을 것이다」 는 생각이 리뷰를 생략하게 만드는 과신. 커버리지 수치를 올리는 것 자체를 목적화하여 테스트를 대량 생성하면, 커버리지율은 향상되어도 버그 검출력이 따르지 않는 테스트가 쌓이는 커버리지의 착각.
그리고 가장 심각한 것이 구현 추인 테스트 (implementation-confirming tests) 다. 이것은 LLM 이 테스트를 생성할 때, 「마땅히 그래야 할 올바른 동작」 이 아니라 「지금의 구현이 실제로 반환하는 값」 으로부터 어서션을 만들어 버리는 현상으로, 버그를 포함한 코드에 대해 특히 심각한 결과를 낳는다. GitHub Copilot・CodiumAI CoverAgent・CoverUp 이라는 3 개의 테스트 생성 도구를 평가한 한 연구에서는, 최종적인 테스트 스위트의 최대 68.1% 가 「버그를 포함한 잘못된 구현에서는 통과하고, 올바른 구현에서는 오히려 실패한다」 —— 즉 버그 그 자체를 “정답” 으로 고정해 버리는 테스트가 되어 있었다고 보고되었다. 커버리지 수치를 올리는 것을 목적으로 한 설계 선택이, 버그의 검출이 아니라 버그의 추인을 낳아버리는 전형적인 사례로 인용된다.
사람의 리뷰라는 불가결한 공정
이상을 근거로, 현시점에 널리 받아들여지고 있는 현실적인 입장은 다음과 같다. LLM 은 테스트의 초안 생성・입력 후보의 상기・정형 작업의 고속화에서 실무적인 가치를 가진다. LLM 은 「무엇이 올바른 동작인가」 를 결정하는 권위를 대체할 수 없다 —— 이것은 기술적 제약이라기보다, oracle 문제 그 자체가 본질적으로 사람의 도메인 지식・사양 이해를 요구하는 문제이기 때문이다. 생성된 테스트는, 통상의 풀 리퀘스트의 코드와 마찬가지로 사람에 의한 리뷰를 거쳐야 하며, 특히 「어서션이 구현의 현재 상태를 그대로 따라가고 있는 것은 아닌가」 (구현 추인 테스트) 를 확인하는 시점이 중요해진다. 테스트가 검증해야 할 것은 「사양대로의 동작」 이지 「현재 코드의 동작」 이 아니라는 원칙은, AI 시대에도 변하지 않는다.
실제 세계의 60 년, 시리즈의 18 개 기사
소프트웨어 테스트가 실제 세계에서 걸어온 길을 압축하면 다음과 같다.
| 연대 | 한마디로 |
|---|---|
| 〜1970 년대 | 수동 확인・디버깅의 연장. 아직 「테스트」 는 독립된 공정이 아니었다 |
| 1990 년대 중반 | SUnit 이 픽스처・테스트 러너라는 xUnit 계열의 원형 패턴을 확립 |
| 1997-2002 년 | JUnit 의 보급과 TDD 의 체계화로, 테스트가 「설계를 이끄는 행위」 가 된다 |
| 2000 년대 후반 | BDD・Cucumber・xUnit Test Patterns 로 테스트의 어휘와 기법이 결정화 |
| 2010 년대 전반 | JS/프론트엔드용 프레임워크 (Jasmine → Jest) 가 폭발적으로 보급 |
| 2010 년대 후반 | Selenium 일강 체제에서 Cypress・Puppeteer 로, E2E 도구의 세대교체 |
| 2020 년대 | Playwright 에 의한 멀티 브라우저 통일과, LLM 에 의한 테스트 생성의 확산 |
계보를 돌아보며 — 18 개 기사를 관통한 질문
이 시리즈는 위의 연표를 18 개의 기사로 나누어 좇아왔다.
| 기사 | 한마디로 |
|---|---|
| 1. 전사 | 수동 확인의 시대, Dijkstra 의 경구, V&V 라는 고전적 구분 |
| 2. xUnit 의 탄생 | SUnit/JUnit 이 픽스처・테스트 러너를 발명 |
| 3. xUnit 의 보급 | xUnit 계열의 패턴이 각 언어의 생태계로 확산 |
| 4. TDD | Red-Green-Refactor, 테스트가 설계를 이끈다 |
| 5. BDD | 「행동」 이라는 말로, 테스트의 심리적 문턱을 낮춘다 |
| 6. 목과 테스트 더블 | 실체를 교체하여 의존을 분리하는 기법의 체계화 |
| 7. 프로퍼티 기반 테스트 | 개별 예가 아니라 「항상 성립하는 성질」 을 검증한다 |
| 8. E2E 의 세대교체 | Selenium 에서 Cypress・Playwright 로 |
| 9. JavaScript 테스트 | 프론트엔드 테스트의 폭발적 보급 |
| 10. 언어별 생태계 | 언어 커뮤니티마다 다른 테스트 문화 |
| 11. 테스트의 형태 | 테스트 피라미드 등, 배분의 이론 |
| 12. 단위 테스트 설계와 계약 | 좋은 단위 테스트의 조건, 계약에 의한 설계 |
| 13. 커버리지와 뮤테이션 | 「충분히 테스트했다」 를 어떻게 측정하는가, 측정하는 것의 한계 |
| 14. 플레이키 테스트 | 비결정성이라는, 테스트의 신뢰성을 갉아먹는 문제 |
| 15. 형식 기법 | 테스트가 닿지 않는 영역을, 수학적 증명으로 보완하려는 시도 |
| 16. 품질은 누구의 일인가 | 테스트가 특정 직능에서, 팀 전체의 관심사로 |
| 17. 대규모 개발의 테스트 | 수백만 건 규모의 테스트를 지탱하는 실행 기반과 설계 |
| 18. AI 시대의 테스트 (이 기사) | 생성・실행의 도구가 바뀌어도, 검증이라는 행위의 근본 문제는 바뀌지 않는다 |
테스트는 무엇을 보증할 수 있는가 — 도구는 바뀌어도 질문은 바뀌지 않는다
이 시리즈의 첫 번째 기사 테스트의 전사 에서 본 대로, Edsger Dijkstra 는 1969 년 NATO 소프트웨어 공학 회의에서의 발표를 거쳐, 1970 년의 논문 「Notes on Structured Programming」 (EWD249) 에서 이렇게 썼다.
“Program testing can be used to show the presence of bugs, but never to show their absence!”
프로그램 테스트는 버그의 존재를 보여주는 데는 쓸 수 있지만, 버그의 부재를 보여주는 것은 결코 할 수 없다. Glenford Myers 가 1979 년에 정의한 「테스트란 프로그램을 파괴하기 위한 파괴적 프로세스다」 라는 관점도, Barry Boehm 의 V&V —— 「올바르게 만들고 있는가 (검증)」 와 「올바른 것을 만들고 있는가 (타당성 확인)」 의 구분도, 반세기가 넘게 지난 지금도 변함없이 현역인 시각으로 남아 있다.
LLM 은 테스트를 쓰는 속도를 높이고, 놓치기 쉬운 입력을 상기시키며, 레거시 코드로의 첫걸음을 가볍게 했다. 하지만 LLM 은 Dijkstra 가 짚은 한계를 지우지 못했다 —— 오히려 구현 추인 테스트라는 형태로, 「테스트가 초록불인 것」 과 「버그가 없는 것」 사이의 거리를, 그 어느 때보다 날카롭게 드러냈다. SUnit 에서 JUnit 으로, TDD 에서 BDD 로, 목에서 프로퍼티 기반 테스트로, Selenium 에서 Playwright 로, 그리고 수백만 건의 테스트를 지탱하는 모노레포의 기반에 이르기까지, 도구와 어휘는 60 년에 걸쳐 몇 번이고 바뀌었다. 그 사이 계속 물어온 것은 단 하나 —— 테스트로 무엇을 보증할 수 있는가 라는 질문이다.
도구가 사람에서 AI 로 바뀌어도, 이 질문에 답할 책임만은 여전히 사람의 몫이다.
독자에게
이 시리즈를 완독한 독자에게는, 다음과 같은 1 차 자료를 직접 접해볼 것을 권하고 싶다. Kent Beck 『Test-Driven Development: By Example』 (2002), Michael Feathers 『Working Effectively with Legacy Code』 (2004), Gerard Meszaros 『xUnit Test Patterns: Refactoring Test Code』 (2007), Steve Freeman 과 Nat Pryce 의 『Growing Object-Oriented Software, Guided by Tests』 (2009). 그리고 Google 의 엔지니어링 문화를 다룬 『Software Engineering at Google』 은, 이 기사에서 다룬 hermetic tests 나 테스트 크기 분류의 1 차 자료이기도 하다. 반세기도 채 되지 않은 이 분야에서, 이미 「고전」 이라 부를 만한 위치를 차지하고 있다.
소프트웨어 테스트의 계보 시리즈는, 여기서 완결된다. Dijkstra 의 경구에서 시작해 AI 가 테스트를 쓰는 시대까지 —— 「작동하는지 확인한다」 는 소박한 행위 안쪽에, 이만큼 풍부한 질문의 역사가 있었다는 것을, 이 18 개의 기사가 전할 수 있었다면 다행이다.