왜 배포 파이프라인이 느려지는가

배포 시간은 단순히 "느리다"로 끝나지 않는다. 파이프라인이 느리면 롤백까지의 시간이 길어지고, 개발자는 여러 변경을 한 번에 몰아 배포하게 되어 장애 원인 격리가 어려워진다. 문제는 전체 시간이 아니라 크리티컬 패스(critical path)다. 병렬로 도는 스테이지가 아무리 많아도, 최종 완료 시점을 결정하는 것은 가장 오래 걸리는 의존성 사슬 하나다. 따라서 최적화는 항상 "지금 크리티컬 패스에 있는 스테이지가 무엇인가"를 찾는 데서 시작한다.

병목은 감이 아니라 데이터로 찾는다

대부분의 CI 시스템은 스테이지별 소요 시간을 로그나 API로 노출한다. 여러 번의 실행을 모아 스테이지별 p50/p95를 집계하면, 평균은 짧지만 꼬리가 긴(간헐적으로 폭발하는) 스테이지가 드러난다. 이런 스테이지가 진짜 위험하다.

-- GitHub Actions 잡 타이밍을 적재한 테이블 기준
SELECT
  job_name,
  COUNT(*)                                   AS runs,
  ROUND(AVG(duration_sec))                   AS avg_sec,
  PERCENTILE_CONT(duration_sec, 0.5) OVER () AS p50,
  PERCENTILE_CONT(duration_sec, 0.95) OVER () AS p95
FROM ci_job_runs
WHERE started_at > NOW() - INTERVAL '14 days'
GROUP BY job_name, duration_sec
ORDER BY p95 DESC
LIMIT 10;

p95가 큰 순서로 정렬하면 최적화 우선순위가 자동으로 결정된다. 평균만 보면 놓치는 "가끔 10분 걸리는" 스테이지를 잡아낼 수 있다.

의존성 그래프를 다시 그린다

많은 파이프라인이 실제로는 병렬 가능한 작업을 직렬로 묶어둔다. 대표적으로 lint → test → build → deploy를 무조건 순차 실행하는 경우다. lint와 unit test는 서로 독립적이므로 병렬화할 수 있고, 이미지 빌드는 test와 겹쳐 시작해도 되는 경우가 많다.

jobs:
  lint:
    runs-on: ubuntu-latest
    steps: [{ uses: ./.github/actions/lint }]
  unit-test:
    runs-on: ubuntu-latest
    steps: [{ uses: ./.github/actions/test }]
  build-image:
    # test 통과를 기다리지 않고 lint만 확인되면 시작
    needs: [lint]
    runs-on: ubuntu-latest
    steps: [{ uses: ./.github/actions/build }]
  deploy:
    needs: [unit-test, build-image]  # 실제 게이트는 여기서만
    runs-on: ubuntu-latest
    steps: [{ uses: ./.github/actions/deploy }]

핵심은 needs를 "관행"이 아니라 "실제 데이터 의존성"으로만 채우는 것이다. deploy 앞에만 진짜 게이트를 두면 크리티컬 패스가 짧아진다.

캐시로 재계산을 없앤다

테스트·빌드 스테이지의 상당 부분은 매번 처음부터 다시 하는 낭비다. 의존성 설치, 빌드 산출물, 도커 레이어를 캐시하면 크리티컬 패스 위의 스테이지 자체가 짧아진다.

# 잠금 파일 해시를 키로 사용 → 의존성 변경 시에만 무효화
- uses: actions/cache@v4
  with:
    path: ~/.cache/pip
    key: pip-${{ hashFiles('requirements.lock') }}
    restore-keys: pip-

# 도커 빌드는 레이어 캐시 재사용
- run: docker build --cache-from type=registry,ref=$IMG:cache
                    --cache-to   type=registry,ref=$IMG:cache,mode=max
                    -t $IMG:${{ github.sha }} .

주의할 점은 캐시 키 설계다. 키가 너무 넓으면 오래된 산출물이 재사용되어 "내 PC에선 되는데" 류의 미묘한 버그를 만들고, 너무 좁으면 캐시가 거의 안 맞는다. 잠금 파일 해시를 키로 쓰는 방식이 가장 안전하다.

비교: 어디에 손대야 효과가 큰가

기법대상 병목기대 효과리스크
병렬화(needs 재정의)불필요한 직렬 의존중~높음낮음
의존성/레이어 캐시반복 재계산높음캐시 무효화 오류
테스트 분할·샤딩단일 긴 테스트 잡높음플래키 테스트 노출
러너 스펙 상향CPU/IO 바운드 잡중비용 증가

측정 없는 최적화는 피한다

가장 흔한 실수는 크리티컬 패스에 없는 스테이지를 최적화하는 것이다. 병렬로 도는 3분짜리 lint를 1분으로 줄여도, 크리티컬 패스가 12분짜리 통합 테스트라면 전체 배포 시간은 1초도 줄지 않는다. 반드시 "이 스테이지가 완료 시점을 결정하는가"를 먼저 확인해야 한다.

변경 후 검증과 재측정

최적화 후에는 다시 p95를 측정해 크리티컬 패스가 실제로 이동했는지 확인한다. 병목을 하나 없애면 다음 병목이 크리티컬 패스로 올라오기 때문에, 이 과정은 한 번으로 끝나지 않는 반복 루프다. 또한 병렬화·샤딩은 플래키 테스트를 드러내므로, 실패율을 함께 모니터링해 "빨라졌지만 불안정해진" 상태를 방지해야 한다. 결국 배포 속도 개선은 측정 → 크리티컬 패스 식별 → 한 스테이지 개선 → 재측정의 순환으로 관리하는 것이 안전하고 지속 가능하다.