Flaky 테스트 지옥에서 탈출하는 방법|원인 5분류·진단부터 재발 방지까지 해설

「테스트가 실패해도 진짜 버그인지 알 수 없다」「CI가 빨간색이어도 아무도 신경 쓰지 않게 됐다」——그 Flaky 테스트 지옥에서 탈출하는 방법을 원인 분류·진단·대책·재발 방지까지 실무 경험을 바탕으로 해설합니다. Wait 설계·환경 차이·데이터 경합·외부 의존 등, Flaky의 종류별로 우선순위를 두고 대책을 소개합니다.

Flaky 테스트의 진짜 두려움은 「테스트가 실패한다」는 것이 아니라, 「테스트 결과를 아무도 신뢰하지 않게 된다」는 것입니다.

📌 이런 분께 추천합니다

  • 「CI가 실패해도 일단 재실행」이 습관이 된 QA 엔지니어
  • Flaky가 너무 많아 테스트 결과에 대한 신뢰가 사라진 팀 리드
  • Flaky가 「왜 일어나는지」 원인을 체계적으로 이해하고 싶은 분
  • Flaky의 재발 방지까지 포함한 근본 해결을 목표로 하는 분

✅ 이 기사를 읽으면 얻을 수 있는 것

  • Flaky 테스트의 종류 분류와 각각의 근본 원인을 알 수 있다
  • 「어떤 Flaky부터 우선적으로 고칠 것인가」의 판단 기준을 알 수 있다
  • 원인별 구체적인 대책 코드와 재발 방지 설계 패턴을 알 수 있다

👤 이 기사를 쓴 사람

QA 엔지니어·테스트 자동화 엔지니어로서 15년 이상의 실무 경험을 가진 Yoshi가 집필. 「CI가 빨간색이어도 아무도 신경 쓰지 않는」상태까지 악화된 Flaky 테스트 지옥을 여러 프로젝트에서 경험·복구한 실체험을 바탕으로 해설합니다.

📖 관련 기사와의 사용 구분

📌 결론 (3가지 포인트)

  • Flaky 테스트는 Wait·환경·데이터·외부 의존·설계의 5종류로 분류할 수 있으며, 종류가 다르면 대책도 다르다
  • 「일단 재실행」은 응급처치. 근본 해결은 「어떤 종류의 Flaky인가」를 특정하는 것부터 시작한다
  • Flaky가 사라지지 않는다면 「자동화해서는 안 되는 테스트」일 가능성이 있다. 삭제도 정답 중 하나

「또 Flaky야」——그 한마디가 일상이 됐을 때, 테스트 자동화는 기능을 잃었습니다. CI가 빨간색이어도 「어차피 Flaky겠지」라고 재실행할 뿐. 진짜 버그를 놓칠 위험이 높아지고, 결국 아무도 테스트 결과를 신뢰하지 않게 됩니다.

이 기사에서는 Flaky 테스트를 「대충 고치는」것이 아니라, 원인을 종류별로 분류하여 체계적으로 탈출하는 방법을 해설합니다.

Flaky 테스트란 무엇인가?왜 위험한가

Flaky 테스트(Flaky Test)란, 「같은 코드로 같은 환경일 텐데, 실행할 때마다 합격/불합격이 바뀌는 불안정한 테스트」를 말합니다.

단계팀의 상태리스크
초기「왠지 실패했지만 재실행했더니 통과됐다」경미
중기「어차피 Flaky겠지」라고 확인 없이 머지⚠️ 버그 놓침 리스크
말기「CI가 빨간색이어도 아무도 신경 쓰지 않는다」🔴 자동화의 가치가 제로로

Flaky 테스트의 종류와 근본 원인이란?5분류

