CI가 너무 느려서 아무도 보지 않게 됐다|실행 시간을 단축하는 7가지 방법

「CI가 끝날 때까지 기다릴 수 없다」「어차피 통과하겠지」——CI 실행 시간이 너무 길어서 생기는 「아무도 기다리지 않는·아무도 보지 않는」문화와, 실행 시간을 단축하는 7가지 구체적인 방법을 해설합니다. pytest-xdist 병렬화·Stage 분할·캐시 설계·목업화 등, 실무에서 바로 사용할 수 있는 CI 고속화 방법을 소개합니다.

CI는 「망가졌을 때」보다 「너무 느릴 때」 쪽이, 서서히 팀을 망가뜨립니다. 「30분 기다려서 빨간 CI」와 「3분만에 빨간 CI」는, 같은 실패여도 팀에 주는 영향이 전혀 다릅니다.

📌 이런 분께 추천합니다

  • CI 실행 시간이 10분을 초과하여 팀이 기다리는 것을 포기하고 있다
  • 「PR을 올리고 CI를 기다리지 않고 머지」가 습관이 된 분
  • GitHub Actions의 CI 고속화를 구체적인 방법으로 알고 싶은 QA 엔지니어
  • 테스트 스위트가 늘어날 때마다 CI가 느려지는 문제를 근본부터 해결하고 싶은 분

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

  • CI가 느려지는 원인의 종류와, 각각이 「아무도 보지 않게 된다」로 이어지는 구조를 알 수 있다
  • pytest-xdist·Stage 분할·캐시·목업화 등, 즉시 구현할 수 있는 고속화 방법을 알 수 있다
  • 「붕괴하기 전에 대처하는」조기 경계 체크리스트를 알 수 있다

👤 이 기사를 쓴 사람

QA 엔지니어·테스트 자동화 엔지니어로서 15년 이상의 실무 경험을 가진 Yoshi가 집필. 「CI를 아무도 기다리지 않게 된」프로젝트를 여러 번 경험하고, E2E 편중·병렬화 없음·캐시 없음 상태에서 실행 시간을 대폭 단축한 실적을 바탕으로 해설합니다.

📖 관련 기사와의 사용 구분

📌 결론 (3가지 포인트)

  • CI 고속화 우선도:①병렬화 →②Stage 분할 →③캐시 →④목업화 →⑤테스트 정리의 순서가 실무적으로 효과가 크다
  • 「CI를 아무도 기다리지 않게 되는」습관은 수개월 단위로 정착하기 시작하며, 방치 기간이 길수록 습관화하기 쉽다. 예조 단계에서 대처할 수 있다
  • CI 실행 시간의 목표는 이상적으로는 「5분 이내에 최초 피드백이 돌아오는 상태」. 10분을 초과하면 개선을 검토하는 현장이 많다

「CI 끝났어?」「아, 아직 돌고 있어. 일단 머지하자」——이 대화가 한 번이라도 일어났다면, CI의 속도 문제는 이미 문화 문제가 됐습니다.

Flaky 테스트와 달리, CI 속도 문제는 「테스트가 실패」하는 것이 아닙니다. 조용히, 서서히, 「CI를 확인하는 문화」를 침식해 갑니다. 이 기사에서는 CI 지연이 일으키는 붕괴 프로세스와, 구체적인 고속화 방법을 해설합니다.

CI 속도 문제가 「아무도 보지 않게 된다」로 이어지는 프로세스란?

단계팀의 행동실행 시간 목안
① 허용「조금 길지만 기다릴 수 있다」〜5분
② 이탈「CI 보면서 다른 작업을 한다」5〜10분
③ 무시「CI 기다리지 않고 머지」가 상태화10〜20분
④ 붕괴「CI가 빨간색이어도 일단 머지」20분 초과
⚠️ 「아무도 보지 않게 되는」것이 가장 위험한 이유:CI가 빨간색인 채로 머지가 계속되면 본번 환경에서의 장애 발생 리스크가 높아집니다. 「CI를 확인하지 않는 문화」가 정착하면, 속도 문제를 해소해도 팀의 습관은 쉽게 돌아오지 않습니다. 느려지는 속도보다, 문화가 무너지는 속도 쪽이 빠릅니다.

