플레이키 테스트와의 싸움 — 신뢰할 수 없는 테스트가 신뢰를 좀먹는다
코드에도 테스트에도 변경이 없는데 실행할 때마다 합격하거나 실패하는 테스트를 '플레이키 테스트'라 부른다. 사소해 보이지만 '자주 떨어지지만 신경 쓰지 않아도 된다'는 분위기가 정착하면 테스트 스위트 전체의 신뢰가 무너지고 진짜 버그까지 놓치게 된다. 원인의 분류, 검지 기법, Google의 대규모 실측 데이터, 그리고 격리·재시도·근절이라는 각 접근의 공과, 이를 떠받치는 CI/CD의 구조를 따라간다.
“가끔 떨어진다”가 앗아가는 것
지난 글 커버리지와 뮤테이션 테스트에서는 “테스트의 질”을 어떻게 측정하는지 다뤘다. 하지만 아무리 질 높은 테스트를 작성해도, 그 테스트가 변덕스럽게 떨어진다면 스위트는 신뢰라는 가장 소중한 자산을 잃는다.
플레이키 테스트(flaky test)란 코드에도 테스트 코드에도 아무런 변경이 없는데 실행할 때마다 합격하거나 실패하는 테스트다. 진짜 버그로 인한 실패는 재현성이 있지만, 플레이키한 실패는 “다시 실행하니 통과했다”는 형태로 나타난다. 실패의 원인이 프로덕트 코드의 결함도, 테스트 로직의 오류도 아니라는 점 — 여기가 플레이키 테스트의 골치 아픔의 핵심이다.
언뜻 “가끔 떨어질 뿐인 사소한 문제”로 보인다. 그러나 그 실해는 테스트라는 영위의 토대 그 자체를 조용히 무너뜨려 간다.
왜 유해한가
플레이키 테스트의 해악은 수치에 잘 드러나지 않는 곳에 숨어 있다.
- 신뢰의 훼손: “빨강(실패)이 나와도 재실행하면 통과한다”는 경험이 쌓이면 CI의 실패 신호 자체에 대한 신뢰가 사라진다
- ‘양치기 소년’ 효과: 진짜 리그레션이 일어나도 “어차피 플레이키겠지”라고 넘겨져 버그가 프로덕션으로 흘러 나간다
- 개발 속도 저하: 실패가 진짜 버그인지 플레이키인지 분간하는 데 시간을 뺏긴다. 특히 릴리스 직전의 “빨강 상시화”는 배포를 늦춘다
- 심리적 비용: 원인 불명의 실패를 반복해서 보는 것은 개발자의 테스트에 대한 동기를 깎는다
테스트의 가치는 “변경을 두려워하지 않아도 되는 자신감”을 주는 데 있다. 플레이키 테스트는 그 자신감을 뒤에서 좀먹는다.
원인의 분류
플레이키함의 원인은 다양하지만 크게 다음과 같이 정리할 수 있다.
| 분류 | 전형적 예 |
|---|---|
| 비결정성 | 난수 시드 미고정, HashMap 등 순서 미정 컬렉션 의존, 부동소수점 반올림 |
| 시간·타임아웃 의존 | 현재 시각에 의존한 로직, CI 부하에 비해 너무 짧은 타임아웃, 날짜 경계(자정·월말)를 넘음 |
| 병행성·경합 조건 | 비동기 처리 완료를 기다리지 않고 어서트, 스레드 간 실행 순서에 의존 |
| 순서 의존 | 어떤 테스트가 후속 테스트의 전제 상태를 암묵적으로 만듦, 실행 순서가 바뀌면 실패 |
| 테스트 간 상태 공유 | 전역 변수·정적 필드·공유 DB 레코드의 정리 누락 |
| 외부 의존 | 실제 네트워크 너머의 외부 API 호출, 상대측 장애·지연의 영향을 받음 |
| 리소스 고갈 | CI 환경의 메모리·CPU 부족, 병렬 실행에 의한 포트 경합이나 디스크 부족 |
이들은 단독이 아니라 복합적으로 나타나는 일도 많다. 예컨대 “비동기 완료 대기가 불충분”한 테스트는 CI 환경의 CPU 부하가 높은 날에만 실패한다 — “병행성”과 “리소스 고갈”이 얽힌 형태다.
검지 — 재실행 차분과 플레이크율
플레이키 테스트를 찾는 가장 기본적인 방법은 동일 커밋에 대해 같은 테스트를 여러 번 실행해 결과가 흔들리는 것을 가려내는 것이다.
- 재실행 차분(rerun diff) 법: 실패한 테스트를 변경 없이 수 회~수십 회 재실행해, 한 번이라도 결과가 바뀌면 “플레이키”로 판정한다
- 플레이크율(flakiness rate) 계측: 일정 기간의 실행에 대한 “불일치 결과”의 비율을 지속적으로 기록하고, 임계값을 넘은 테스트를 자동 플래그한다
- 통계적 신호: “같은 커밋 해시에 대해 pass와 fail이 모두 관측되었다”는 사건을 CI 로그에서 기계적으로 추출한다
GitHub Actions, Jenkins, Buildkite 같은 CI나 테스트 리포팅 도구에는 이러한 재실행·플레이크 검지 기능을 갖춘 것이 늘어나, 검지 자체의 자동화가 정착되고 있다.
Google의 대규모 실측
플레이키 테스트가 “개별 프로젝트의 부주의”가 아니라 대규모 개발에 구조적으로 수반되는 과제임을 보여주는 일차 자료로, Google이 2016년 테스팅 블로그에 공개한 숫자가 자주 인용된다.
- 전체 테스트 중 약 16% 가 어떤 수준의 플레이키함을 보인다(엔지니어가 작성한 테스트 7개 중 1개 이상이 우발적으로 실패할 수 있다)
- 테스트 실행 전체로 보면 약 1.5% 의 실행 결과가 “플레이키”로 판정된다
- pass에서 fail로의 전이 중 약 84% 가 플레이키 테스트에 의한 것으로, 이것이 “새로운 실패가 진짜인지 아닌지”의 조사 비용을 크게 끌어올린다
동사는 나아가 플레이키 테스트를 제거하는 노력과 새로 혼입되는 노력이 거의 같은 페이스로 진행되어, “일정한 플레이크율과 계속 공존한다”는 구조적 상태에 있다고 지적한다.
auto-wait — E2E라는 최대의 온상에 대한 구조적 대책
E2E·브라우저 테스트의 세대교체에서 보았듯, UI 렌더링 완료·비동기 통신·애니메이션 종료 등 타이밍에 기인하는 플레이키함은 E2E 층에 특히 많다. 구세대(Selenium 등)에서는 고정 시간의 sleep가 많이 쓰였고, 이 자체가 “너무 짧으면 실패, 너무 길면 느림”이라는 온상이 되었다.
Cypress나 Playwright 같은 신세대 도구는 auto-wait(자동 대기) 를 기본 탑재해, 요소가 실제로 조작 가능한 상태(표시되고, 애니메이션이 끝나고, 비활성화되지 않은 등)가 될 때까지 자동으로 폴링하며 기다린다. 이로써 고정 sleep 의존을 줄이고 플레이키함의 주요 발생원 중 하나를 구조적으로 완화했다.
격리와 재시도 — 응급처치의 공과
플레이키로 판명된 테스트에 대한 현실적 운용이 둘 있다. 둘 다 “공존책”이지 “해결책”이 아니다.
quarantine(격리) 는 플레이키한 테스트를 CI의 합불 판정에서 일시적으로 빼고 별도 트랙에서 계속 실행하는 운용이다. 본류 CI를 막지 않아 개발 속도를 지킬 수 있는 이점이 있는 반면, “일시적”이어야 할 것이 방치되어 항구화되기 쉽다. 격리된 기간 동안 그 테스트는 진짜 리그레션을 검지하는 힘을 잃는다. 격리 목록이 끝없이 늘면 스위트 전체의 실효 커버리지가 줄어든다.
재시도는 실패한 테스트를 자동으로 몇 번 재실행해 한 번이라도 성공하면 합격 취급하는 운용이다. 구현이 쉽고 즉효성이 있는 반면, 진짜 리그레션(프로덕트 측의 타이밍 버그)을 “재시도로 통과했으니 문제없음”으로 덮어 가릴 위험이 있다. 재시도 횟수가 부풀고 실행 시간도 늘어난다.
어느 쪽이든 누가·언제까지 고칠지라는 소유권과 세트가 아니면, 단순한 “못 본 척”의 제도화로 끝난다.
근절로 — 설계 문제로서의 플레이키함
본질적 대처는 원인을 분류표에 따라 특정하고 설계 수준에서 제거하는 것이다.
- hermetic화: 테스트가 필요로 하는 상태(시각·난수·외부 의존)를 전부 테스트 자신이 제어한다(대규모 개발의 테스트의 hermetic tests 참조)
- 시각·난수 고정: 시스템 클록에 대한 직접 의존을 그만두고, 주입 가능한 시각 취득·난수 시드로 대체한다
- 테스트의 독립성: 각 테스트가 자신이 필요로 하는 데이터·상태를 자기 완결적으로 셋업·파기해 암묵적 순서 의존을 끊는다
- 비동기의 명시적 완료 대기:
sleep가 아니라 대상의 상태 변화를 폴링하거나 콜백/Promise의 완료를 기다린다 - 외부 의존의 목킹: 실제 네트워크 호출을 테스트 더블로 대체해 외부 서비스의 가용성·레이턴시로부터 분리한다
플레이키 테스트의 상당수는 설계상의 문제(암묵적 의존, 경계의 결여)가 증상으로 나타난 것이라 이해할 수 있다.
CI/CD라는 무대
플레이키함이 가장 아픔을 동반하는 것은 테스트가 자동으로 계속 돌아가는 CI/CD 파이프라인 위다. 현대의 파이프라인은 단위→통합→E2E로 단계적으로 테스트를 실행해, 빠르고 저렴한 것을 먼저, 느리고 비싼 것을 나중에 배치한다(fail-fast). 테스트 수가 늘면 병렬화·샤딩으로 실행 시간을 억제하고, 변경에 관련된 테스트만 고르는 테스트 영향 분석(Test Impact Analysis) 으로 낭비를 줄인다.
이 맥락에서 “머지 전에 테스트를 통과시키는” 것이 게이트가 되는 이상, 플레이키 테스트는 게이트 그 자체를 불안정하게 만든다. 그렇기에 플레이크 검지·격리·재시도 같은 운용은 CI/CD의 신뢰성을 지키기 위한 구조로 편입된다. 테스트의 자동화는 테스트의 신뢰성이라는 전제 위에서만 가치를 낳는다.
정리
플레이키 테스트는 “가끔 떨어질 뿐인 사소한 문제”가 아니라 스위트 전체에 대한 신뢰를 조용히 좀먹는 구조적 과제다. 원인은 비결정성·시간 의존·병행성·상태 공유·외부 의존 등 다양하며, Google의 실측이 보여주듯 대규모 현장에서는 무시할 수 없는 규모로 발생한다. 격리나 재시도는 증상을 완화하는 현실적 운용이지만, 근본 해결에는 설계 수준에서의 독립성·hermetic성 확보가 필요하다.
여기까지 본 테스트는 모두 “실행해서 확인하는” 영위였다. 그러나 실행에 의한 테스트에는 원리적 한계가 있다 — 제1화에서 놓은 Dijkstra의 경구 “테스트는 버그의 존재는 보일 수 있지만 부재는 보일 수 없다”이다. 다음 글에서는 이 한계에 다른 측면에서 도전하는 형식 기법을 다룬다.
참고문헌
- John Micco, “Flaky Tests at Google and How We Mitigate Them” (Google Testing Blog, 2016)
- Google Testing Blog, “Where do our flaky tests come from?” (2017)
- 각 E2E 도구(Cypress / Playwright) 공식 문서의 auto-wait / retry 관련 기술