종류전형적인 증상검지 방법주요 발생 도구수정 난이도
① Wait 계「요소를 찾을 수 없다」「클릭할 수 없다」재실행하면 통과Selenium / Playwright🟢 비교적 낮음
② 환경 의존 계「로컬은 통과하지만 CI에서 실패」CI 환경에서만 실패Selenium / Playwright🟡 중간
③ 데이터 경합 계「병렬 실행 시에만 실패」병렬 실행 시에만 실패전반🔴 높음
④ 외부 의존 계「외부 API 응답이 느리면 실패」특정 시간대·외부 장애 시에 집중API / E2E 테스트🟡 중간
⑤ 설계 기인 계「테스트 순서를 바꾸면 실패」실행 순서 변경으로 재현전반🔴 높음

먼저 확인:증상에서 Flaky 종류를 진단한다면?

이런 증상이 있다먼저 의심하는 Flaky 종류대책 STEP
재실행하면 통과됐다① Wait 계→ STEP 2로
로컬은 통과하지만 CI에서만 실패② 환경 의존 계→ STEP 3으로
병렬 실행(pytest-xdist)시에만 실패③ 데이터 경합 계→ STEP 4로
특정 시간대·외부 서비스 장애 시에 집중④ 외부 의존 계→ STEP 5로
테스트 순서를 바꾸면 재현된다⑤ 설계 기인 계→ STEP 6으로
어떤 조건에서도 재현 불가·대책해도 재발격리·삭제를 검토→ STEP 7로

STEP 1:먼저 Flaky를 계측·가시화한다란?

「고치기 전에 먼저 세어본다」——이것이 지옥 탈출의 첫 번째 단계입니다. 감각이 아닌 데이터로 파악함으로써 어디서부터 손을 댈지 우선순위가 정해집니다.

지표계측 방법판단 목안
Flaky율「실패한 횟수 ÷ 실행한 횟수」5% 이상 요대처(팀 규모·CI 빈도·재실행 운용에 따라 다르지만, 많은 현장에서 이 정도부터 「CI를 신뢰하지 않게 되는」경향이 강해짐)
영향 테스트 수CI 실패 로그에서 집계전체 테스트의 10% 초과는 위험 영역
CI 블록 시간재실행이 필요했던 횟수×평균 실행 시간주 1시간 초과는 업무 비용화
# ⚠️ pytest 표준 기능이 아닙니다. 사전 설치 필요:pip install pytest-repeat
# ※ CI 편입 시에는 실행 시간 증가 → Flaky 조사 용도로만 사용 권장
pytest tests/test_login.py --count=5

# pytest --junitxml=results.xml 로 결과 저장
# Datadog / Grafana / Allure Report / ReportPortal 등에서 장기 트렌드 확인

STEP 2:① Wait 계 Flaky의 대책이란?

가장 많고 비교적 고치기 쉬운 Flaky입니다(SPA나 virtual DOM 환경에서는 난이도가 올라가는 경우도 있습니다).

from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC
from selenium.webdriver.common.by import By
# 주:driver가 초기화되어 있는 것을 전제로 합니다(pytest fixture 또는 setUp()으로 생성)

# ❌ Flaky:time.sleep 고정 대기
import time
time.sleep(3)
driver.find_element(By.ID, "submit").click()

# ✅ 권장:WebDriverWait로 명시적 대기
wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "submit")))
element.click()

# ✅ 비동기로 disabled→enabled가 되는 버튼(위의 wait 정의 필요)
wait.until(lambda d: not d.find_element(By.ID, "submit").get_attribute("disabled"))

Playwright의 경우

from playwright.sync_api import expect
# Playwright는 click() 전에 visible/stable/enabled 등을 자동 확인하는 auto-waiting 기능을 가집니다
# 단 이것은 「그 순간의 상태 체크」이므로 비동기 상태 변화에는 expect()와의 병용이 필요합니다

# ✅ expect()로 enabled 상태를 확인하고 나서 클릭
expect(page.locator("#submit")).to_be_enabled()
page.locator("#submit").click()

# ✅ API 응답 후에 표시되는 콘텐츠
expect(page.locator(".search-results")).to_be_visible()
⚠️ Playwright로 이전해도 Flaky가 사라지지 않는 케이스:auto-waiting으로 개선되는 것은 주로 「Wait 설계 기인」의 Flaky입니다. 데이터 경합·외부 의존·설계 기인의 Flaky는 도구를 바꿔도 근본 해결이 되지 않습니다.