CI가 느려지는 7가지 원인

「왠지 느리다」인 채로 두면 영원히 개선되지 않습니다. 먼저 어떤 원인으로 느린지를 특정하는 것이 고속화의 첫 번째 단계입니다.

원인전형적인 증상속도 임팩트
① E2E 편중「테스트는 전부 E2E로 작성」방침이 되어 있다🔴 큼
② 병렬화 없음테스트를 순차 실행(1스레드로 순서대로 실행)🔴 큼
③ 캐시 없음매번 pip install・npm install을 처음부터 실행🔴 큼
④ 외부 I/O 의존테스트가 실제 DB・외부 API를 사용하고 있다🟡 중간
⑤ Stage 설계 없음모든 테스트를 1잡으로 일괄 실행하고 있다🟡 중간
⑥ 불필요한 sleeptime.sleep(3)이 각 테스트에 분산되어 있다🟡 중간
⑦ 테스트 비대화낡은·중복된 테스트가 정리되지 않았다🟢 작음〜중간

대책①:pytest-xdist로 병렬 실행한다

가장 즉효성이 높은 대책입니다. pytest-xdist를 사용하면 테스트를 여러 CPU 코어로 병렬 실행할 수 있습니다. 테스트 수가 많을수록 효과가 크고, 100건 이상의 테스트 스위트에서는 실행 시간이 2〜4배 단축되는 경우가 많습니다.

# 설치
# pip install pytest-xdist

# 로컬:CPU 코어 수를 자동 감지하여 병렬 실행
pytest tests/ -n auto

# GitHub Actions:워커 수를 명시적으로 지정
pytest tests/ -n 4

# CI 한정으로 유효화(권장 - 로컬 디버깅과의 충돌을 피하기 위해)
# pytest.ini의 addopts가 아닌 GitHub Actions의 run 스텝에 설정:
#   run: pytest tests/ -n auto
#
# ⚠️ addopts에 -n auto를 상시 설정하면
# VSCode Test Explorer・디버거・coverage 계측・Flaky 조사와 충돌이 발생할 수 있습니다
⚠️ 병렬화 전 주의사항:병렬 실행은 테스트의 독립성이 전제입니다. 테스트 간에 DB·파일·글로벌 상태를 공유하고 있으면 데이터 경합이 발생하여 Flaky가 됩니다. pytest --randomly-seed=1234(※pip install pytest-randomly가 필요)로 테스트 순서에 의존하지 않는지 확인하세요.
💡 실무 Tip:E2E 테스트(Playwright/Selenium)를 병렬화하는 경우는 브라우저 인스턴스가 테스트마다 독립되어 있는지 확인하세요. 상태 변경을 수반하는 scope="session" fixture(로그인 상태나 DB 쓰기를 수반하는 것)는 병렬 실행과 상성이 나쁩니다. 읽기 전용 데이터나 immutable한 설정값의 fixture는 scope="session"이어도 안전하게 사용할 수 있습니다.

대책②:CI를 Stage로 분할하여 조기 피드백을 얻는다

「모든 테스트가 끝나지 않으면 아무것도 알 수 없는」설계는 아무리 빠르게 해도 한계가 있습니다. 단위 테스트→통합 테스트→E2E 테스트의 순서로 Stage를 나누면 버그를 빠른 단계에서 감지할 수 있습니다.

