AI 코딩 에이전트에게 테스트를 시켜도 품질이 좋아지지 않는 이유

AI 코딩 에이전트 테스트가 형식에 머무는 이유와 계약 기반 검증, 독립 오라클, 위험 기반 보안 테스트를 실무 기준으로 정리한다.

AI 코딩 에이전트에게 테스트를 시켜도 품질이 좋아지지 않는 이유

AI 코딩 에이전트에게 테스트를 맡기는 팀이 늘었지만, 테스트가 실행됐다는 사실만으로 품질이 올라가지는 않는다. 초록색 체크가 많아도 틀린 기대값을 검증하고 있다면 버그는 그대로 남는다.

최근 공개된 실험은 이 불편한 장면을 꽤 구체적으로 보여준다. 에이전트에게 TDD, 퍼징, 속성 기반 테스트 같은 이름을 알려주는 것과 그 기법의 목적을 이해시켜 실제 위험을 찌르게 하는 것은 전혀 다른 작업이다. 이 글에서는 그 차이를 살펴보고, 우리 팀의 AI 코딩 에이전트 테스트 흐름에 어떤 안전장치를 붙일지 적어본다.

테스트 파이프라인을 떠올릴 때 화면에 보이는 것은 보통 테스트 수, 통과율, 커버리지다. 하지만 진짜 질문은 조금 다르다. “이 테스트가 틀린 구현을 통과시키지 않을 수 있는가?”다. 이 질문을 놓치면 테스트는 품질 장치가 아니라 작업 완료를 표시하는 체크리스트가 된다.

AI 코딩 에이전트 테스트와 독립 검증에서 숨은 결함을 찾는 흐름

테스트를 시켰다는 말과 검증이 된다는 말은 다르다

에이전트가 잘하는 일과 놓치는 일

AI 코딩 에이전트는 명시적인 작업에는 강하다. 테스트 파일을 만들고, 명령어를 실행하고, 실패한 assertion을 고치고, 커버리지 숫자를 올리는 일은 빠르게 반복한다. 문제는 그 다음이다. 무엇을 틀리면 안 되는지, 어떤 입력 조합이 실제 장애로 이어지는지, 결과를 어떤 독립된 기준으로 판정해야 하는지는 요구사항에 빈틈이 있으면 같이 흐려진다.

그래서 “퍼징을 사용해라”라는 지시는 랜덤 바이트를 잔뜩 넣으라는 뜻으로 축소될 수 있다. “형식 검증을 해라”라는 지시는 실제 구현의 위험한 경계를 증명하는 대신, 틀려도 아무 영향이 없는 추상 명제를 증명하는 일로 변할 수 있다. 기법의 이름은 지켰지만 기법이 존재하는 이유는 사라진다.

Dan Luu가 공개한 에이전트의 테스트·검증 기법 활용 실험은 이 현상을 실험 조건으로 쪼개서 본다. 한국어 요약은 GeekNews의 정리에서 볼 수 있고, 같은 글이 올라간 Hacker News 토론에는 “방법을 적용했지만 목적을 잃는다”는 관찰이 이어진다. 개인적인 인상이라기보다 반복 실행에서 자꾸 나타나는 행동 패턴에 가깝다는 뜻이다.

테스트를 많이 쓰는 에이전트가 곧 테스트를 잘 쓰는 에이전트는 아니다. 테스트의 수는 실행 비용과 리뷰 비용을 늘리지만, 실패해야 할 코드가 실패하는지는 별도의 문제다. 품질을 측정할 때 통과율을 첫 번째 지표로 놓으면 이 간극을 보기 어렵다.

26개 조건 실험이 보여준 테스트의 빈틈

Zstd와 IMAP에서 드러난 차이

실험은 Rust로 Zstd 구현을 만들게 하면서 26개 프롬프트 조건을 비교했다. 추가 지시가 없는 기본값, TDD, 퍼징, 속성 기반 테스트, 변형 테스트, 차분 테스트, QuickCheck, 여러 형식 검증 도구 등이 조건에 들어갔다. Zstd처럼 비트 조작과 압축 상태가 얽힌 문제뿐 아니라 IMAP RFC를 대상으로 한 실행도 함께 살폈다.

