AI 시대의 개발자는 검증을 설계한다 (5) - Test를 검증하는 변형

새로운 테스트를 작성했습니다. '원-달러 환율이 1,450원이고 / 금액을 5000원으로 입력하면 / 3.45 USD가 화면에 보여야한다...

2026. 07. 30·linkedin에 발행

새로운 테스트를 작성했습니다. ‘원-달러 환율이 1,450원이고 / 금액을 5000원으로 입력하면 / 3.45 USD가 화면에 보여야한다…’

테스트 실행. 기분 좋게 통과합니다. ✅

🤔.. 그런데 이 통과를 믿고 코드를 배포해도 될까요?

이 테스트가 버그를 잡는 방어력이 있어야 합니다. 지금 버그가 없어서 통과하는지, 테스트가 느슨해서 통과하는지는 모르는 일이니까요.

흔히 ‘테스트를 잘 짰다’라고 하면 커버리지 100% 같은 지표를 떠올립니다. 하지만 커버리지는 테스트가 어떤 코드를 실행했다는 뜻일 뿐. 버그를 실제로 막아내는지 알 수는 없습니다.

그럼 어떻게 버그를 막아낼지 알까요?

간단합니다. 버그를 심어보면 되죠.

코드를 바꿔서 버그를 심어봅니다. 등호 (=)를 부등호 (>=)로 바꿉니다. Guard 검사 조건 `if this { return }`을 없앱니다.

일부러 버그를 심었는데 테스트가 통과한다고요? 그러면 제 테스트가 허술하다는 뜻이겠죠.

비유로 설명하자면, 테스트는 갑옷입니다.

image

갑옷은 적의 화살을 막는 방어력이 있어야 합니다.

갑옷을 만들고 나서 검사를 하면 알 수 있습니다. 실제 적이 쏠 법한 강도와 각도로 화살을 쏴봅니다. 그리고 갑옷이 뚫리지 않는지 보는 거죠.

뚫렸다면, 뭔가 문제가 있는 거니까요.

테스트가 갑옷이라면, ‘일부러 심은 버그’가 테스트를 검증하는 ‘화살’입니다.

이런 의도적 버그를 심는 검증을 소프트웨어 테스팅에서는 **변형 테스트(Mutation test)**라고 부릅니다.

최고라면서 왜 아무도 안 쓸까?

변형 테스트(Mutation testing)는 사실 새로운 개념이 아닙니다. 오래된 기법이고, 연구도 많이 됐습니다.

어떤 연구는 실제 버그 357개를 뜯어봤더니, 변형 테스트가 커버리지보다 테스트 품질을 더 잘 예측한다는 걸 보여줬습니다. (Just et al., 2014). <Predictive Mutation Testing>에선 ‘실제 테스트의 능력을 검증하는 데 최고다’라는 평까지 합니다.

그런데 이상합니다. 저는 현업에서 한번도 변형 테스트라는 단어를 못 들어봤거든요.

> 그렇게 좋다는데, 왜 아무도 안 쓸까요?

2가지 문제가 있었습니다.

첫째, 전통적인 방식은 변형을 프로그램이 생성합니다. (StrykerJS, PIT 등) 코드를 정해진 규칙대로, 가능한 모든 방식으로 망가뜨립니다.

이 방법은 간단한 함수 하나에서도 변형이 10-20개씩 쏟아집니다. 코드베이스 전체면 변형이 수천개씩 나올 수 있습니다. 덕분에 테스트를 돌리는 비용과 시간이 엄청나게 들어갑니다.

둘째, 변형을 집어넣고 테스트가 실패했으면 괜찮습니다. 그런데 변형을 집어넣고도 테스트가 통과하면 어떨까요.

2가지 가능성이 있습니다. 진짜 테스트에 구멍이 있거나, 아니면 변형이 유의미한 버그를 만들지 못했거나. 극단적인 예로 a + b를 b + a로 바꾸는 변형이었다고 해볼까요. 이러면 코드는 다르지만 결과는 늘 같습니다. 테스트의 문제가 아닌 거죠. 원래는 사람이 이렇게 테스트의 문제, 변형의 문제인지 판단을 해야했습니다.

여기까지 듣고 보니 ‘아 그래서 아무도 안 썼구나’ 싶습니다.

테스트도 짜기 귀찮아 하는 사람이 많습니다. 테스트의 방어력을 검증하는 테스트에 이렇게 시간과 노력이 들어간다니. 아무리 효과가 좋다고 해도 막상 실무에선 안 쓰는 거죠.

> 하지만… 변형을 AI가 작성하고, AI가 판단한다면 어떨까요?

AI는 전수 작성을 하는 대신, 맥락을 보고 진짜 위험한 변형 3-5개만 만들 수 있습니다. 완벽한 검증은 아니라도, 충분히 쓸만한 검증이 가능합니다.