# GitHub Actions Stage 분할 예(.github/workflows/test.yml)
# 참고:대규모 프로젝트에서는 reusable workflow나 composite action도 검토하세요
name: CI Pipeline
on: [push, pull_request]
jobs:
  # Stage 1:단위 테스트(목표:1분 이내)
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"              # 내장 pip 캐시 - 별도 캐시 스텝 불필요
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run unit tests
        run: pytest tests/unit/ -n auto --tb=short

  # Stage 2:통합 테스트(목표:3분 이내)
  # 단위 테스트가 통과한 경우에만 실행
  integration-tests:
    needs: unit-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Run integration tests
        run: pytest tests/integration/ -n auto

  # Stage 3:E2E 테스트(목표:5분 이내)
  # 통합 테스트가 통과한 경우에만 실행
  e2e-tests:
    needs: integration-tests
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"
      - name: Install dependencies
        run: pip install -r requirements.txt
      - name: Install Playwright browsers
        run: playwright install chromium --with-deps
      - name: Run E2E tests
        run: pytest tests/e2e/ -n 2

대책③:의존성 캐시로 매번의 설치를 생략한다

매번 pip install을 실행하면 그것만으로 2〜5분이 걸리는 경우가 있습니다. 의존성이 변하지 않은 경우는 캐시를 이용하여 설치 시간을 거의 제로로 할 수 있습니다.

# GitHub Actions 캐시 설정 예
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # ✅ 방법A:setup-python 내장 pip 캐시(간단)
      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"
          cache: "pip"   # requirements.txt가 변경됐을 때만 무효화

      - name: Install dependencies
        run: pip install -r requirements.txt

      # ✅ Playwright 브라우저 바이너리 캐시
      - name: Cache Playwright browsers
        uses: actions/cache@v4
        with:
          path: ~/.cache/ms-playwright
          key: playwright-${{ hashFiles('requirements.txt') }}

      - name: Install Playwright browsers(캐시가 없을 경우에만)
        run: playwright install chromium --with-deps
💡 캐시 키 설계:hashFiles('requirements.txt')를 사용하면 requirements.txt가 변경됐을 때만 캐시가 무효화됩니다. Playwright 브라우저 캐시는 특히 효과가 크고, 초회 설치(1〜3분)를 거의 매번 생략할 수 있습니다. actions/setup-python@v5cache: "pip" 옵션이 가장 간단하고 키 설계도 자동으로 처리해줍니다.

대책④:외부 I/O를 목업화하여 CI 환경의 대기 시간을 삭감한다

실제 DB・외부 API・메일 발송 등을 사용하는 테스트는, 네트워크 지연·외부 서비스의 응답 시간이 CI의 병목이 되는 경우가 있습니다.

from unittest.mock import patch, MagicMock
import requests
import pytest

from sqlalchemy import create_engine
from sqlalchemy.orm import Session, declarative_base

# Base 정의 예(실제 프로젝트에서는 모델 파일에서 선언 완료 상정)
# from sqlalchemy.orm import declarative_base
# Base = declarative_base()
Base = declarative_base()

# ❌ 느림:실제 외부 API를 매번 호출
def test_send_notification():
    result = requests.post(
        "https://api.slack.com/api/chat.postMessage",
        json={"channel": "#test", "text": "테스트 완료"}
    )
    assert result.status_code == 200
    # 네트워크 왕복으로 200〜500ms 걸린다

# ✅ 빠름:외부 API를 목업화하여 즉시 반환
@patch("requests.post")
def test_send_notification_fast(mock_post):
    mock_post.return_value = MagicMock(status_code=200)
    result = requests.post(
        "https://api.slack.com/api/chat.postMessage",
        json={"channel": "#test", "text": "테스트 완료"}
    )
    assert result.status_code == 200
    # 목업이므로 1ms 이하로 완료

# ✅ DB도 픽스처로 인메모리를 사용한다
@pytest.fixture
def db_session():
    """SQLite 인메모리 DB를 사용한 고속 DB 픽스처"""
    engine = create_engine("sqlite:///:memory:")
    Base.metadata.create_all(engine)
    with Session(engine) as session:
        yield session
    # with 블록을 벗어나면 자동으로 close・rollback