중요한 점은 특정 도구의 승패표가 아니다. 26개 조건에서 이름을 알려주는 것만으로는 에이전트가 그 기법의 고유한 검증력을 끌어내지 못했다는 점이다. 기본값 조건이 평균보다 잘 나온 경우도 있었고, 기법을 강하게 지정한 조건이 더 많은 테스트를 쓰면서도 숨은 테스트를 통과하는 정확도가 좋아지지 않는 경우도 있었다. 실험자는 조건 순위를 강하게 일반화하지 말라고 경고하지만, 이름만 붙인 지시의 한계는 일관되게 관찰된다.

Zstd의 네 개 스트림 점프 테이블처럼 입력 사이의 관계가 중요한 구간에서 에이전트는 그럴듯한 정상 케이스를 여러 개 만들었다. 그런데 네 스트림을 모두 같게 만들거나 단순한 패턴을 반복하면 순서를 뒤집는 버그가 있어도 테스트는 통과한다. 테스트가 기능을 호출했다는 사실은 남지만, 버그를 구별하는 입력은 사라진 셈이다.

IMAP에서도 큰 방향은 달라지지 않았다. 프로토콜 문제에서는 상태 전이, 허용되지 않은 순서, 경계 응답이 중요하다. 그렇지만 에이전트가 이름만 전달받으면 표준적인 단위 테스트를 먼저 만들고, 나중에 도구를 형식적으로 붙이는 흐름으로 기울기 쉽다. 복잡한 입력 공간이 있는 문제인지 단순한 함수인지와 상관없이, “테스트 기법을 썼다”는 기록과 “실패를 찾아냈다”는 결과 사이에 간격이 생긴다.

기법 이름만 알려줬을 때 생기는 피상적 사용

퍼징에서는 무작위 바이트가 대부분 같은 거부 경로로 들어갔다. 속성 기반 테스트에서는 확인할 속성이 하나뿐이거나, 라운드트립처럼 이미 정상 동작을 전제로 하는 쉬운 속성에 집중했다. 차분 테스트에서는 서로 다른 구현을 비교해야 하는데 사실상 같은 로직을 두 번 작성해 같은 버그를 공유하는 장면이 나왔다. 변형 테스트라고 해놓고 실제 변형을 거의 만들지 않는 경우도 있었다.

형식 도구도 마찬가지다. 구현의 버그가 있을 법한 경계를 대상으로 하지 않고, 범위를 벗어나지 않는다는 식의 당연한 명제를 증명하면 도구의 실행 흔적은 남아도 정확성은 늘지 않는다. 사람 리뷰어라면 “이 증명이 실제 장애를 어떻게 막나?”라고 물을 텐데, 에이전트는 그 질문을 스스로 끝까지 유지하지 못한다.

이 대목에서 프롬프트에 도구 목록을 더 길게 붙이는 방식은 매력적이지만 효과가 제한적이다. 에이전트가 따르는 것은 가장 쉽게 확인할 수 있는 지시일 가능성이 크다. 테스트 파일이 생겼는지, 명령어가 0으로 끝났는지는 확인하기 쉽다. 반면 테스트의 기대값이 요구사항과 독립적인지, 실패한 변형이 정말 버그를 대표하는지는 추가 오라클이 없으면 확인하기 어렵다.

TDD와 기본값 결과를 읽는 법

테스트를 먼저 쓴다고 자동으로 좋은 테스트가 되지는 않는다

실험에서 TDD 조건은 행동을 크게 바꿨다. 에이전트는 기본값보다 더 많은 테스트를 만들었고, 구현 전에 실패하는 테스트를 남기는 빈도도 높았다. 테스트-코드-테스트를 반복하는 흐름은 분명 더 선명해졌다. 그런데 Zstd의 어려운 경계 사례를 더 잘 찌른다는 보장은 없었다. 오히려 일반 케이스와 쉬운 테스트가 늘면서, 일부 오류를 잡지 못하는 구현이 나왔다.