STEP 3:② 환경 의존 계 Flaky의 대책이란?

원인증상대책
viewport 차이CI에서 클릭 불가--window-size=1920,1080 고정
CI 성능 부족타임아웃이 CI에서만 빈발CI timeout을 로컬보다 크게 설정
Docker 이미지 차이브라우저 버전 불일치Docker 이미지의 Chrome 버전 고정
타임존날짜·시간 테스트가 CI에서 오동작TZ=UTC 를 CI 환경 변수에 설정

STEP 4:③ 데이터 경합 계 Flaky의 대책이란?

import pytest
import uuid

# ✅ 대책:테스트마다 유니크한 데이터를 생성한다
@pytest.fixture
def unique_user():
    """테스트마다 유니크한 유저를 생성하고 테스트 후 삭제"""
    email = f"test_{uuid.uuid4().hex[:8]}@example.com"
    user = create_user(email)
    yield user
    try:
        delete_user(user.id)
    except Exception as e:
        # cleanup 실패는 테스트 본체에 영향시키지 않는다
        # 실무에서는 logger.warning(f"cleanup failed: {e}") 으로 기록 권장
        pass

def test_update_user(unique_user):
    update_user(unique_user.id, name="Updated")
💡 실무 Tip:pytest --randomly-seed=random(pytest-randomly 플러그인)으로 테스트 순서를 셔플하면 순서 의존 Flaky가 즉시 발견됩니다.

STEP 5:④ 외부 의존 계 Flaky의 대책이란?

from unittest.mock import patch, MagicMock

# ✅ 대책①:외부 API를 목업화한다
@patch("requests.get")
def test_user_profile_mocked(mock_get):
    mock_get.return_value = MagicMock(status_code=200, json=lambda: {"id": 1})
    response = requests.get("https://api.example.com/users/1")
    assert response.status_code == 200

# ✅ 대책②:리트라이(외부 의존이 피할 수 없는 경우만)
from tenacity import retry, stop_after_attempt, wait_exponential

@retry(stop=stop_after_attempt(3), wait=wait_exponential(multiplier=1, min=1, max=4))
def call_external_api():
    return requests.get("https://api.example.com/users/1", timeout=(3, 10))

STEP 6:⑤ 설계 기인 계 Flaky의 대책이란?

import pytest

# ❌ 설계 기인 Flaky:상태 변경을 수반하는 scope="session"(scope="session" 자체가 나쁜 것은 아님)
# 읽기 전용 데이터·immutable한 설정값에서는 안전하게 사용 가능
@pytest.fixture(scope="session")
def logged_in_driver(driver):
    driver.get("/login")
    driver.find_element(By.ID, "submit").click()
    yield driver
    # 로그아웃 없음 → 다음 테스트에 상태가 남는다

# ✅ 대책:테스트마다 셋업·클린업을 완결시킨다
@pytest.fixture
def logged_in_driver(driver):
    driver.get("/login")
    driver.find_element(By.ID, "submit").click()
    yield driver
    driver.get("/logout")  # ← 매번 클린업

STEP 7:고칠 수 없는 Flaky는 격리·삭제를 검토한다란?

삭제에 의해 커버리지가 저하되고 리그레션 검지가 약해지는 트레이드오프가 있습니다. 삭제하는 경우는 대상 시나리오를 다른 수단(수동 테스트·단위 테스트)으로 담보하는 것이 조건입니다.

import pytest

# ✅ 방법①:일시적 격리(이유·날짜·이슈 링크를 반드시 기록)
@pytest.mark.skip(reason="Flaky: 원인 조사 중 (2026-05-01 by Yoshi) #issue-123")
def test_payment_flow():
    pass

# ✅ 방법②:실패 예정 마크
@pytest.mark.xfail(reason="외부 결제 API 불안정 - #issue-456", strict=False)
def test_external_payment():
    pass

# ✅ 방법③:flaky 전용 mark로 메인 CI에서 제외
# pytest.ini: markers = flaky: 불안정한 테스트
@pytest.mark.flaky
def test_unstable_feature():
    pass