대책⑤:time.sleep을 배제하여 명시적 대기로 교체한다

100건의 테스트에 time.sleep(2)가 1개씩 있으면 그것만으로 3분 초과의 고정 로스가 됩니다. sleep은 Flaky의 원인이기도 하며, CI 고속화 관점에서도 맨 먼저 배제해야 할 대상입니다.

# time.sleep 사용 부분을 일괄 검색(asyncio.sleep 등과의 혼동을 피하기 위해 명시적으로 지정)
# grep -R "time.sleep(" ./tests

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

# ✅ 빠름:조건이 충족되면 즉시 다음으로
from selenium.webdriver.support.ui import WebDriverWait
from selenium.webdriver.support import expected_conditions as EC

wait = WebDriverWait(driver, 10)
element = wait.until(EC.element_to_be_clickable((By.ID, "result")))
element.click()
# 요소가 클릭 가능해진 순간에 실행(평균 0.3초, sleep보다 대폭 단축)

대책⑥:실행 시간이 긴 테스트를 가시화하여 정리한다

실행 시간 프로파일링으로 「느린 테스트 상위 10건」을 가시화하는 것만으로 개선의 우선순위가 명확해집니다.

# pytest 표준 기능으로 실행 시간이 긴 테스트를 표시(추가 플러그인 불필요)
pytest tests/ --durations=20

# 실무 권장:1초 이상 걸리는 테스트만 표시(짧은 테스트의 노이즈 제거)
pytest tests/ --durations=20 --durations-min=1.0

# ❌ 문제 있는 패턴:
# - 단위 테스트가 3초 이상 걸린다 → 외부 I/O가 혼입되어 있을 가능성
# - E2E 테스트가 20초 이상 걸린다 → 테스트의 스코프가 너무 넓을 가능성
# - 「같은 시나리오를 미묘하게 변경한」테스트가 다수 있다 → parametrize로 통합을 검토
💡 정리 판단 기준:①단위 테스트에서 3초 이상 걸린다(외부 I/O 혼입 의혹), ②E2E 테스트에서 30초 이상 걸린다(스코프를 분할할 수 없는지 검토), ③6개월 이상 실패도 수정도 되지 않았다, ④다른 테스트와 시나리오가 거의 중복된다——중 하나라도 해당되면 삭제·통합·이동을 검토하세요.

대책⑦:테스트 피라미드를 재검토하여 E2E 편중을 시정한다

CI가 근본적으로 느린 경우, 원인은 종종 「테스트 피라미드의 역전」에 있습니다. E2E 테스트만 늘어나고 단위·통합 테스트가 적은 상태가 이것입니다.

테스트 종류이상 비율실행 시간 목안커버하는 것
단위 테스트70%〜0.1초/건함수·클래스의 단체 동작
통합 테스트20%〜1초/건모듈 간 연계·API
E2E 테스트10%10〜30초/건주요 사용자 시나리오
⚠️ E2E로 검증하지 않아도 되는 것을 재검토:①유효성 검사 로직(필수 입력·문자 수 제한)→단위 테스트, ②API의 응답 형식·상태 코드→API 테스트(requests/Playwright API), ③에러 메시지 표시 조건→통합 테스트. E2E는 「사용자가 실제로 사용하는 주요 시나리오」에만 한정하는 것이 이상적입니다.

「아무도 보지 않게 되기 전에」대처하는 예조 체크리스트란?

3개 이상 해당되면 지금 당장 개선을 시작하는 신호입니다.

체크 항목해당?
CI 실행 시간이 10분을 초과하고 있다✅ / ❌
PR의 머지를 CI 완료 전에 행하는 경우가 있다✅ / ❌
테스트를 순차 실행(병렬화하지 않음)✅ / ❌
매번 pip install / npm install을 처음부터 실행하고 있다✅ / ❌
테스트 코드에 time.sleep()이 10곳 이상 있다✅ / ❌
E2E 테스트가 전체 테스트의 50% 이상을 차지하고 있다✅ / ❌
「전체 테스트가 끝나지 않으면 다음이 동작하지 않는」1잡 설계가 되어 있다✅ / ❌