이 결과를 TDD 자체의 실패로 읽으면 곤란하다. TDD는 요구사항의 예시를 빠르게 실행 가능한 문서로 만들고, 작은 피드백 루프를 제공하는 좋은 설계 습관이다. 다만 에이전트가 TDD를 “실패하는 테스트를 하나 만들고, 그 테스트만 통과시키는 순서”로 축소할 수 있다는 점이 문제다. 테스트가 사업 규칙이나 프로토콜 계약을 대변하지 않으면 순서를 지켜도 검증력은 낮다.

내가 보는 핵심은 테스트를 먼저 쓰느냐 나중에 쓰느냐가 아니다. 테스트를 만들기 전에 위험한 동작과 정답 판정법을 충분히 외부화했느냐에 가깝다. 사람이 코드를 읽고 난 뒤에야 보이는 경계가 있다면, 블랙박스 예시만 먼저 쌓는 흐름은 그 경계를 계속 미룰 수 있다.

기본값이 잘 나온 이유를 과하게 해석하지 않기

추가 지시가 없는 기본값이 상대적으로 좋은 결과를 냈다고 해서 “에이전트에게 맡길수록 좋다”는 뜻도 아니다. 기본값 에이전트가 우연히 문제에 맞는 일반 테스트를 만들었을 수 있고, 조건별 차이가 실행 표본의 변동일 수도 있다. 반대로 TDD나 퍼징이 항상 나쁘다는 뜻도 아니다. 숙련된 사람이 입력 분포와 판정 기준을 설계하면 같은 도구가 훨씬 다른 결과를 낸다.

이 실험에서 가져올 문장은 “기법 이름을 지시하면 품질이 오른다”가 아니라 “목적과 오라클까지 지시하지 않으면 기법이 껍데기만 남을 수 있다”다. AI 에이전트의 작업 루프를 설계할 때 실행 단계마다 완료 조건을 분리했던 것처럼, 테스트도 실행·판정·독립 검토를 서로 다른 단계로 둬야 한다.

계약 기반 테스트로 오라클을 먼저 세우기

사전조건·사후조건·정의되지 않은 동작을 적는다

좋은 대안은 테스트 코드를 바로 쓰게 하기 전에 계약을 작성하게 하는 것이다. Google Research의 계약 기반 테스트 연구는 이 흐름을 사전조건, 사후조건, 정의되지 않은 동작으로 구체화한다. 에이전트가 코드와 요구사항을 읽고 이 세 가지를 먼저 문서화한 뒤 테스트를 생성하게 했더니, 전통적인 테스트 생성 에이전트 기준으로 버그 탐지율이 9.8%p, 분기 커버리지가 2.5%p 개선됐다. arXiv의 원문 초록에서도 같은 평가 결과와 비교 기준을 확인할 수 있다.

여기서 계약은 거창한 형식 명세일 필요가 없다. 예를 들어 결제 요청 함수라면 “인증된 사용자만 호출할 수 있다”는 사전조건, “성공 응답에는 하나의 거래 ID가 있고 재시도해도 중복 결제가 생기지 않는다”는 사후조건, “결제 제공자가 타임아웃 뒤 승인 여부를 확정하지 않은 상태”를 정의되지 않은 동작 또는 별도 정책으로 적을 수 있다. 이 문장이 있어야 에이전트가 입력을 만들 때 무엇을 흔들고, 결과를 어떻게 판정할지 결정할 수 있다.

// 계약을 먼저 고정하고, 구현을 따라가는 기대값은 피한다.
it("재시도해도 같은 멱등 키는 거래를 한 번만 만든다", async () => {
  const first = await pay({ amount: 12000, idempotencyKey: "order-42" });
  const retry = await pay({ amount: 12000, idempotencyKey: "order-42" });

  expect(first.status).toBe("approved");
  expect(retry.transactionId).toBe(first.transactionId);
  expect(await ledger.count({ orderId: "order-42" })).toBe(1);
});

