본문으로 건너뛰기

© 2026 Molayo

Zenn헤드라인2026. 06. 23. 21:20

AI에게 개발과 테스트를 모두 맡겼더니 테스트는 올 그린(All Green). 하지만 앱은 실행되지 않았다.

요약

AI 에이전트에게 구현과 테스트를 동시에 맡길 때 발생하는 품질 저하 문제를 해결하기 위한 실험적 접근을 다룹니다. 구현에 맞춰 테스트를 작성하는 AI의 특성을 극복하기 위해 에이전트를 격리하고, 사양(Specification) 기반의 검증 루프를 구축하는 방법을 제안합니다.

핵심 포인트

  • 구현과 테스트를 동일 에이전트에게 맡기면 테스트가 버그를 복제하는 문제가 발생함
  • 구조 커버리지(Structural Coverage)와 사양 커버리지(Specification Coverage)를 구분하여 측정해야 함
  • 기계 판독이 가능한 YAML 형식의 Given-When-Then 사양을 통해 인수 테스트 자동화를 지향함
  • 에이전트 간의 협조가 아닌 격리를 통해 테스트의 객관성을 확보하는 전략을 사용함

1. 시도한 것

로컬 LLM과 클라우드 LLM을 사용하여 테스트 자동화 환경을 구축하고 있다. 구현과 단위 테스트(Unit Test)는 로컬에서, 인수 테스트(Acceptance Test)는 클라우드에서 수행하는 구성이다. 이번에는 그 시도로서 테스트의 품질을 어떻게 담보할 것인지를 정리했다. 본고는 그 기록이다.

1.1 왜 품질 담보가 필요한가

구현과 테스트를 동일한 에이전트에게 작성하게 하면, 전건 패스(All Pass)를 신뢰할 수 없게 된다. AI는 테스트를 구현에 맞추기 때문이다. 구현의 버그까지 그대로 복제한 테스트는 당연히 그린(Green) 상태가 된다. 이는 이해 상충이다.

따라서 외부의 기계적인 체크를 수행할 필요가 있다.

1.2 선행 기사 정리

AI에게 테스트를 작성하게 하는 이야기는 이미 몇 가지 나와 있다. 읽은 것들을 나열한다.

기사취급남겨진 과제
D-MemFS 개발 전기C0에서 97%, 나머지 3%는 방어 코드로 간주C0 수준에 머무름
...

세 편에 공통되는 점이 두 가지 있다.

구조 측면에 치우쳐 있음: 모두 화이트박스(White-box, 코드를 보고 작성하는 테스트)에 관한 이야기다. 사양 측면(Black-box)의 망라성을 기계로 측정하는 단계까지는 나아가지 못했다. -
결국 인간이 멈춤: 커버리지를 인간의 리뷰 재료로 사용한다. 루프를 닫고 기계의 정지 조건으로 삼은 사례는 없다.

즉, 사양 측면의 테스트를 기계로 검증하고 인간을 거치지 않고 루프를 닫는다. 이 부분이 미개척 상태로 남아 있다. 이번에 확인하고 싶었던 것이 바로 이것이다.

다만, 사양 주도 개발(SDD)이라는 용어가 나오고 있듯이 사양을 인수 게이트(Acceptance Gate)로 삼으려는 시도도 있다.

두 가지를 모두 도입하면 가장 좋지 않을까? 생각했으나, 이미 선행 연구가 있었다.

학술 측면에서는 CANDOR(arXiv:2506.02943)가 있다. 여러 LLM에 테스트와 기대 결과를 분담하여 생성하게 하고, 서로 대조하여 정밀도를 높이는 수법이다. 뮤테이션 스코어(Mutation Score)도 평가에 사용하고 있다. 다만 벤치마크상의 검증이며, 복수 에이전트를 협조시키는 방향의 연구다. 본고는 반대로 에이전트를 격리하는 방향으로 간다.

1.3 테스트 품질을 무엇으로 측정할 것인가

측정할 평가 축을 세 가지 준비했다. 강도가 다르다.

평가 축무엇을 나타내는가강도
구조 커버리지 (Structural Coverage)코드를 통과했는가약함. assert가 비어 있어도 100%가 됨
...
커버리지가 측정하는 것은 "어디를 통과했는가"이고, 뮤테이션(Mutation)이 측정하는 것은 "통과한 지점에서 무엇을 확인하고 있는가"이다. 이번에는 이 두 가지를 나누어 사용한다.

또한, 구조 커버리지와 사양 커버리지(Specification Coverage)는 별개다. 전자는 분모가 코드이고, 후자는 분모가 요구 사양이다.

1.4 사양 작성 방법

이 루프에서는 사양 작성 방법을 바꾸고 있다. 설계로 옮기기 위한 기존의 사양서가 아니라, 인수 테스트로 그대로 변환할 수 있는 형식으로 작성한다.

각 요구사항을 Given-When-Then으로 분해하고 ID를 부여한다. 1요구사항 = 1시나리오로 고정한다. 형식은 기계 판독이 가능한 YAML로 하였다.

# auth.yaml (1파일 1기능)
feature: 인증·계정 잠금
requirements:
...

이 형식에는 세 가지 의도가 있다.

인간이 읽을 수 있음: given과 then으로 조건과 결과가 나누어져 있다. 대화를 통해 만들 때 육안으로 확인할 수 있다. -
테스트로 변환 가능: then을 기대값 확인으로, given을 전제 조건으로 기계적으로 떨어뜨릴 수 있다. 인수 테스트 생성이 흔들리지 않는다. -
망라성을 기계로 측정 가능: 요구 ID의 집합과 테스트에 붙인 ID의 집합을 차분하면, 검증되지 않은 요구사항이 나온다.