🚀 지금 당장 시작하는 우선순위 TOP3

1위pytest-xdist로 병렬화(오늘 할 수 있다·효과 최대)
pip install pytest-xdist → CI의 run 스텝에 -n auto 추가
2위캐시 설정을 추가한다(1〜2시간으로 완료)
setup-python에 cache: "pip" 추가, Playwright 브라우저도 캐시
3위time.sleep 사용 부분을 grep으로 찾아낸다
grep -R "time.sleep(" ./tests로 전체 건수 확인, 상위부터 교체

FAQ

Q. CI 실행 시간의 목표값은 어느 정도가 이상적인가요?

많은 현장에서 5분 전후를 초과하면 대기율이 낮아지기 시작하는 경향이 있습니다. 10분을 초과하면 이탈율이 높아지기 시작하고, 20분 초과는 「아무도 기다리지 않는」문화가 정착하는 리스크가 높아집니다. 우선 현재 실행 시간을 계측하고, 「지금보다 30% 단축」이라는 목표부터 시작하는 것이 현실적입니다.

Q. pytest-xdist를 사용하면 테스트가 불안정해지는데요?

병렬화로 Flaky가 늘어나는 경우, 대부분의 원인은 테스트 간의 데이터 공유 또는 실행 순서에의 의존입니다. pytest --randomly-seed=1234로 순서 의존을 확인하고, 테스트 데이터는 uuid 등으로 고유화해 주세요. 상태 변경을 수반하는 scope="session" fixture가 가장 많은 원인이며 scope="function"으로 변경하면 대부분 해소됩니다.

Q. GitHub Actions의 캐시는 어느 정도 효과가 있나요?

의존성이 변하지 않는 경우 pip install의 2〜5분과 Playwright 브라우저 설치의 1〜3분이 생략됩니다. 합계로 매번 3〜8분 단축할 수 있는 경우가 많으며, 비용 대비 효과가 가장 높은 개선 중 하나입니다. actions/setup-python@v5cache: "pip"가 가장 간단한 구현입니다.

CI는 「코드 품질을 지키기 위한 것」이지만, 너무 느리면 「아무도 기다리지 않는 것」이 됩니다. 기술적인 문제인 것 같지만, 방치하면 문화적인 문제로 변합니다. 오늘 실행 시간을 계측하고, 1가지만 개선해 보세요. 「5분 CI를 기다릴 수 있는 팀」과 「30분 CI를 포기한 팀」은 1년 후 품질에 큰 차이가 생깁니다.

📋 이 기사의 정리

  • CI 속도 문제는 「문화 붕괴」를 일으킨다. CI를 기다리지 않는 습관은 수개월 단위로 정착하기 시작하며, 방치 기간이 길수록 습관화하기 쉽다
  • 대책 우선순위:①pytest-xdist 병렬화(효과 최대·오늘 할 수 있다)→②캐시 설정(3〜8분 단축)→③Stage 분할(조기 피드백)
  • grep -R "time.sleep(" ./tests로 찾아내어 WebDriverWait/expect()로 교체한다
  • E2E 편중은 CI 지연의 근본 원인. 단위 테스트 70%·통합 테스트 20%·E2E 10%의 피라미드를 목표로 한다
  • pytest --durations=20 --durations-min=1.0으로 느린 테스트 상위를 가시화한다(표준 기능·플러그인 불필요)
  • 병렬화로 Flaky가 늘어난 경우는 데이터 경합이 원인. 유니크 데이터 생성과 상태 변경을 수반하는 session scope fixture 재검토로 해소
제목과 URL을 복사했습니다