기존엔 투자 대비 효용이 없어 쓰지 않던 ‘검증을 검증하는 방법’. 그런데 그걸 AI의 도움으로 써먹는다? 그동안 제가 탐구해온 ‘검증 설계’에 딱 맞는 주제였습니다.

> ‘테스트가 진짜 방어력이 있나?’ 매번 테스트를 작성하는 커밋마다, 이 검증을 AI가 알아서 자동으로 해준다면 어떨까요?

그래서 제가 직접 파보았습니다.

이 과정을 자동화할 스킬과 훅을 만들었습니다.

써보고 저는 상당히 만족스러웠는데요. 어떤 원칙을 담아 만들었는지 한번 살펴볼까요?

아무 변형이나 심으면 의미가 없다

테스트는 테스트 케이스 작성이 중요합니다. 마찬가지로 변형 테스트도 ‘변형’을 잘 만들어야 합니다.

갑옷이 진짜 화살을 막는지 최대한 적은 화살로 테스트하려면 어떻게 해야하죠?

진짜 적이 화살을 쏘는 강도와 각도로 쏴서 갑옷을 검증해야합니다.

변형도 단순 ‘값을 바꿔보자’ ‘조건문을 뒤집자’ ‘코드 순서를 바꾸자’로는 안 됩니다. 진짜 우리가 막으려는 버그에 가깝게 만들어야 합니다.

먼저 우리가 ‘막고 싶은 버그’을 잘 알아야 합니다. 그리고 거기서부터 진짜 버그 같은 변형을 만들어야 합니다. (3편에서 말했던, 좋은 테스트를 쓰려면 스펙 정의가 중요하다는 내용과도 연결됩니다)

예를 들어, 무료 배송 혜택을 받는 최소 주문 금액 계산 코드가 있습니다.

그냥 무지성으로 값을 바꿔보겠습니다.

흠. 배송비가 무료가 아니라 1원이 된다?

이것도 변형은 변형입니다. 하지만 실제 세상의 버그와는 좀 동떨어져있습니다.

반대로 이 변형을 볼까요?

최소 주문 금액이 3만원에서 2만원으로 바뀌었습니다. 2만원만 주문해도 무료 배송이 나갑니다. 이게 라이브에 배포되었다면? 회사가 배송비를 다 감당해야합니다.

듣기만 해도 오싹한 버그죠. 😱

이런 버그가 이전의 예시보다 더 개발자가 진짜 무서워하고 ‘막고 싶은 버그’에 가깝습니다.

변형은 실제 세상의 버그를 만들어야 합니다.

이게 가장 핵심입니다.

정책 조건의 제거나 반전. API 응답 필드 오해하기. 수수료가 있는데 확인 단계 건너 뜀. 실패 응답을 성공처럼 처리. 효과 좋은 변형입니다.

단순 문구 변경. 현실적이지 않은 무작위 코드 삭제. 타입 에러만 나는 변경. 겉으로 보이지 않는 내부 리팩터링 차이. 좋지 않은 변형입니다.

그 외에도 몇가지 조건이 더 있습니다.

  • 테스트 케이스에서 도달하는 코드를 변형해야 한다. 가끔 애초에 실행되지 않는 죽은 분기에 변형을 심는 경우가 있는데, 이러면 어차피 실행조차 안되고, 테스트가 못 잡아도 테스트의 탓이 아닙니다.

  • 변형으로 인해 겉으로 보이는 결과가 바뀌어야 합니다. 내부 계산은 틀렸는데 화면/반환값 같은 관찰 표면엔 안 드러나는 경우. 이러면 테스트가 볼 수 없습니다.

  • 앞에서 변형을 해보았다면, 같은 규칙의 다른 지점을 찔러봐야 합니다. 경계값(>=)을 한 번 찔렀으면, 다음엔 guard를 통째로 지워보는 식이죠.

  • 컴파일이 되는 변형이어야 합니다. 한 연구에서 GPT-4o가 만든 변형은 24.4%가 컴파일조차 되지 않았습니다. 규칙 기반 도구가 1.67%였던 것에 비하면 15배죠. (Wang et al., 2024)

자세한 조건들은 제가 만든 스킬에 담아두었습니다.

변형을 했는데 테스트가 통과했다면?

진짜 버그 같은 변형을 만들었습니다. 테스트를 돌립니다. 그런데 테스트가 통과했습니다..?

그럼 이 테스트는 부실한 걸까요?

아직입니다. 테스트의 문제가 아닐 수도 있거든요.

  • 다른 테스트가 이미 잡고 있는 버그였을 수도 있습니다.

  • 아니면 바꿔도 결과가 동일한 변형일 수도 있고요. (a+b -> b+a)

  • 아니면 변형은 했는데 사실 코드상 실행이 안 되어서 결과가 똑같았을 수도 있습니다.