# pytest -m "not flaky"  # 메인 CI
# pytest -m "flaky"      # 별도 감시 잡
💡 판단 기준:①3회 이상 대책해도 재발, ②수정에 1일 이상 걸리며 실행 빈도가 월 1회 이하, ③커버 시나리오를 다른 방법으로 담보 가능——2개 이상 해당하면 격리·삭제가 합리적입니다.
⚠️ 격리 후 감시가 중요:격리 = 방치가 되어서는 안 됩니다. 「격리한 채 잊혀지는」것이 최악입니다.

  • Slack 통지:flaky 마크 테스트 결과를 전용 채널에 자동 통지
  • Flaky 목록 관리:격리 일·담당자·이슈 링크를 스프레드시트나 Jira에서 관리
  • 주간 리뷰:3개월 이상 방치된 것은 삭제 판단

Flaky 테스트의 재발 방지:설계 단계에서 할 것

설계 규칙내용효과
테스트의 독립성각 테스트는 앞뒤 테스트에 의존하지 않는다🔴 데이터 경합 방지
유니크 데이터테스트 데이터는 매번 유니크하게 생성🔴 데이터 경합 방지
외부 의존의 목업화외부 API는 원칙적으로 목업으로 대체🔴 외부 의존 방지
명시적 대기time.sleep 폐지, expect()/WebDriverWait 사용🟢 Wait 계 방지
환경 통일Docker 이미지와 브라우저 버전을 고정🟡 환경 의존 방지
정기적인 Flaky 계측주간으로 Flaky율 확인·5% 초과하면 즉시 대처전반적인 조기 발견

FAQ

Q. 「일단 재실행」은 절대 NG인가요?

NG는 아니지만 응급처치에 불과합니다. 문제는 습관이 된 순간입니다. 재실행 결과를 기록하고 「이번 주 재실행이 필요했던 테스트 목록」을 정기적으로 확인하여 원인 조사에 활용하세요. 「기록하지 않는 재실행」이 지옥의 입구입니다.

Q. Playwright는 Selenium보다 Flaky가 적은가요?

Wait 설계 기인의 Flaky는 줄어듭니다. 단, 데이터 경합·외부 의존·설계 기인의 Flaky는 도구를 바꿔도 해결되지 않습니다. 「Playwright로 이전하면 Flaky 지옥에서 탈출할 수 있다」는 반은 맞는 말입니다.

Q. Flaky 테스트의 대응 우선순위는 어떻게 정하나요?

①Flaky율이 높은 것(5% 이상), ②CI를 블록하는 빈도가 높은 것, ③수정 난이도가 낮은 것(Wait 계)——이 3가지 축으로 스코어를 매겨 우선순위를 정하세요. 낮은 난이도의 Wait 계·환경 의존 계부터 해결하면 「Quick Win」을 얻어 모티베이션이 유지됩니다.

먼저 계측하여 가시화하고, 종류를 특정하여 우선순위를 두고 대처한다——이 순서로 진행하는 것이 지옥에서 영속적으로 탈출하기 위한 가장 효율적인 방법입니다.

📋 이 기사의 정리

  • Flaky의 진짜 두려움:테스트 결과를 아무도 신뢰하지 않게 된다. 계측·가시화가 첫 번째 단계
  • Flaky는 Wait 계·환경 의존 계·데이터 경합 계·외부 의존 계·설계 기인 계의 5종류로 분류 가능
  • 수정 난이도:Wait 계≤환경 의존 계≤외부 의존 계<데이터 경합 계≒설계 기인 계. 낮은 난이도부터 착수
  • Wait 계:time.sleep 폐지, WebDriverWait/expect()로 상태를 명시적으로 확인
  • 데이터 경합 계:유니크 데이터 생성, fixture에서 클린업 보증
  • 고쳐지지 않는 Flaky는 격리하고 계속 감시——격리한 채 잊혀지는 것이 최악의 패턴
  • 재발 방지 설계 규칙:독립성·유니크 데이터·목업화·환경 통일·정기 계측
제목과 URL을 복사했습니다