테스트 14분 읽기

AI로 빠르게 개발하는 팀이 회귀와 싸운 기록 — 결국 '측정'부터

AI로 개발 속도가 빨라지자 회귀가 폭증했다. 온갖 검증을 시도하다 결국 '무엇이 새는지부터 측정하자'로 돌아온 이야기다.

AI로 개발 속도가 빨라지자, 역설적이게도 회귀(regression)가 폭증했다. 온갖 검증을 시도하다 결국 “무엇이 새는지부터 측정하자” 로 돌아온 이야기다.


AI가 개발을 가속하자, 회귀가 폭증했다

우리 팀(치과 3D 스캔 솔루션)은 AI를 개발에 적극적으로 들이면서 코드 변경 속도가 크게 빨라졌다. 서비스 품질도, 코드 품질도 개선하겠다고 손을 댄 변경이 그만큼 많아졌다. 그런데 역설적이게도 — 그 많은 변경이 회귀(regression) 문제를 쏟아냈다. 올해 들어 배포할 때마다 “고친 게 다시 깨지고”, “내 변경이 엉뚱한 곳을 망가뜨리는” 일이 반복됐다.

그때마다 우리는 회귀를 막으려고 여러 방법을 시도했다. 이 글은 그 시도들의 기록이고, 결국 “무엇이 새는지부터 측정하자” 로 돌아온 이야기다.

회귀를 잡으려 한 시도들

회귀 대응 여정

① 개발 검증 문서. 처음엔 기능마다 “개발 검증을 마쳤다”는 문서를 작성하는 것으로 시작했다. 하지만 배포 일정은 빡빡했고, 모든 기능을 문서로 검증하는 건 시간상 거의 불가능했다.

② e2e로 명세를 검증. 그래서 검증 문서에 적힌 기능을 e2e 테스트로 작성하고, 그 테스트가 통과하면 개발 검증이 끝난 것으로 보기로 했다. 테스트 결과로 검증 문서를 채우는 방향이었다. 방향은 옳았다. 그런데 당시엔 e2e를 돌릴 환경은커녕 셋업 코드조차 제대로 없었다. 환경 만드는 데만 시간이 다 들어갔고, 결국 일정 안에 모든 테스트 케이스를 못 만들어 수동 테스트로 메웠다.

③ AI 검증 (Playwright MCP). 테스트 코드 작성이 막힌 상황에서, AI가 브라우저를 직접 몰며 명세대로 검증하게 했다. 생각보다 잘 됐고, 남은 기간 동안 많은 케이스를 검증할 수 있었다. 문제는 AI가 개입한 검증은 비결정적이라는 것이었다. 정식으로 쓰려면 “항상 같은 방식으로 같은 것을 검증한다”는 보장이 필요했다.

④ AI 검증 방식을 문서화. 그래서 AI가 검증한 절차를 YAML로 박제해보았다. 하지만 이번엔 e2e 코드 대비 시간과 AI 토큰 비용이 만만치 않았다.

⑤ 격리환경 + e2e. 결국 e2e 코드로 돌아왔다. 이번엔 결정론적인 결과를 위해 격리환경을 도입했다. 이전엔 dev 환경에서 시드 데이터와 픽스처로 테스트를 돌렸는데, 머지되는 코드가 많아 검증 환경의 안정성을 보장할 수 없었다. (사실 격리환경은 이걸 위해 만든 건 아니었다. 원하는 시점의 커밋을 그대로 재현하는 게 본래 목적이었다.) 어쨌든 격리 위에서 작성한 e2e로 한결 결정론적인 결과를 얻게 됐다.

그럼에도 — 5월로 예정됐던 배포는 품질등급을 통과하지 못해 7월로 밀렸다. 이렇게 애를 썼는데도, “안전하게 바꿀 수 있는 환경”은 끝내 만들어지지 않은 것이다. 그리고 기본적인 검증 토대가 없는 한 이 상태가 계속될 거라는 게 보였다.

한 걸음 물러서서 — 왜 못 잡았을까

현 상황을 돌아봤다.

우리 서비스엔 애초에 적절한 테스트가 거의 없었다. 지금 있는 유닛·통합·e2e 테스트의 대부분은 AI 도입 이후에 생성된 것이다. 테스트는 그 자체가 유지보수 비용이고 특히 e2e는 비싸서 과거엔 엄두를 못 냈는데, AI로 훨씬 빠르게 찍어낼 수 있게 되면서 많이 만들어졌다. PR마다 돌리고 있기도 하다.