코드가 이미 만든 값을 복사해 assertion에 넣는 방식과 계약에서 독립적으로 도출한 기대값을 비교해 보자. 전자는 구현과 테스트가 같은 오해를 공유하기 쉽다. 후자는 실패 시 “어느 규칙을 어겼는가”를 말할 수 있다. 이 차이가 오라클의 차이다.

계약 기반 체크리스트

실무에서는 다음 질문을 이슈 템플릿이나 에이전트 작업 파일에 고정해두면 좋다.

  • 유효한 입력의 최소·최대·빈 경계는 어디인가?
  • 입력이 만족해야 할 사전조건은 무엇이며, 위반하면 어떤 오류를 반환하는가?
  • 성공 후 반드시 유지되어야 하는 상태와 불변식은 무엇인가?
  • 외부 API 지연, 중복 요청, 순서 뒤바뀜을 어떤 정책으로 다루는가?
  • 정의되지 않은 동작을 허용할지, 거부할지, 별도 경고로 남길지 누가 결정했는가?
  • 기대값을 구현 코드가 아닌 규격, 참조 구현, 불변식, 독립 계산에서 만들 수 있는가?

이 체크리스트를 통과하지 못한 테스트는 숫자를 늘리기 전에 멈춘다. 테스트를 더 생성하는 것이 아니라 판정 기준을 더 명확히 하는 쪽이 먼저다.

계약 기반 검증과 변형 테스트로 AI 에이전트의 경계 조건을 확인하는 장면

보안 테스트는 위험도와 독립성을 붙여야 한다

SecureVibeBench가 던지는 현실적인 질문

기능 테스트가 통과해도 보안 결함은 남을 수 있다. SecureVibeBench는 AI 에이전트가 실제 오픈소스 취약점이 도입되는 맥락을 재구성하도록 만든 C/C++ 보안 코딩 벤치마크다. 여러 파일을 수정해야 하는 저장소 작업과 정적·동적 오라클을 함께 사용해 기능과 보안을 같이 평가한다. 핵심 메시지는 단순하다. 코드가 컴파일되고 일반 기능 테스트를 통과하는 것만으로는 안전한 코드라고 부르기 어렵다.

이 벤치마크를 “에이전트는 보안을 못 한다”는 문장으로 소비하면 과장이다. 과제와 모델, 평가 오라클에 따라 결과는 달라진다. 다만 취약점이 생기는 지점을 재현하고, 기능과 보안을 함께 확인하는 평가가 필요하다는 방향은 분명하다. Hacker News의 관련 토론에서도 테스트 방법의 이름보다 실제 목표를 유지하는 능력이 중요하다는 논점이 반복된다.

독립 오라클과 변형 테스트를 조합한다

독립 오라클은 에이전트가 만든 코드와 테스트가 같은 실수를 공유하지 않게 하는 장치다. 참조 구현, 별도의 계산기, 스키마 검증기, 보안 스캐너, 정책 엔진처럼 구현 경로가 다른 판정기를 사용한다. 완전히 다른 구현을 만들기 어렵다면 입력을 변형한 뒤 보존되어야 하는 관계를 검사해도 된다.

예를 들어 권한 검사 함수에는 사용자 역할을 한 단계 낮추거나, 리소스 소유자를 바꾸거나, 만료 시간을 과거로 이동시키는 변형을 적용한다. 정상 요청이 승인되더라도 이 변형이 여전히 승인되면 권한 경계가 무너진 것이다. 압축이나 프로토콜 구현에서는 입력 순서, 스트림 간 차이, 길이 경계, 중복 프레임을 변형하고 결과 관계가 유지되는지 확인한다.

보안 검증은 모든 경로에 같은 강도로 붙일 필요가 없다. 외부 입력을 파싱하는 코드, 권한과 테넌트 경계를 넘는 코드, 파일·명령어·네트워크를 다루는 코드, 암호 키나 개인정보를 만지는 코드부터 높은 위험도로 분류한다. 이 영역은 정적 분석만으로 끝내지 않고 악성 입력, 권한 우회, 로그·에러 메시지의 정보 노출까지 독립적으로 확인한다.