이런 경우에는 테스트의 잘못이 아니니까 그냥 넘어가야 합니다.

반대로 변형도 제대로 됐고, 결과도 바뀌었는데 테스트가 통과했다면 테스트가 부실했던 거죠.

이 경우에는 테스트를 보강해줘야 합니다.

여기서 AI가 판정을 제일 자주 틀립니다. 제가 돌려보면서 가장 많이 본 실패가 이거였어요. “테스트가 통과했네? 이건 변형이 문제였겠지” 하고 넘어가버립니다.

이 판정을 정확하게 하는 장치를 넣어봤습니다.

원래는 이 순서로 판단을 합니다. “변형을 심었는데 테스트가 통과했다” → “왜 통과했을까?” LLM은 주어진 결과에 그럴듯한 설명을 붙이는 데 아주 능숙하기 때문에 쉽게 통과 근거를 만듭니다.

순서를 바꿉니다. 먼저 결과를 보기 전에 ‘이 변형이 어떤 테스트를 실패하게 만들지’ 한 줄 적게 합니다. “이 변형은 장바구니_재고없음 테스트를 실패시킬 것이다.” (약간 red-green TDD 같죠?)

“실패를 예측한다” → “변형을 심는다” → “‘예측한 테스트가 실제로 실패했는가?” 이 질문은 결정적입니다. 실패 안했을 때 얼버무리기가 훨씬 어렵습니다.

그래도 애매하면 테스트가 부실했다고 남겨둡니다. 부실한데 통과시켜주는 것보단 나으니까요.

자세한 판정 방법은 역시 제가 만든 스킬에 담아두었습니다.

커밋할 때마다 변형 테스트하기

코딩 에이전트에는 훅(Hook) 기능이 있습니다.

훅을 활용해서 커밋을 하려는 순간을 캐치합니다. 그리고 변형 테스트를 했는지 검사합니다. 안했으면 변형 테스트하라고 지시하면서 스킬을 피드백으로 돌려줍니다.

이렇게 하면 제가 ‘테스트 짜줘’만 하면 됩니다. 테스트 검증 루프가 알아서 돌아갑니다.

AI가 테스트를 원칙에 맞춰서 짠다 > 테스트를 통과시킨다 > 커밋을 하려고 한다 > Hook에서 막힌다 > 변형 테스트를 해야한다고 피드백을 받는다 > 변형 테스트를 수행한다 > 결과를 커밋 메시지에 적는다 > Hook이 커밋을 통과시킨다.

변형 테스트 결과를 코드베이스에 적진 않았습니다. 코드가 변하면서 내용이 어긋날 위험이 있습니다. 특정 시점에 검증을 했다는 의미로 커밋 메시지 정도에만 적어두면 충분하다고 생각했습니다.

생각보다 정말 효과가 좋았습니다. 부실한 테스트가 많지는 않았습니다. 하지만 매번 성실하게 테스트를 알아서 수행하다가, 한번씩 부실한 테스트가 드러나서, 재작성을 합니다. 그걸 보니 ‘일을 제대로 하는군’ 하고 기분이 좋더라구요.

진짜 버그를 막는 테스트라는 걸 믿을 수 있게 됐습니다.

검증을 검증하기

테스트는 코드가 제대로 동작하는지 검증합니다. 하지만 테스트 결과를 믿을 수 있으려면, 테스트가 제대로 동작하는지도 검증해야 합니다. 검증 기계도 검증이 필요해요.

‘테스트 통과했다고 믿지 말고, 실제 버그를 심어보자’ 라는 뜻입니다. 갑옷 만들었다, 안전하다. 믿지 말고 실제 적군처럼 화살을 쏴보는 겁니다.

이렇게 우리는 AI가 짠 코드를 더 믿고 배포할 수 있게 됩니다.

생각해보면 재미있습니다. 이 ‘검증을 설계한다’ 시리즈는 계속 같은 패턴을 반복하고 있거든요.

변형 테스트(Mutation testing)은 오래된 기법입니다. 이미 소프트웨어 공학에 있는 좋은 기법이었죠. 다만 노력이 너무 많이 들어서 안 쓰였을 뿐입니다.

하지만 이 시리즈는 AI로 그걸 재평가합니다. 그동안 하지 못했던 엔지니어링의 베스트 프랙티스를 더 싸게 구현해봅니다.

테스트도 검증이 필요하지만 사람이 하기엔 너무 귀찮았습니다. AI가 변형 테스트를 할 수 있게 가이드를 주고 개발 과정에 통합시켰습니다.

AI가 딸깍 짜는데 테스트가 필요없는 거 아니야? 아닙니다. 오히려 더 튼튼한 테스트를 짜야겠죠. 새로운 시대의 병목은 검증이 되었으니까요.

공유드린 스킬과 훅 한번 사용해보시고, 단단한 개발에 도움이 되시길 바랍니다!