그런데 간과한 게 있었다. 그렇게 만든 테스트의 품질, 그리고 그게 정말 의미 있는 테스트인지였다. red→green 방식으로 버그를 고쳐도 리오픈되는 버그가 생기고, 내 변경이 아예 다른 곳에 영향을 주는 일이 계속됐으니까.

처음엔 이렇게 봤다. 모두가 AI로 경계 없이 개발하다 보니 내 변경이 어디까지 영향을 미칠지 놓치는 일이 많아졌다. AI가 코드를 쉽게 써주니, 예전엔 보수적으로 막던 수정을 이제는 더 근본적인 단계에서 손대게 됐다. 근본적인 수정은 의존하는 모든 곳에 영향을 미치는데, 꼼꼼히 확인하지 않으면 놓친다. 이게 AI를 활용한 개발의 함정이라는 생각이 들었다.

그렇다면 왜 테스트가 이걸 못 잡았나? 처음엔 테스트 품질의 문제라고 생각했다 — 이 생각은 지금도 변하지 않는다. 프론트엔드 개발자로서 AI가 만든 프론트 유닛·통합·e2e를 보면 의아한 게 많았다. 좋은 테스트는 구현 방식이 바뀌어도 깨지지 않아야 한다고 생각한다. 그래야 “무엇을 검증하려는지”를 명확히 담고 있다는 뜻이니까. 그런데 AI가 만든 테스트, 특히 유닛·통합은 구현 그 자체를 검증하는 경우가 많았다. 이게 틀린 건 아니다 — 레거시 코드에 테스트를 붙이는 한 방법이니까. 다만 그건 이미 존재하는 코드를 고정하기 위한 것이지, 우리에게 필요한 “명세를 대신할 수 있는 테스트”는 아니었다.

그런데 진짜 원인은 ‘구조’였다

테스트 품질이 원인이라 보고 조사를 시작했는데 — 조사해보니 테스트 품질보다 구조적인 원인이 컸다.

  1. 무엇이 검증을 빠져나가는지 측정할 방법이 없었다. 이런 지표가 있어야 “개선되는 점”과 “개선해야 할 점”을 알 수 있는데, 지금은 그 정보를 얻기조차 어려웠다.
  2. 코드 오너십이 강하지 않았다. 그래서 내 수정이 다른 영역에 영향을 줄지 모르는 경우가 많았다.
  3. 테스트가 실제로 놓치는 부분이 많았다. AI가 분석한 테스트 품질은 높게 나왔다. 그런데 mutation testing을 돌려보니 — “A급”으로 평가받은 코어 라이브러리조차 mutation score가 57%였다. 코드에 일부러 심은 결함의 43%가 테스트를 통과했다는 뜻이다. (가장 나쁜 파일은 47%였다.)

심은 결함 중 테스트가 잡은 비율 — mutation 57%

결국 AI가 만들고 AI가 검증하니, 놓치는 부분이 많고 그 질이 떨어지는 구조였다. 이 구조적 문제들이 테스트 품질을 끌어내리는 진짜 원인이었다.

mutation testing은 소스에 일부러 결함(mutant)을 심고, 테스트가 그걸 잡아내는지(kill) 측정하는 기법이다. 커버리지가 “코드를 실행했나”라면, mutation score는 “그 코드가 틀렸을 때 정말 빨개지나”를 본다. 사람이나 AI의 정성 평가가 “단단하다”고 해도, mutation score는 종종 그게 착각임을 숫자로 드러낸다.

첫 단추 — 측정부터

그래서 가장 먼저 할 일을 정했다. 우리가 확인할 수 있는 지표를 찾고, 그걸 시각화하자.

이렇게 “검증을 빠져나간 결함의 비율”을 업계에서는 Defect Escape Rate(DER) 라 부르고, 이를 재는 통계 지표가 여럿 있다 — DRE(Defect Removal Efficiency), DDP(Defect Detection Percentage), PCE(Phase Containment Effectiveness), TDCE(Total Defect Containment Effectiveness) 등. 그런데 이들에겐 공통점이 있다. 모두 분모에 “잡은 결함”이 들어간다. 「개발이 잡은 결함 + 빠져나간 결함」 중 빠져나간 비율을 보는 식이라, 개발 단계에서 자체적으로 잡은 결함을 기록하고 있어야 한다. 그런데 우리에겐 그 데이터가 없었다 — 개발이 코드를 고치면서 무엇을 걸러냈는지를 아무 데도 남기지 않고 있었던 것이다.