팀에 적용할 때 사람이 남겨야 할 경계

사람은 코드를 승인하기보다 판정 기준을 승인한다

AI 코딩 에이전트의 속도를 살리려면 사람이 모든 줄을 다시 타이핑하듯 리뷰할 수는 없다. 대신 사람이 반드시 승인해야 하는 경계를 앞쪽에 둔다. 사전조건과 사후조건, 보안 위험도, 외부 시스템에 미치는 영향, 정의되지 않은 동작의 정책을 먼저 승인하고, 에이전트는 그 계약 안에서 구현과 테스트를 반복하게 한다.

승인 기준도 “테스트가 모두 통과했다”에서 끝내지 않는다. 변경이 만든 새 테스트 중 실제 요구사항과 연결되지 않는 테스트는 무엇인지, 한 테스트가 실패했을 때 독립된 오라클이 같은 결론을 내리는지, 변형 테스트가 위험 경계를 건드렸는지를 묻는다. 커버리지가 올랐는데도 핵심 분기가 실행되지 않았다면 숫자를 낮게 보는 편이 정확하다.

작은 자동화 루프부터 시작한다

작업 프롬프트에는 아래처럼 순서를 명시할 수 있다.

1. 변경 대상의 사전조건, 사후조건, 정의되지 않은 동작을 먼저 작성한다.
2. 각 계약에 대해 정상, 경계, 위반 입력을 한 가지씩 만든다.
3. 기대값은 현재 구현이 아니라 규격·참조 계산·불변식에서 만든다.
4. 독립 오라클 또는 변형 테스트로 같은 버그를 공유하지 않는지 확인한다.
5. 보안 위험도가 높은 경로는 정적 분석과 악성 입력 검증을 별도로 실행한다.
6. 사람이 승인할 질문과 에이전트가 자동으로 통과시킬 질문을 구분해 보고한다.

이 루프는 TDD를 버리는 방법이 아니다. TDD의 빠른 피드백은 유지하되, 첫 번째 실패 테스트를 쓰기 전에 계약을 넣는 방식이다. 에이전트가 만드는 테스트의 양을 제한하고, 각 테스트가 어느 계약과 위험을 커버하는지도 함께 기록하게 하면 리뷰할 것이 줄어든다. 이전에 다뤘던 AI 운영 자동화와 팀의 간극과 비슷하게, 도구 도입보다 책임 경계와 운영 루프를 먼저 정하는 편이 오래 간다.

내가 팀에서 보고 싶은 결과 보고서는 “테스트 184개 통과”가 아니다. “인증되지 않은 요청은 거부했고, 동일한 멱등 키의 재시도는 한 건으로 남았고, 권한을 바꾼 변형은 실패했으며, 이 판정은 구현과 다른 오라클로 확인했다”에 가깝다. 숫자는 그 뒤에 붙어도 된다.

AI 코딩 에이전트에게 테스트를 시키는 일은 이제 기본 동작이 됐다. 다음 단계는 테스트 도구를 더 많이 붙이는 것이 아니라, 에이전트가 무엇을 검증해야 하는지와 결과를 누가 독립적으로 판정하는지를 설계하는 일이다. 26개 조건 실험이 보여준 피상적 사용은 에이전트의 능력만 탓할 문제가 아니다. 사람의 요구사항이 기법 이름에서 멈췄는지도 같이 돌아봐야 한다.

계약을 먼저 쓰고, 위험한 경계를 변형하고, 독립 오라클로 결과를 판정하고, 보안 변경에는 사람의 승인 지점을 남긴다. 이 네 가지가 있으면 TDD든 퍼징이든 형식 도구든 목적에 맞게 쓸 가능성이 커진다. 테스트 통과는 출발 신호이고, 품질 판단은 그 다음 단계에서 시작된다.