다만, 이 형식이 보장하는 것은 "작성한 요구사항이 빠짐없이 테스트가 된다"는 것뿐이다. "작성해야 할 요구사항이 누락되지 않았는가"는 보장하지 않는다. 요구사항의 누락은 인간이 확인해야 한다.

1.5 구성한 체계

세 역할로 나누었다.

역할담당책무
구현자로컬 LLM (Qwen3.6:27b)구현 및 단위 테스트
...
설계의 요점을 나열한다.
  • 사양을 진리로 삼는다: 1.4 YAML을 유일한 기준으로 삼는다. 테스트도 코드도 이를 기준으로 판정한다.
  • 종료 판정은 외부 계산이 담당한다: 종료 판정을 LLM에게 맡기지 않는다. 테스트의 Pass(통과) 수 개선을 통해 진척도를 측정한다.
  • 구현자와 수락 판정자는 서로의 코드를 보지 않는다: 서로의 코드를 읽으면 독립적인 검증이 아니게 된다.

구현을 담당하는 로컬 LLM이 수락 테스트를 참조하지 않도록, 프롬프트에 "수락 테스트를 참조해서는 안 된다"라고 작성했다.

여기까지가 준비다.

2. 일어난 일

개발 측의 로컬 LLM이 수락 테스트 파일을 read(읽기) 하고 있었다. 로그를 육안으로 확인했다.

구현된 core.py에는,

load_error

confirmed

is_open

과 같이, 사양에는 없지만 수락 테스트 쪽에만 등장하는 이름들이 나타나 있었다. read(읽기) 한 내용이 구현에 포함되어 있다.

결과는 전건 Green(통과)이었다. 하지만 구현은 검증되어야 할 테스트를 보고 작성되었다. 이 Green은 독립적인 검증이 아니다.

독립 검증은 실패했다...

3. 해결책

실패로부터 도출한 해결책. 아마 당연한 것이겠지만.

차단은 의지가 아니라 구조로 한다. "보지 마"라고 쓰는 것이 아니라, 볼 수 없는 상태를 만든다.

방어 방식구현강도
의지프롬프트에 "참조 금지"라고 작성약함. 존재를 알면 깨짐
구조존재를 쓰지 않음 / 별도 디렉토리 / 파일 권한깨지지 않음
  • 존재를 쓰지 않음: 구현자의 프롬프트에서 수락 테스트에 대한 언급을 삭제한다. 금지 문구도 쓰지 않는다.
  • 보이지 않는 곳에 배치: 구현 세션의 작업 디렉토리 외부에 둔다. 채점 시에만 합류시킨다.
  • 쓸 수 없게 만듦: 파일 권한으로 보호한다.

4. 그래도, 테스트는 나쁘지 않았다

독립적인 검증이 되지 않았다는 것은 알게 되었다. 그렇다면 테스트 그 자체에 버그를 잡아내는 힘이 있는가? Mutation Test(변이 테스트)를 실시했다.

구현을 일부러 수정하여 테스트가 실패하는지 확인한다. 스코어는 76%. 나머지 24%, 19건이 살아남았다. 살아남은 변이(survivor)를 하나씩 살펴보았다.

구분내용건수
equivalentencoding, indent, 에러 문구 변경 등. 동작이 변하지 않음약 12
...

equivalent(동등 변이)는 수정해도 동작이 변하지 않는 변이다. 떨어뜨리지 못하는 것이 당연하며, 스코어의 분모에서 제외해도 된다. 이를 제외하면 실질적인 스코어는 더 높다. 남은 survivor는 GUI 인접층과 삭제의 기본 인자(default argument)였다. 둘 다 수락 테스트가 의도적으로 범위 외로 설정한 부분이었다.

숫자상으로는 테스트가 나쁘지 않다. 사양 커버리지(Specification Coverage)는 Green이다. 요구사항 ID는 모두 테스트에 연결되어 있다. Mutation(변이) 또한 equivalent를 제외하면 대부분 잡아낸다. 기계적인 지표로 보면 문제는 없어 보인다.

5. 실행해 보았다

앱을 실행하려고 했다.

하지만, 아무 일도 일어나지 않는다.

어라, 이상하네.

소스 코드를 살펴보니...

main 함수가 구현되어 있지 않았다. 클래스 정의만 나열되어 있을 뿐이다. Tkinter는 import조차 되어 있지 않다. GUI는 실행되지 않는 것이 아니라, 존재하지 않았다.

구현된 것은 수락 테스트가 검증하는 범위뿐이었다. 테스트는 Headless(화면 없는) 컨트롤러를 본다. 그래서 컨트롤러는 존재한다. 테스트는 화면을 보지 않는다. 그래서 화면은 없다.

전건 Green. Mutation 76%. main은 없다.

기계적인 지표는 모두 "작성된 코드"를 측정한다. 작성되지 않은 코드는 어떤 지표에도 나타나지 않는다. 아무도 "앱이 실행된다"를 요구사항에 쓰지 않았다.

6. 다음에 할 일

격리를 구조로 다시 설계하여 실행한다. 그 검증은 별도 기사로 다룬다.

Discussion

AI 자동 생성 콘텐츠

본 콘텐츠는 Zenn AI의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0