다행히 얼마 전부터 “개발 테스트”라는 필드를 도입해서, QA 환경으로 넘기기 전에 버그를 수정하지 않은 다른 개발자가 검증하고 pass/fail을 남기게 했다. 이 데이터는 다음 배포부터 쌓일 것이다. 하지만 지금은 없다.

그래서 지금 당장 쓸 수 있는 형태로 찾은 것이 CFR(Change Failure Rate) 계열의 접근이다. 핵심은 분모를 잡은 결함의 총량이 아니라 “배포”로 바꾸는 것이다. 「이 배포가 흘려보낸 결함이 몇 개인가」 — 이건 개발이 무엇을 걸러냈는지 몰라도 셀 수 있다. DevOps의 DORA 메트릭이 Change Failure Rate를 배포 단위로 재는 것과 같은 발상이다. 우리 식으로는 “배포당 빠져나간(escape) 결함” 이 된다.

배포당 escaped 결함 추세 (CFR)

여기서 몇 가지 설계 판단이 있었다.

  • 단순 건수가 아니라 이변량으로 본다. “Minor 17개”와 “Critical 17개”는 전혀 다른 무게다. 그래서 건수와 함께 severity 가중치를 같이 추적한다.
  • 측정 단위를 시간이 아니라 ‘배포’로 둔다. 버그가 언제 발견되느냐는 QA의 검증 속도와 순서에 휘둘린다(검증 인력이 이번 주에 한 영역을 집중해서 보면 그 영역 버그가 급증한다 — 코드는 그대로인데). 반면 “이 배포가 결국 몇 개의 결함을 흘려보냈나”는, 충분한 시간이 지나면 고정된다. 그래서 시간이 아니라 배포를 단위로 잡는다.
  • 정상 범위는 ‘관리 한계(control limit)‘로 본다. “20개 넘으면 사고” 같은 임의의 기준선을 긋지 않는다. 대신 우리 데이터의 분포에서 정상 변동 범위를 계산한다(위 그래프의 점선). 이 선을 넘는 배포만 “통계적으로 비정상 = 사고 배포”로 본다. 절대 점수가 아니라, 우리 자신이 나아지고 있는지를 추세로 보는 것이다.
  • 한 배포가 아니라 여러 배포에서 발견된 결함은 ‘재발’ 신호다. 실제로 두 번 이상 발견된 결함의 93%가 리오픈된 것이었다. 그래서 같은 데이터에서 escape와 재발을 함께 읽는다.

이걸 대시보드에서 추세선으로 볼 수 있게 했다. 위 그래프처럼, 대부분의 배포는 정상 범위 안에서 흔들리고, 가끔 관리 한계를 한참 넘는 “사고 배포” 가 튄다. 그 배포들이 회고의 첫 대상이다.

이건 시작일 뿐이다

CFR은 모든 것의 시작이다. 무엇이 새는지가 보이기 시작했으니, 이제 개선되는지/악화되는지를 추적할 수 있다.

다음 단계는 내 코드 오너십 영역의 테스트를 전수 검증하고, 테스트 품질과 커버리지를 끌어올리는 것이다. 우선순위는 아직 고민 중이다.

그리고 측정도 진화할 것이다. “개발 테스트” 데이터가 쌓이면 앞서 못 썼던 DRE 같은 효율 지표로 올라갈 수 있고, 그 전에도 두 독립 검증(개발/QA)이 각각 잡은 결함의 겹침으로 미발견 결함을 통계적으로 추정하는 capture-recapture(생태학에서 개체수를 추정하는 그 기법) 같은 방법도 있다. CFR은 그 여정의 첫걸음이다.

측정의 진화 — CFR에서 시작한다


돌아보며 — AI 시대 개발에서 배운 것. AI는 테스트를 빠르게 만들어준다. 그런데 AI가 만든 테스트를 다시 AI가 평가하면, “괜찮아 보이는데 실제로는 버그를 못 잡는” 사각이 생긴다. 그리고 우리는 그걸 측정하지 않으면 영영 모른다. 좋은 테스트는 구현이 아니라 의도(명세)를 검증해야 하고, 품질 개선은 “느낌”이 아니라 무엇이 새는지를 숫자로 보는 것에서 시작한다. 화려한 자동화보다, 우리가 지금 어디에 서 있는지를 먼저 보이게 만드는 것 — 그게 첫 단추였다.


참고 자료

Defect Escape Rate (DER)

Change Failure Rate (DORA)

Mutation Testing / “Coverage Theater”

결함 제거 효율 (DRE) 벤치마크

Capture-Recapture (검증이 놓친 결함 추정)