🍀이 글은 다음과 같은 분들께 추천합니다
- AI 생성 E2E(End-to-End) 테스트가 대량으로 실패하여 원인 확인에 매달리고 있는 분
- 'Fail'과 'skip'을 수동으로 분류하는 데 한계를 느끼는 분
- 테스트 실패의 트리아지(Triage, 우선순위 분류)를 시스템적으로 가볍게 하고 싶은 QA 엔지니어
주석: 본 기사에서 사용된 제품명이나 디렉토리 구조 등은 설명을 위한 임시 명칭 또는 추상화입니다.
서론
[이전 글]
안녕하세요! 평소에는 QA 엔지니어로서 AI를 활용한 E2E 테스트 자동화 프레임워크 설계에 매진하고 있습니다.
AI에게 대량의 테스트 생성을 시키고 실행하면, 다음과 같은 벽에 부딪힙니다.
- Fail과 skip이 한 번에 수십 건씩 발생하여 무엇부터 봐야 할지 모름
- 실패 이유가 '테스트 측의 실수'인지 '앱의 결함'인지 매번 열어 확인해야 함
- '미구현이라서 skip'와 '환경 문제로 도달할 수 없어 skip'이 같은 skip에 섞여 있음
즉, 테스트를 빠르게 돌릴 수 있게 되었지만, 실패 후 분류 작업이 새로운 병목 현상(bottleneck)이 된 것입니다. 본 기사에서는 테스트 실패의 원인을 자동으로 분류하고, 사람이 봐야 할 것만 트리아지 큐(triage queue)에 쌓는 시스템을 공유합니다. 가장 효과적이었던 점은 분류 규칙을 늘리는 것이 아니라, '이 문자열만으로는 결정하지 않는다'고 정한 것입니다.
Code / Limit / Defect의 세 가지 분류는 Spec Drift(명세와 구현의 불일치) 글에서 '분류가 첫 번째 게이트'라는 방향성만 소개했습니다. 본 기사는 그 내용을 다룹니다 — 실제로 무엇을 보고 분류하는지 그리고 분류를 잘못했을 때 어떤 일이 발생하는지입니다. Spec Drift 글을 읽지 않아도 이해할 수 있도록 작성했습니다.
시리즈 독자 대상: Spec Drift 글 이후 변경된 점
Limit 대응으로 작성했던 '재시도(Retry)' 기능은 현재 사용하지 않습니다 (테스트의 자동 재시는 0으로 설정하고, 결함 의심은 후술할 재현 확인에서 판단합니다). 데이터 기인 실패는 테스트 데이터의 충돌(이전 테스트의 잔해 등)을 테스트 측의 정리 문제로 Code에 옮기고, 데이터 부족은 Limit 상태 그대로 skip 처리합니다. 또한, 스토리 단위로 실행 경로를 복구하는 단계에서는 미배포 등의 '자동 skip'도, skip을 권장하고 이유를 기록하는 형태로 변경했습니다 (테스트를 생성하는 단계에서는 외부 서비스 의존성 등으로 인해 skip을 작성할 수 있습니다. 여러 스토리를 묶어 돌리는 배치(batch) 경로는 아직 자동으로 skip합니다). 스토리 단위로 실행하는 경로에서는, 힐링(healing) 3단계를 거치며 하던 원인 분리도, 복구 전 분류 단계로 옮겼습니다 (배치 경로에서는 여전히 복구 과정에서 분리하고 있습니다).
결론: 원인을 3가지 분류로 나누고, 결정적 증거가 없는 것은 사람에게 넘긴다
먼저 결론부터 말씀드립니다. 테스트 실패는 원인을 Code / Limit / Defect의 세 가지 분류로 나눕니다.
| 분류 | 의미 | 일차 대응 |
|---|
| Code | 테스트 구현 측 문제 (셀렉터/대기 시간 부족) | 자동 힐링 (실패한 테스트를 AI가 자동으로 수리하는 시스템) |
| ... | | |
| 각 판정에는 기계의 규칙 또는 상위 AI(고성능 모델)가 확신도(해당 분류가 얼마나 확실한지를 0~1로 나타낸 값)를 부여합니다. 자동 복구에 투입되는 것은 원칙적으로 'Code이며, 확신도가 일정 이상'인 경우만입니다. | | |
참고로, 본 기사에서 다루는 복구와의 연계(복구 대상인지 여부 판정)는 스토리 단위로 실행하는 경로의 이야기입니다. 여러 스토리를 묶어 돌리는 배치 경로는 아직 복구 전 이 판정이 들어가 있지 않으며, 실패한 테스트를 복구한 후에만 기계 단계의 분류가 작동합니다.
핵심은 Defect라고 단정하기 위해서는 확신이 필요하다는 설계입니다. AI가 '이것은 결함이다'라고 가볍게 단정하면, 위양성(False Positive) 이슈가 양산되어 개발팀의 신뢰를 잃게 됩니다. 따라서 기계로도 상위 AI로도 결정적 증거가 나오지 않은 것은 Defect로도 Code로도 분류하지 않고, human-review
(사람이 보는 보류 큐)로 넘깁니다.
왜 Pass / Fail의 이진법과 단순한 skip으로는 안 되는가
이유는 간단합니다. skip과 Fail의 내용물이 제각각이기 때문입니다.
예를 들어 'Fail' 중에는 셀렉터만 바뀐 경우(Code), 대상 기능이 아직 배포되지 않은 경우(Limit), 정말 앱이 고장 난 경우(Defect)가 뒤섞여 있습니다. 이들을 같은 'Fail'로 취급하면, 사람은 전 건을 열어 내용물을 판단할 수밖에 없습니다.
skip도 마찬가지입니다. test.skip()
만약 스킵하는 이유가 구조화되어 있지 않다면, '의도적으로 제외된 대상'과 '현재는 실행할 수 없을 뿐인 경우'를 구분할 수 없습니다.
분류 로직: 기계로 결정 가능한 것만 결정하고, 나머지는 AI와 재현으로 확인하기
첫 번째 단계는 실행 시 얻을 수 있는 신호를 기계적으로 대조하는 것입니다. 주로 다음을 사용합니다.
- 오류 유형과 call log (Playwright가 실패 시 남기는 조작 로그)의 결정적인 문자열:
intercepts pointer events
(부모 요소가 클릭 이벤트를 가로채는 경우)와 같이, 원인을 거의 특정할 수 있는 신호를 Code에 부여합니다. 명확히 특정하기 어려운 것들(후술할 waitForResponse 등)은 확신도를 '수정 허용 하한선' 바로 지점에 두고 Code에 임시 배치합니다 (→과제1).
- 기대치와의 불일치: 사양을 검증하는 Assertion이 기대값과 다른 경우 Defect 후보로 만듭니다 (표시된 항목 수와 같이 숫자를 값으로 비교하여 발생한 차이는 제외. 후술).
- 사전 검사 결과: 테스트 항목서와 구현 내용을 대조하여 '앱 측 기능 미구현'으로 판정된 것은 Limit에, '구현 측을 수정해야 함'으로 판정된 것은 Defect 후보로 만듭니다.
여기까지는 정규 표현식과 상태 대조만 사용하며 AI는 사용하지 않습니다. 기계적으로 결정할 수 없는 것들은 상위의 AI가 오류 로그, 실패 화면, 테스트 코드를 읽고 재판단합니다. Limit의 상당 부분은 이 단계나 후속 단계의 재현 확인을 통해 결정됩니다. 권한 오류나 환경 고유의 실패는 오류 문구만으로는 기계적으로 분류할 수 없기 때문입니다.
같은 오류 문구라도 원인은 하나로 정해지지 않는다
분류 규칙을 만들면서 가장 효과를 본 깨달음이 이것입니다.
| 오류 문구 | 발생 가능한 원인 | 이 시스템에서의 처리 방식 |
|---|
Timeout ... exceeded (로케이터 대기 시간 초과) | 셀렉터 변경(Code) / 대기 부족(Code) / 기능 오류로 요소가 나타나지 않음(Defect) | 기계적으로 단정하지 않고, 상위 AI의 재판단으로 넘김. 그래도 결정되지 않으면 human-review |
waitForResponse 타임아웃 | 대기 부족(Code) / API 호출이 안 됨(Defect) | Code로 처리하고, 확신도는 수정 허용 하한선 바로 지점으로 설정함. '실제 브라우저 확인 필요' 표시가 붙지만, 수정 대상으로도 회귀함 (→과제1) |
intercepts pointer events | 오버레이 잔존(Code) | Code로 간주하여 자동 수정으로 진행 |
| 표시된 항목 수가 기대치와 1개 차이 날 때 (숫자를 값으로 비교한 경우) | 결함(Defect) / 테스트 데이터 배열 순서나 타이밍(Code) | 기계 단계에서는 결정적인 근거가 없다고 처리하며, Defect로 단정하지 않고 수정도 하지 않음. 상위 AI의 재판단으로 넘어가고, 거기서 Code로 판명되면 수정 대상으로 회귀할 수 있음 |
억지로 정규 표현식을 늘려 1행을 분리하는 것보다, 판단하지 않는 것이 더 안전했습니다.
특히 주의해야 할 것은 1행의 Defect 가능성입니다. '요소를 찾을 수 없음'은 셀렉터 변경뿐만 아니라 기능 오류로 요소 자체가 아예 나타나지 않을 때도 동일한 메시지가 됩니다. 이 부분을 상위 AI가 Code로 오판하여 자동 수정으로 보내면, 셀렉터를 교체하면서 결함을 덮어버릴(Masking) 위험이 있습니다. (→과제2)
'재현 확인'은 자동 리트라이(Automatic Retry)가 아니다
Defect 후보의 재현 확인은 단순한 리트라이(같은 테스트를 다시 실행하는 것)가 아닙니다. 그렇게 하면 flaky(실행할 때마다 결과가 흔들리는 불안정성)만 테스트하게 됩니다. 여기서는 MCP (AI가 실제 브라우저 같은 외부 도구를 조작하기 위한 메커니즘)를 통해, 다른 AI가 실제 브라우저를 조작하여 재현을 시도합니다.
- 'Defect로 판정되었다'는 사실은 전달하지만, 어떤 신호로 그렇게 판정했는지에 대한 근거는 제공하지 않습니다 (선입견 주입 방지).
- 로그인(또는 저장된 세션)부터 테스트 절차를 1단계씩 실제 화면에서 재실행합니다.
재현 확인 결과에 따른 경로는 다음과 같습니다.
| 재현 확인 결과 | 이동할 곳 | 자동 수정 여부 |
|---|
| 재현됨 | Defect 등록 후보 | 안 함 |
| ... | | 재현되지 않음 |
원칙적으로 하지 않습니다.
※ 재현 확인 AI가 결과를 반환하지 않았거나, 하향 조정(Degrade)할 곳을 제시하지 않은 경우, Defect 등록 후보 상태로 남게 됩니다. 이 결과를 분류에 반영하는 것은 사람이 수동으로 처리할 때입니다.
기대값 불일치로 Defect 후보가 된 항목은 재현 확인 전부터 '확인 필요' 표시와 함께 이슈 생성 후보 큐에 배열됩니다 (도식의 '이슈 생성 후보'는 확인 후 기준). Issue의 실제 생성(起票)은 재현 확인 결과를 보고 사람이 수행합니다.
스킵 이유를 기계가 읽을 수 있게 만들기
테스트에 스킵(skip)을 넣을 때는 (Limit으로 판정된 것을 사람이나 AI가 스킵 처리하는 경우, 처음부터 스킵하는 테스트 모두 포함) '이유'를 구조화하여 기록합니다. 작성자가 작성하는 것은 이유의 문구뿐이며, category와 같은 항목은 이유 문구로부터 규칙에 의해 자동으로 부여됩니다.
{
"category": "test-precondition-fail",
"autoSolvable": true,
...
}
category: 스킵의 종류 (seed 부족, 전제 조건 실패, 미구현, 환경 제한, 수동만 등) -
autoSolvable: 자동 해결 가능한지 여부. seed(테스트 데이터 초기 투입)를 갖추면 실행할 수 있는 것은 true
permanent: 영구적으로 수동으로만 확인 가능하여 자동화 대상에서 제외하는 것인지 (false)
false의 스킵은 품질 지표(Pass율 계열 KPI. 정의는 별도 글로 다룹니다)의 분모에서 제외되는 방식으로 처리됩니다. 따라서 autoSolvable: false는 기능 미구현, 환경 제한, 수동 확인만처럼 현재 실행할 수 없는 경우로 한정해야 합니다. 그렇지 않으면 '지금은 불가능하다'가 아닌 것을 false로 설정하여 원하는 만큼 스킵을 하고 분모를 줄일 구실이 생기기 때문입니다. 다만 코드 레벨에서 강제하지는 않았으며, 공통 규칙에도 '기대값이 모호함'과 같이 해결할 여지가 있는 경우를 false로 지정하는 패턴이 남아 있습니다 (→숙제6). 참고로, 전제 조건 체크에서 에러 페이지로 이동하여 멈춘 경우 역시 데이터 부족과 마찬가지로 autoSolvable: true의 스킵으로 처리됩니다. 품질 지표 분모에는 남기 때문에 '스킵에 묻혀 사라지는' 일은 없습니다. 이러한 스킵은 기능 미구현 스킵과는 category가 구분되지만, 데이터 부족과 환경 이상은 구별되지 못하고 있습니다. 다단계 판정 글인 'QA 엔지니어가 AI 생성 E2E 테스트의 「Pass / Fail의 이진 판정」을 그만둔 이야기'에서 과제로 삼았던 환경 이상에 대한 독립적인 분류는 아직 만들지 못했습니다 (→숙제5).
분류는 '프레임워크'와 '프로젝트별 데이터'로 분리하기
스킵의 분류 로직을 프로젝트마다 작성하면, 비슷한 판정이 여기저기 흩어져 이중 구현이 발생합니다. 그래서 판정 프레임워크(공통 라이브러리)와 프로젝트별 규칙 데이터로 분리했습니다. 프로젝트 측에서 가지는 것은 이런 규칙 목록뿐입니다.
[
{
"category": "seed-missing",
...
}
스킵 이유가 패턴과 일치하면 해당 category에 할당되고, 어느 것에도 일치하지 않으면 공통 규칙으로 폴백(fallback)합니다. 이 규칙 파일을 교체하는 것만으로도 다른 서비스에서도 동일한 분류기가 그대로 작동할 수 있습니다. 판정 기준을 수정할 때도 기본적으로는 이 규칙 파일과 공통 라이브러리만 보면 됩니다.
트리아지 큐에 쌓고, run 직하에 영속화하기
분류된 결과는 그 자리에서 사라지는 로그로 두지 않고, run(1회 실행)별 트리아지 큐로서 영속화합니다. 실행 디렉토리 바로 아래에 결과를 남김으로써, 나중에 '이 run에서 사람이 봐야 했던 것은 무엇인가'를 추적할 수 있게 했습니다.
분류 단계(여기서 말하는 리뷰의 진입점)에서 사람이 열어보는 것은 'Defect 이슈 생성 후보'와 'human-review 보류'(데이터 충돌로 사람 리뷰로 넘어간 Code)만으로 충분합니다. 수정되어 통과된 것이 정말 올바른지는 별도로 육안 검토를 통해 확인해야 합니다. Code는 자동 힐링(auto healing)으로, Limit은 환경 쪽으로 흐름이 분리되므로, 리뷰의 진입점이 한순간에 좁아집니다.
무엇이 달라지는가 (정성적)
| 관점 | 분류 없음 | 분류 있음 |
|---|
| 떨어진 후 사람이 보는 대상 | 떨어진 것을 순차적으로 열람 | Defect 후보 + 보류(+데이터 충돌 Code) |
| ... | | |
'테스트를 빠르게 돌리는 것' 다음에는 반드시 '떨어진 후 분류 작업'이 뒤따릅니다. 이 부분을 시스템으로 가볍게 만들지 않으면, 자동화 속도가 그대로 리뷰 부하로 변질됩니다. (공수 절감에 대한 정량적 검증은 앞으로 진행할 것이며, 현재는 운영 감각상의 체감이 전부입니다.)
설계 판단 메모
결함 단정의 기준을 높이다
AI에게 원인을 분류하게 할 때 가장 위험한 것은 '결함이라고 쉽게 말하는 것'입니다. 오보가 계속되면 개발팀은 알림 자체를 무시하게 됩니다. 따라서 확신이 없는 것은 보류 처리하는 것을 철저히 하고 있습니다.
분류는 기계가 주역, AI는 보조
어디까지를 기계적인 시그널로 판단하고, 어디부터를 AI에게 맡길지 경계를 짓는 것이 핵심입니다. 기계로 판단할 수 있는 부분을 AI에 넘기면 비용과 오류율이 증가합니다. 반대로, 기계가 처리할 수 없는 것을 무리하게 분류하려고 하면 오분류가 늘어납니다. '결정적인 근거가 없으면 판정하지 않는다'를 기계 단계의 원칙으로 삼고 있습니다.
현재 상황 및 과제
구조적으로는 여기까지 작동하고 있지만, 아직 막히지 않은 부분이 있습니다.
| 과제 | 현재 상태 | 다음 조치 |
|---|
1. waitForResponse 타임아웃의 신뢰도 | 자동 복구를 허용하는 하한선과 정확히 같은 값이므로, '실제 브라우저 확인 필요' 표시가 붙으면 동시에 자동 복구로 넘어감 | 임계값 설정 방식을 재검토할 것 |
| 2. AI가 Code로 판단한 '요소 없음' 오류 | 결함을 지우는(덮어쓰는) 수정이 발생할 수 있음. 안전장치는 사람의 검토뿐임 | 수정 후 기대값의 출처를 사양과 대조하는 시스템 구축 |
| 3. 복구 가능 여부 판정 | 실제로는 세분화된 분류(Code/Limit/Defect 아래에 붙는 원인별 라벨)로 결정되므로, 상위 AI의 판단 조합에 따라 Code 외의 것도 복구 대상으로 넘어올 수 있음 | 대분류와 세분류 간의 일관성을 게이트에서 검사할 것 |
| ... | 환경 이상을 별도의 카테고리로 분류 | |
6. autoSolvable: false 할당 | 공통 규칙에도, 처리 방법이 있는 것(기대값이 모호한 경우 등)을 false로 하는 패턴이 있음 | 실행할 수 없는 것에만 범위를 좁힐 것 |
결론
AI를 통해 테스트를 대량 생산할 수 있게 되면, 가치의 중심은 '작성'에서 '실패 후 어떻게 처리하느냐'로 이동합니다. 원인을 Code / Limit / Defect로 나누고, 결정적인 근거가 없는 것은 사람에게 넘기며, 스킵(skip) 이유를 기계가 읽을 수 있도록 하는 것—이 세 가지 점만으로도 리뷰의 진입 장벽을 상당히 좁힐 수 있습니다.
같은 방식으로 실패와 씨름하고 있는 팀들에게 참고가 되기를 바랍니다.
마지막으로…
주식회사 COMPASS에서는 함께 교육을 더 좋게 만들어 나갈 동료를 모집하고 있습니다.
조금이라도 흥미를 느끼신 분은 아래에서 부담 없이 지원해 주세요.
일단 이야기를 들어보고 싶으신 분은 꼭 캐주얼 면담에 와 주시면 감사하겠습니다.
Discussion